From e2e3929dc6a7a2bdf454ecc5b4956f473865327d Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 4 Aug 2026 13:59:46 -0700 Subject: [PATCH 1/4] session: scoped customization enablement Customizations gain an optional `enablement` array of explicit decisions, one per scope that has one: CustomizationEnablement = | { kind: 'global'; enabled: boolean } | { kind: 'workspace'; uri: URI; enabled: boolean } | { kind: 'session'; enabled: boolean } The array is a wire contract. Producers MUST publish entries sorted by descending specificity (session, workspace, then global), and the agent host emits at most one workspace entry, for the session's primary working directory. Consumers MAY therefore treat `enablement[0]` as decisive, with `enablement?.[0]?.enabled ?? true` as the effective value. An absent or empty array means no explicit decision, so the customization is enabled by default. Only the host publishes this; clients treat it as read-only provenance. The field lives on the customization base rather than on MCP servers alone, so it applies to every customization type. `session/customizationToggled` carries `enablement` in place of `enabled` and replaces the complete decision set, so a caller changing one scope must include every decision it intends to preserve. An empty array clears all decisions and restores the default. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- clients/go/ahp/reducers.go | 41 +++- clients/go/ahptypes/actions.generated.go | 9 +- clients/go/ahptypes/state.generated.go | 188 +++++++++++++++- .../microsoft/agenthostprotocol/Reducers.kt | 41 ++-- .../generated/Actions.generated.kt | 6 +- .../generated/State.generated.kt | 212 +++++++++++++++++- clients/rust/crates/ahp-types/src/actions.rs | 20 +- clients/rust/crates/ahp-types/src/state.rs | 145 +++++++++++- clients/rust/crates/ahp/src/reducers.rs | 73 ++++-- .../Generated/Actions.generated.swift | 10 +- .../Generated/State.generated.swift | 209 ++++++++++++++++- .../AgentHostProtocol/NativeReducer.swift | 32 ++- .../Sources/AgentHostProtocol/Reducers.swift | 2 +- ...60804-scoped-customization-enablement.json | 4 + docs/guide/actions.md | 4 +- docs/guide/customizations.md | 46 +++- schema/actions.schema.json | 155 ++++++++++++- schema/commands.schema.json | 155 ++++++++++++- schema/errors.schema.json | 155 ++++++++++++- schema/notifications.schema.json | 140 +++++++++++- schema/state.schema.json | 140 +++++++++++- scripts/generate-go.ts | 76 ++++++- scripts/generate-kotlin.ts | 78 ++++++- scripts/generate-rust.ts | 30 ++- scripts/generate-swift.ts | 71 +++++- types/channels-session/actions.ts | 10 +- types/channels-session/reducer.ts | 24 +- types/channels-session/state.ts | 34 ++- ...on-customizationtoggled-toggles-by-id.json | 33 ++- ...zationtoggled-is-no-op-for-unknown-id.json | 7 +- ...s-no-op-when-customizations-undefined.json | 7 +- ...tomizationtoggled-toggles-child-by-id.json | 13 +- ...toggled-is-no-op-for-unknown-child-id.json | 7 +- ...ustomizationtoggled-clears-enablement.json | 62 +++++ 34 files changed, 2127 insertions(+), 112 deletions(-) create mode 100644 docs/.changes/20260804-scoped-customization-enablement.json create mode 100644 types/test-cases/reducers/263-session-customizationtoggled-clears-enablement.json diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index 9d2a34161..7b093c9c0 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -367,39 +367,68 @@ func containerChildren(c *ahptypes.Customization) *[]ahptypes.ChildCustomization return nil } -func setContainerEnabled(c *ahptypes.Customization, enabled bool) { +func effectiveEnablement(enablement []ahptypes.CustomizationEnablement) bool { + if len(enablement) == 0 { + return true + } + switch decision := enablement[0].Value.(type) { + case *ahptypes.CustomizationEnablementGlobal: + return decision.Enabled + case *ahptypes.CustomizationEnablementWorkspace: + return decision.Enabled + case *ahptypes.CustomizationEnablementSession: + return decision.Enabled + default: + return true + } +} + +func applyContainerEnablement(c *ahptypes.Customization, enablement []ahptypes.CustomizationEnablement) { + enabled := effectiveEnablement(enablement) + provenance := append([]ahptypes.CustomizationEnablement(nil), enablement...) switch v := c.Value.(type) { case *ahptypes.PluginCustomization: v.Enabled = enabled + v.Enablement = provenance case *ahptypes.DirectoryCustomization: v.Enabled = enabled + v.Enablement = provenance case *ahptypes.McpServerCustomization: v.Enabled = enabled + v.Enablement = provenance } } -func setChildEnabled(c *ahptypes.ChildCustomization, enabled bool) { +func applyChildEnablement(c *ahptypes.ChildCustomization, enablement []ahptypes.CustomizationEnablement) { + enabled := effectiveEnablement(enablement) + provenance := append([]ahptypes.CustomizationEnablement(nil), enablement...) switch v := c.Value.(type) { case *ahptypes.AgentCustomization: v.Enabled = &enabled + v.Enablement = provenance case *ahptypes.SkillCustomization: v.Enabled = &enabled + v.Enablement = provenance case *ahptypes.PromptCustomization: v.Enabled = &enabled + v.Enablement = provenance case *ahptypes.RuleCustomization: v.Enabled = &enabled + v.Enablement = provenance case *ahptypes.HookCustomization: v.Enabled = &enabled + v.Enablement = provenance case *ahptypes.McpServerCustomization: v.Enabled = enabled + v.Enablement = provenance } } -func applyToggle(list []ahptypes.Customization, id string, enabled bool) bool { +func applyToggle(list []ahptypes.Customization, id string, enablement []ahptypes.CustomizationEnablement) bool { for i := range list { got, ok := customizationID(list[i]) if ok && got == id { - setContainerEnabled(&list[i], enabled) + applyContainerEnablement(&list[i], enablement) return true } } @@ -411,7 +440,7 @@ func applyToggle(list []ahptypes.Customization, id string, enabled bool) bool { for j := range *children { got, ok := childCustomizationID((*children)[j]) if ok && got == id { - setChildEnabled(&(*children)[j], enabled) + applyChildEnablement(&(*children)[j], enablement) return true } } @@ -938,7 +967,7 @@ func ApplyActionToSession(state *ahptypes.SessionState, action ahptypes.StateAct if state.Customizations == nil { return ReduceOutcomeNoOp } - if applyToggle(state.Customizations, a.Id, a.Enabled) { + if applyToggle(state.Customizations, a.Id, a.Enablement) { return ReduceOutcomeApplied } return ReduceOutcomeNoOp diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index fccdb904b..d9a98a24d 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -992,12 +992,15 @@ type SessionCustomizationsChangedAction struct { // `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`. +// +// The `enablement` array completely replaces all explicit decisions. A caller +// changing one scope must include every decision it intends to preserve. type SessionCustomizationToggledAction struct { Type ActionType `json:"type"` - // The id of the container or child to toggle. + // The id of the container or child to update. Id string `json:"id"` - // Whether to enable or disable the targeted customization. - Enabled bool `json:"enabled"` + // The complete set of explicit decisions, replacing any existing set. + Enablement []CustomizationEnablement `json:"enablement"` } // Upserts a top-level customization (plugin or directory). diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 99693a7bb..182d11b37 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -320,6 +320,15 @@ const ( CustomizationTypeMcpServer CustomizationType = "mcpServer" ) +// Scope at which customization enablement is decided. +type CustomizationEnablementKind string + +const ( + CustomizationEnablementKindGlobal CustomizationEnablementKind = "global" + CustomizationEnablementKindWorkspace CustomizationEnablementKind = "workspace" + CustomizationEnablementKindSession CustomizationEnablementKind = "session" +) + // Discriminant values for {@link CustomizationLoadState}. type CustomizationLoadStatus string @@ -2384,6 +2393,18 @@ type PluginCustomization struct { Uri URI `json:"uri"` // Human-readable name. Name string `json:"name"` + // Explicit enablement decisions for this customization, one entry per scope + // that has one. This is a wire contract: producers MUST publish entries + // sorted by descending specificity (Session, Workspace, then Global). + // The agent host emits at most one Workspace entry, for the session's primary + // working directory. Consumers MAY treat + // `enablement[0]` as the decisive decision and + // `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + // absent or empty array means no explicit decision exists, so the + // customization is enabled by default. + // + // Only the agent host publishes this; clients treat it as read-only provenance. + Enablement []CustomizationEnablement `json:"enablement,omitempty"` // Icons for UI display. Icons []Icon `json:"icons,omitempty"` // Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -2445,6 +2466,18 @@ type ClientPluginCustomization struct { Uri URI `json:"uri"` // Human-readable name. Name string `json:"name"` + // Explicit enablement decisions for this customization, one entry per scope + // that has one. This is a wire contract: producers MUST publish entries + // sorted by descending specificity (Session, Workspace, then Global). + // The agent host emits at most one Workspace entry, for the session's primary + // working directory. Consumers MAY treat + // `enablement[0]` as the decisive decision and + // `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + // absent or empty array means no explicit decision exists, so the + // customization is enabled by default. + // + // Only the agent host publishes this; clients treat it as read-only provenance. + Enablement []CustomizationEnablement `json:"enablement,omitempty"` // Icons for UI display. Icons []Icon `json:"icons,omitempty"` // Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -2508,6 +2541,18 @@ type DirectoryCustomization struct { Uri URI `json:"uri"` // Human-readable name. Name string `json:"name"` + // Explicit enablement decisions for this customization, one entry per scope + // that has one. This is a wire contract: producers MUST publish entries + // sorted by descending specificity (Session, Workspace, then Global). + // The agent host emits at most one Workspace entry, for the session's primary + // working directory. Consumers MAY treat + // `enablement[0]` as the decisive decision and + // `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + // absent or empty array means no explicit decision exists, so the + // customization is enabled by default. + // + // Only the agent host publishes this; clients treat it as read-only provenance. + Enablement []CustomizationEnablement `json:"enablement,omitempty"` // Icons for UI display. Icons []Icon `json:"icons,omitempty"` // Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -2562,6 +2607,18 @@ type AgentCustomization struct { Uri URI `json:"uri"` // Human-readable name. Name string `json:"name"` + // Explicit enablement decisions for this customization, one entry per scope + // that has one. This is a wire contract: producers MUST publish entries + // sorted by descending specificity (Session, Workspace, then Global). + // The agent host emits at most one Workspace entry, for the session's primary + // working directory. Consumers MAY treat + // `enablement[0]` as the decisive decision and + // `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + // absent or empty array means no explicit decision exists, so the + // customization is enabled by default. + // + // Only the agent host publishes this; clients treat it as read-only provenance. + Enablement []CustomizationEnablement `json:"enablement,omitempty"` // Icons for UI display. Icons []Icon `json:"icons,omitempty"` // Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -2634,6 +2691,18 @@ type SkillCustomization struct { Uri URI `json:"uri"` // Human-readable name. Name string `json:"name"` + // Explicit enablement decisions for this customization, one entry per scope + // that has one. This is a wire contract: producers MUST publish entries + // sorted by descending specificity (Session, Workspace, then Global). + // The agent host emits at most one Workspace entry, for the session's primary + // working directory. Consumers MAY treat + // `enablement[0]` as the decisive decision and + // `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + // absent or empty array means no explicit decision exists, so the + // customization is enabled by default. + // + // Only the agent host publishes this; clients treat it as read-only provenance. + Enablement []CustomizationEnablement `json:"enablement,omitempty"` // Icons for UI display. Icons []Icon `json:"icons,omitempty"` // Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -2689,6 +2758,18 @@ type PromptCustomization struct { Uri URI `json:"uri"` // Human-readable name. Name string `json:"name"` + // Explicit enablement decisions for this customization, one entry per scope + // that has one. This is a wire contract: producers MUST publish entries + // sorted by descending specificity (Session, Workspace, then Global). + // The agent host emits at most one Workspace entry, for the session's primary + // working directory. Consumers MAY treat + // `enablement[0]` as the decisive decision and + // `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + // absent or empty array means no explicit decision exists, so the + // customization is enabled by default. + // + // Only the agent host publishes this; clients treat it as read-only provenance. + Enablement []CustomizationEnablement `json:"enablement,omitempty"` // Icons for UI display. Icons []Icon `json:"icons,omitempty"` // Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -2743,6 +2824,18 @@ type RuleCustomization struct { Uri URI `json:"uri"` // Human-readable name. Name string `json:"name"` + // Explicit enablement decisions for this customization, one entry per scope + // that has one. This is a wire contract: producers MUST publish entries + // sorted by descending specificity (Session, Workspace, then Global). + // The agent host emits at most one Workspace entry, for the session's primary + // working directory. Consumers MAY treat + // `enablement[0]` as the decisive decision and + // `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + // absent or empty array means no explicit decision exists, so the + // customization is enabled by default. + // + // Only the agent host publishes this; clients treat it as read-only provenance. + Enablement []CustomizationEnablement `json:"enablement,omitempty"` // Icons for UI display. Icons []Icon `json:"icons,omitempty"` // Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -2796,6 +2889,18 @@ type HookCustomization struct { Uri URI `json:"uri"` // Human-readable name. Name string `json:"name"` + // Explicit enablement decisions for this customization, one entry per scope + // that has one. This is a wire contract: producers MUST publish entries + // sorted by descending specificity (Session, Workspace, then Global). + // The agent host emits at most one Workspace entry, for the session's primary + // working directory. Consumers MAY treat + // `enablement[0]` as the decisive decision and + // `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + // absent or empty array means no explicit decision exists, so the + // customization is enabled by default. + // + // Only the agent host publishes this; clients treat it as read-only provenance. + Enablement []CustomizationEnablement `json:"enablement,omitempty"` // Icons for UI display. Icons []Icon `json:"icons,omitempty"` // Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -2847,6 +2952,18 @@ type McpServerCustomization struct { Uri URI `json:"uri"` // Human-readable name. Name string `json:"name"` + // Explicit enablement decisions for this customization, one entry per scope + // that has one. This is a wire contract: producers MUST publish entries + // sorted by descending specificity (Session, Workspace, then Global). + // The agent host emits at most one Workspace entry, for the session's primary + // working directory. Consumers MAY treat + // `enablement[0]` as the decisive decision and + // `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + // absent or empty array means no explicit decision exists, so the + // customization is enabled by default. + // + // Only the agent host publishes this; clients treat it as read-only provenance. + Enablement []CustomizationEnablement `json:"enablement,omitempty"` // Icons for UI display. Icons []Icon `json:"icons,omitempty"` // Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -2861,7 +2978,8 @@ type McpServerCustomization struct { // out-of-band. Meta map[string]json.RawMessage `json:"_meta,omitempty"` Type CustomizationType `json:"type"` - // Whether this MCP server is currently enabled. + // Whether this MCP server is effectively enabled after resolving all scopes. + // {@link CustomizationBase.enablement | `enablement`} records its inputs. Enabled bool `json:"enabled"` // Current lifecycle state of the MCP server. State McpServerState `json:"state"` @@ -3557,6 +3675,74 @@ type ResourceChange struct { Type ResourceChangeType `json:"type"` } +// ─── Customization Enablement Union ─────────────────────────────────────── + +// CustomizationEnablement is a single explicit customization enablement decision. +type CustomizationEnablement struct { + Value isCustomizationEnablement +} + +type isCustomizationEnablement interface{ isCustomizationEnablement() } + +type CustomizationEnablementGlobal struct { + Kind string `json:"kind"` + Enabled bool `json:"enabled"` +} + +func (*CustomizationEnablementGlobal) isCustomizationEnablement() {} + +type CustomizationEnablementWorkspace struct { + Kind string `json:"kind"` + URI URI `json:"uri"` + Enabled bool `json:"enabled"` +} + +func (*CustomizationEnablementWorkspace) isCustomizationEnablement() {} + +type CustomizationEnablementSession struct { + Kind string `json:"kind"` + Enabled bool `json:"enabled"` +} + +func (*CustomizationEnablementSession) isCustomizationEnablement() {} + +func (e *CustomizationEnablement) UnmarshalJSON(data []byte) error { + disc, _, err := readDiscriminator(data, "kind") + if err != nil { + return err + } + switch disc { + case "global": + var value CustomizationEnablementGlobal + if err := json.Unmarshal(data, &value); err != nil { + return err + } + e.Value = &value + case "workspace": + var value CustomizationEnablementWorkspace + if err := json.Unmarshal(data, &value); err != nil { + return err + } + e.Value = &value + case "session": + var value CustomizationEnablementSession + if err := json.Unmarshal(data, &value); err != nil { + return err + } + e.Value = &value + default: + return &json.UnmarshalTypeError{Value: "CustomizationEnablement"} + } + return nil +} + +func (e CustomizationEnablement) MarshalJSON() ([]byte, error) { + if e.Value == nil { + return []byte("null"), nil + } + return json.Marshal(e.Value) +} + // ToolInput is raw tool input represented inline or by content reference. type ToolInput struct { Inline *string 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 6222ed855..0ab85ee99 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -271,21 +271,36 @@ private fun withCustomizationChildren(c: Customization, children: List c } -private fun withCustomizationEnabled(c: Customization, enabled: Boolean): Customization = when (c) { - is CustomizationPlugin -> CustomizationPlugin(c.value.copy(enabled = enabled)) - is CustomizationDirectory -> CustomizationDirectory(c.value.copy(enabled = enabled)) - is CustomizationMcpServer -> CustomizationMcpServer(c.value.copy(enabled = enabled)) +private fun effectiveEnablement(enablement: List): Boolean = when (val decision = enablement.firstOrNull()) { + is CustomizationEnablement.Global -> decision.value.enabled + is CustomizationEnablement.Workspace -> decision.value.enabled + is CustomizationEnablement.Session -> decision.value.enabled + null -> true +} + +private fun withCustomizationEnablement(c: Customization, enablement: List): Customization { + val enabled = effectiveEnablement(enablement) + val provenance = enablement.takeIf { it.isNotEmpty() }?.toList() + return when (c) { + is CustomizationPlugin -> CustomizationPlugin(c.value.copy(enabled = enabled, enablement = provenance)) + is CustomizationDirectory -> CustomizationDirectory(c.value.copy(enabled = enabled, enablement = provenance)) + is CustomizationMcpServer -> CustomizationMcpServer(c.value.copy(enabled = enabled, enablement = provenance)) 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)) +private fun withChildCustomizationEnablement(c: ChildCustomization, enablement: List): ChildCustomization { + val enabled = effectiveEnablement(enablement) + val provenance = enablement.takeIf { it.isNotEmpty() }?.toList() + return when (c) { + is ChildCustomizationAgent -> ChildCustomizationAgent(c.value.copy(enabled = enabled, enablement = provenance)) + is ChildCustomizationSkill -> ChildCustomizationSkill(c.value.copy(enabled = enabled, enablement = provenance)) + is ChildCustomizationPrompt -> ChildCustomizationPrompt(c.value.copy(enabled = enabled, enablement = provenance)) + is ChildCustomizationRule -> ChildCustomizationRule(c.value.copy(enabled = enabled, enablement = provenance)) + is ChildCustomizationHook -> ChildCustomizationHook(c.value.copy(enabled = enabled, enablement = provenance)) + is ChildCustomizationMcpServer -> ChildCustomizationMcpServer(c.value.copy(enabled = enabled, enablement = provenance)) is ChildCustomizationUnknown -> c + } } private fun childCustomizationId(c: ChildCustomization): String? = when (c) { @@ -685,7 +700,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat val idx = list.indexOfFirst { customizationId(it) == a.id } if (idx >= 0) { val updated = list.toMutableList() - updated[idx] = withCustomizationEnabled(updated[idx], a.enabled) + updated[idx] = withCustomizationEnablement(updated[idx], a.enablement) state.copy(customizations = updated) } else run { for (i in list.indices) { @@ -693,7 +708,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat val childIdx = children.indexOfFirst { childCustomizationId(it) == a.id } if (childIdx < 0) continue val newChildren = children.toMutableList() - newChildren[childIdx] = withChildCustomizationEnabled(newChildren[childIdx], a.enabled) + newChildren[childIdx] = withChildCustomizationEnablement(newChildren[childIdx], a.enablement) val updated = list.toMutableList() updated[i] = withCustomizationChildren(list[i], newChildren) return@run state.copy(customizations = updated) diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt index cde9f9ede..b0bc05d55 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 @@ -1058,13 +1058,13 @@ data class SessionCustomizationsChangedAction( data class SessionCustomizationToggledAction( val type: ActionType, /** - * The id of the container or child to toggle. + * The id of the container or child to update. */ val id: String, /** - * Whether to enable or disable the targeted customization. + * The complete set of explicit decisions, replacing any existing set. */ - val enabled: Boolean + val enablement: List ) @Serializable 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 d6fe2b4e6..638fc681f 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 @@ -557,6 +557,19 @@ enum class CustomizationType { MCP_SERVER } +/** + * Scope at which customization enablement is decided. + */ +@Serializable +enum class CustomizationEnablementKind { + @SerialName("global") + GLOBAL, + @SerialName("workspace") + WORKSPACE, + @SerialName("session") + SESSION +} + /** * Discriminant values for {@link CustomizationLoadState}. */ @@ -3256,6 +3269,20 @@ data class PluginCustomization( * Human-readable name. */ val name: String, + /** + * Explicit enablement decisions for this customization, one entry per scope + * that has one. This is a wire contract: producers MUST publish entries + * sorted by descending specificity (Session, Workspace, then Global). + * The agent host emits at most one Workspace entry, for the session's primary + * working directory. Consumers MAY treat + * `enablement[0]` as the decisive decision and + * `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + * absent or empty array means no explicit decision exists, so the + * customization is enabled by default. + * + * Only the agent host publishes this; clients treat it as read-only provenance. + */ + val enablement: List? = null, /** * Icons for UI display. */ @@ -3332,6 +3359,20 @@ data class ClientPluginCustomization( * Human-readable name. */ val name: String, + /** + * Explicit enablement decisions for this customization, one entry per scope + * that has one. This is a wire contract: producers MUST publish entries + * sorted by descending specificity (Session, Workspace, then Global). + * The agent host emits at most one Workspace entry, for the session's primary + * working directory. Consumers MAY treat + * `enablement[0]` as the decisive decision and + * `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + * absent or empty array means no explicit decision exists, so the + * customization is enabled by default. + * + * Only the agent host publishes this; clients treat it as read-only provenance. + */ + val enablement: List? = null, /** * Icons for UI display. */ @@ -3412,6 +3453,20 @@ data class DirectoryCustomization( * Human-readable name. */ val name: String, + /** + * Explicit enablement decisions for this customization, one entry per scope + * that has one. This is a wire contract: producers MUST publish entries + * sorted by descending specificity (Session, Workspace, then Global). + * The agent host emits at most one Workspace entry, for the session's primary + * working directory. Consumers MAY treat + * `enablement[0]` as the decisive decision and + * `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + * absent or empty array means no explicit decision exists, so the + * customization is enabled by default. + * + * Only the agent host publishes this; clients treat it as read-only provenance. + */ + val enablement: List? = null, /** * Icons for UI display. */ @@ -3487,6 +3542,20 @@ data class AgentCustomization( * Human-readable name. */ val name: String, + /** + * Explicit enablement decisions for this customization, one entry per scope + * that has one. This is a wire contract: producers MUST publish entries + * sorted by descending specificity (Session, Workspace, then Global). + * The agent host emits at most one Workspace entry, for the session's primary + * working directory. Consumers MAY treat + * `enablement[0]` as the decisive decision and + * `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + * absent or empty array means no explicit decision exists, so the + * customization is enabled by default. + * + * Only the agent host publishes this; clients treat it as read-only provenance. + */ + val enablement: List? = null, /** * Icons for UI display. */ @@ -3579,6 +3648,20 @@ data class SkillCustomization( * Human-readable name. */ val name: String, + /** + * Explicit enablement decisions for this customization, one entry per scope + * that has one. This is a wire contract: producers MUST publish entries + * sorted by descending specificity (Session, Workspace, then Global). + * The agent host emits at most one Workspace entry, for the session's primary + * working directory. Consumers MAY treat + * `enablement[0]` as the decisive decision and + * `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + * absent or empty array means no explicit decision exists, so the + * customization is enabled by default. + * + * Only the agent host publishes this; clients treat it as read-only provenance. + */ + val enablement: List? = null, /** * Icons for UI display. */ @@ -3655,6 +3738,20 @@ data class PromptCustomization( * Human-readable name. */ val name: String, + /** + * Explicit enablement decisions for this customization, one entry per scope + * that has one. This is a wire contract: producers MUST publish entries + * sorted by descending specificity (Session, Workspace, then Global). + * The agent host emits at most one Workspace entry, for the session's primary + * working directory. Consumers MAY treat + * `enablement[0]` as the decisive decision and + * `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + * absent or empty array means no explicit decision exists, so the + * customization is enabled by default. + * + * Only the agent host publishes this; clients treat it as read-only provenance. + */ + val enablement: List? = null, /** * Icons for UI display. */ @@ -3718,6 +3815,20 @@ data class RuleCustomization( * Human-readable name. */ val name: String, + /** + * Explicit enablement decisions for this customization, one entry per scope + * that has one. This is a wire contract: producers MUST publish entries + * sorted by descending specificity (Session, Workspace, then Global). + * The agent host emits at most one Workspace entry, for the session's primary + * working directory. Consumers MAY treat + * `enablement[0]` as the decisive decision and + * `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + * absent or empty array means no explicit decision exists, so the + * customization is enabled by default. + * + * Only the agent host publishes this; clients treat it as read-only provenance. + */ + val enablement: List? = null, /** * Icons for UI display. */ @@ -3792,6 +3903,20 @@ data class HookCustomization( * Human-readable name. */ val name: String, + /** + * Explicit enablement decisions for this customization, one entry per scope + * that has one. This is a wire contract: producers MUST publish entries + * sorted by descending specificity (Session, Workspace, then Global). + * The agent host emits at most one Workspace entry, for the session's primary + * working directory. Consumers MAY treat + * `enablement[0]` as the decisive decision and + * `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + * absent or empty array means no explicit decision exists, so the + * customization is enabled by default. + * + * Only the agent host publishes this; clients treat it as read-only provenance. + */ + val enablement: List? = null, /** * Icons for UI display. */ @@ -3851,6 +3976,20 @@ data class McpServerCustomization( * Human-readable name. */ val name: String, + /** + * Explicit enablement decisions for this customization, one entry per scope + * that has one. This is a wire contract: producers MUST publish entries + * sorted by descending specificity (Session, Workspace, then Global). + * The agent host emits at most one Workspace entry, for the session's primary + * working directory. Consumers MAY treat + * `enablement[0]` as the decisive decision and + * `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + * absent or empty array means no explicit decision exists, so the + * customization is enabled by default. + * + * Only the agent host publishes this; clients treat it as read-only provenance. + */ + val enablement: List? = null, /** * Icons for UI display. */ @@ -3873,7 +4012,8 @@ data class McpServerCustomization( val meta: Map? = null, val type: CustomizationType, /** - * Whether this MCP server is currently enabled. + * Whether this MCP server is effectively enabled after resolving all scopes. + * {@link CustomizationBase.enablement | `enablement`} records its inputs. */ val enabled: Boolean, /** @@ -4662,6 +4802,76 @@ data class ResourceChange( val type: ResourceChangeType ) +// ─── Customization Enablement Union ───────────────────────────────────── + +/** + * A single explicit customization enablement decision. + */ +@Serializable(with = CustomizationEnablementSerializer::class) +sealed interface CustomizationEnablement { + @JvmInline value class Global(val value: CustomizationEnablementGlobal) : CustomizationEnablement + @JvmInline value class Workspace(val value: CustomizationEnablementWorkspace) : CustomizationEnablement + @JvmInline value class Session(val value: CustomizationEnablementSession) : CustomizationEnablement +} + +@Serializable +data class CustomizationEnablementGlobal( + val enabled: Boolean, + val kind: String = "global", +) + +@Serializable +data class CustomizationEnablementWorkspace( + val uri: URI, + val enabled: Boolean, + val kind: String = "workspace", +) + +@Serializable +data class CustomizationEnablementSession( + val enabled: Boolean, + val kind: String = "session", +) + +internal object CustomizationEnablementSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("CustomizationEnablement") + + override fun deserialize(decoder: Decoder): CustomizationEnablement { + val input = decoder as? JsonDecoder + ?: error("CustomizationEnablement can only be deserialized from JSON") + val element = input.decodeJsonElement() + val obj = element as? JsonObject + ?: error("Expected JsonObject for CustomizationEnablement") + return when ((obj["kind"] as? JsonPrimitive)?.contentOrNull) { + "global" -> CustomizationEnablement.Global( + input.json.decodeFromJsonElement(CustomizationEnablementGlobal.serializer(), element), + ) + "workspace" -> CustomizationEnablement.Workspace( + input.json.decodeFromJsonElement(CustomizationEnablementWorkspace.serializer(), element), + ) + "session" -> CustomizationEnablement.Session( + input.json.decodeFromJsonElement(CustomizationEnablementSession.serializer(), element), + ) + else -> error("Unknown CustomizationEnablement kind") + } + } + + override fun serialize(encoder: Encoder, value: CustomizationEnablement) { + val output = encoder as? JsonEncoder + ?: error("CustomizationEnablement can only be serialized to JSON") + val element: JsonElement = when (value) { + is CustomizationEnablement.Global -> + output.json.encodeToJsonElement(CustomizationEnablementGlobal.serializer(), value.value) + is CustomizationEnablement.Workspace -> + output.json.encodeToJsonElement(CustomizationEnablementWorkspace.serializer(), value.value) + is CustomizationEnablement.Session -> + output.json.encodeToJsonElement(CustomizationEnablementSession.serializer(), value.value) + } + output.encodeJsonElement(element) + } +} + // ─── Tool Input ────────────────────────────────────────────────────────────── /** diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index 849a339c5..0f4f83af6 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -16,11 +16,12 @@ use crate::state::{ AgentInfo, AgentSelection, Annotation, AnnotationEntry, Changeset, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, ChangesetStatus, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ChatSummary, - ConfirmationOption, ContentRef, Customization, ErrorInfo, McpAuthRequirement, McpServerState, - Message, ModelSelection, PendingMessageKind, ResponsePart, SessionActiveClient, - SessionInputRequest, SideChatSelection, TerminalClaim, TerminalInfo, TextRange, - ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallContributor, ToolCallResult, - ToolCallRiskAssessment, ToolDefinition, ToolInput, ToolResultContent, Turn, UsageInfo, + ConfirmationOption, ContentRef, Customization, CustomizationEnablement, ErrorInfo, + McpAuthRequirement, McpServerState, Message, ModelSelection, PendingMessageKind, ResponsePart, + SessionActiveClient, SessionInputRequest, SideChatSelection, TerminalClaim, TerminalInfo, + TextRange, ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallContributor, + ToolCallResult, ToolCallRiskAssessment, ToolDefinition, ToolInput, ToolResultContent, Turn, + UsageInfo, }; // ─── ActionType ────────────────────────────────────────────────────── @@ -1168,13 +1169,16 @@ pub struct SessionCustomizationsChangedAction { /// `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`. +/// +/// The `enablement` array completely replaces all explicit decisions. A caller +/// changing one scope must include every decision it intends to preserve. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionCustomizationToggledAction { - /// The id of the container or child to toggle. + /// The id of the container or child to update. pub id: String, - /// Whether to enable or disable the targeted customization. - pub enabled: bool, + /// The complete set of explicit decisions, replacing any existing set. + pub enablement: Vec, } /// Upserts a top-level customization (plugin or directory). diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 0e9f5cba7..bdedec6dc 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -446,6 +446,17 @@ pub enum CustomizationType { McpServer, } +/// Scope at which customization enablement is decided. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum CustomizationEnablementKind { + #[serde(rename = "global")] + Global, + #[serde(rename = "workspace")] + Workspace, + #[serde(rename = "session")] + Session, +} + /// Discriminant values for {@link CustomizationLoadState}. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum CustomizationLoadStatus { @@ -2913,6 +2924,19 @@ pub struct PluginCustomization { pub uri: Uri, /// Human-readable name. pub name: String, + /// Explicit enablement decisions for this customization, one entry per scope + /// that has one. This is a wire contract: producers MUST publish entries + /// sorted by descending specificity (Session, Workspace, then Global). + /// The agent host emits at most one Workspace entry, for the session's primary + /// working directory. Consumers MAY treat + /// `enablement[0]` as the decisive decision and + /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + /// absent or empty array means no explicit decision exists, so the + /// customization is enabled by default. + /// + /// Only the agent host publishes this; clients treat it as read-only provenance. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enablement: Option>, /// Icons for UI display. #[serde(default, skip_serializing_if = "Option::is_none")] pub icons: Option>, @@ -2982,6 +3006,19 @@ pub struct ClientPluginCustomization { pub uri: Uri, /// Human-readable name. pub name: String, + /// Explicit enablement decisions for this customization, one entry per scope + /// that has one. This is a wire contract: producers MUST publish entries + /// sorted by descending specificity (Session, Workspace, then Global). + /// The agent host emits at most one Workspace entry, for the session's primary + /// working directory. Consumers MAY treat + /// `enablement[0]` as the decisive decision and + /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + /// absent or empty array means no explicit decision exists, so the + /// customization is enabled by default. + /// + /// Only the agent host publishes this; clients treat it as read-only provenance. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enablement: Option>, /// Icons for UI display. #[serde(default, skip_serializing_if = "Option::is_none")] pub icons: Option>, @@ -3054,6 +3091,19 @@ pub struct DirectoryCustomization { pub uri: Uri, /// Human-readable name. pub name: String, + /// Explicit enablement decisions for this customization, one entry per scope + /// that has one. This is a wire contract: producers MUST publish entries + /// sorted by descending specificity (Session, Workspace, then Global). + /// The agent host emits at most one Workspace entry, for the session's primary + /// working directory. Consumers MAY treat + /// `enablement[0]` as the decisive decision and + /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + /// absent or empty array means no explicit decision exists, so the + /// customization is enabled by default. + /// + /// Only the agent host publishes this; clients treat it as read-only provenance. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enablement: Option>, /// Icons for UI display. #[serde(default, skip_serializing_if = "Option::is_none")] pub icons: Option>, @@ -3115,6 +3165,19 @@ pub struct AgentCustomization { pub uri: Uri, /// Human-readable name. pub name: String, + /// Explicit enablement decisions for this customization, one entry per scope + /// that has one. This is a wire contract: producers MUST publish entries + /// sorted by descending specificity (Session, Workspace, then Global). + /// The agent host emits at most one Workspace entry, for the session's primary + /// working directory. Consumers MAY treat + /// `enablement[0]` as the decisive decision and + /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + /// absent or empty array means no explicit decision exists, so the + /// customization is enabled by default. + /// + /// Only the agent host publishes this; clients treat it as read-only provenance. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enablement: Option>, /// Icons for UI display. #[serde(default, skip_serializing_if = "Option::is_none")] pub icons: Option>, @@ -3197,6 +3260,19 @@ pub struct SkillCustomization { pub uri: Uri, /// Human-readable name. pub name: String, + /// Explicit enablement decisions for this customization, one entry per scope + /// that has one. This is a wire contract: producers MUST publish entries + /// sorted by descending specificity (Session, Workspace, then Global). + /// The agent host emits at most one Workspace entry, for the session's primary + /// working directory. Consumers MAY treat + /// `enablement[0]` as the decisive decision and + /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + /// absent or empty array means no explicit decision exists, so the + /// customization is enabled by default. + /// + /// Only the agent host publishes this; clients treat it as read-only provenance. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enablement: Option>, /// Icons for UI display. #[serde(default, skip_serializing_if = "Option::is_none")] pub icons: Option>, @@ -3260,6 +3336,19 @@ pub struct PromptCustomization { pub uri: Uri, /// Human-readable name. pub name: String, + /// Explicit enablement decisions for this customization, one entry per scope + /// that has one. This is a wire contract: producers MUST publish entries + /// sorted by descending specificity (Session, Workspace, then Global). + /// The agent host emits at most one Workspace entry, for the session's primary + /// working directory. Consumers MAY treat + /// `enablement[0]` as the decisive decision and + /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + /// absent or empty array means no explicit decision exists, so the + /// customization is enabled by default. + /// + /// Only the agent host publishes this; clients treat it as read-only provenance. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enablement: Option>, /// Icons for UI display. #[serde(default, skip_serializing_if = "Option::is_none")] pub icons: Option>, @@ -3320,6 +3409,19 @@ pub struct RuleCustomization { pub uri: Uri, /// Human-readable name. pub name: String, + /// Explicit enablement decisions for this customization, one entry per scope + /// that has one. This is a wire contract: producers MUST publish entries + /// sorted by descending specificity (Session, Workspace, then Global). + /// The agent host emits at most one Workspace entry, for the session's primary + /// working directory. Consumers MAY treat + /// `enablement[0]` as the decisive decision and + /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + /// absent or empty array means no explicit decision exists, so the + /// customization is enabled by default. + /// + /// Only the agent host publishes this; clients treat it as read-only provenance. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enablement: Option>, /// Icons for UI display. #[serde(default, skip_serializing_if = "Option::is_none")] pub icons: Option>, @@ -3381,6 +3483,19 @@ pub struct HookCustomization { pub uri: Uri, /// Human-readable name. pub name: String, + /// Explicit enablement decisions for this customization, one entry per scope + /// that has one. This is a wire contract: producers MUST publish entries + /// sorted by descending specificity (Session, Workspace, then Global). + /// The agent host emits at most one Workspace entry, for the session's primary + /// working directory. Consumers MAY treat + /// `enablement[0]` as the decisive decision and + /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + /// absent or empty array means no explicit decision exists, so the + /// customization is enabled by default. + /// + /// Only the agent host publishes this; clients treat it as read-only provenance. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enablement: Option>, /// Icons for UI display. #[serde(default, skip_serializing_if = "Option::is_none")] pub icons: Option>, @@ -3437,6 +3552,19 @@ pub struct McpServerCustomization { pub uri: Uri, /// Human-readable name. pub name: String, + /// Explicit enablement decisions for this customization, one entry per scope + /// that has one. This is a wire contract: producers MUST publish entries + /// sorted by descending specificity (Session, Workspace, then Global). + /// The agent host emits at most one Workspace entry, for the session's primary + /// working directory. Consumers MAY treat + /// `enablement[0]` as the decisive decision and + /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + /// absent or empty array means no explicit decision exists, so the + /// customization is enabled by default. + /// + /// Only the agent host publishes this; clients treat it as read-only provenance. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enablement: Option>, /// Icons for UI display. #[serde(default, skip_serializing_if = "Option::is_none")] pub icons: Option>, @@ -3453,7 +3581,8 @@ pub struct McpServerCustomization { /// out-of-band. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, - /// Whether this MCP server is currently enabled. + /// Whether this MCP server is effectively enabled after resolving all scopes. + /// {@link CustomizationBase.enablement | `enablement`} records its inputs. pub enabled: bool, /// Current lifecycle state of the MCP server. pub state: McpServerState, @@ -4258,6 +4387,20 @@ pub struct ResourceChange { pub r#type: ResourceChangeType, } +// ─── Customization Enablement Union ─────────────────────────────────────── + +/// A single explicit customization enablement decision. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "kind")] +pub enum CustomizationEnablement { + #[serde(rename = "global")] + Global { enabled: bool }, + #[serde(rename = "workspace")] + Workspace { uri: Uri, enabled: bool }, + #[serde(rename = "session")] + Session { enabled: bool }, +} + /// Raw tool input represented inline or by content reference. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(untagged)] diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 027f48e06..2a6ef0955 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -59,7 +59,8 @@ use ahp_types::actions::{ }; use ahp_types::state::{ ActiveTurn, AnnotationsState, ChangesetOperationStatus, ChangesetState, ChangesetStatus, - ChatInputRequest, ChatState, ChildCustomization, ConfirmationOption, Customization, ErrorInfo, + ChatInputRequest, ChatState, ChildCustomization, ConfirmationOption, Customization, + CustomizationEnablement, ErrorInfo, InputRequestResponsePart, McpServerStartingState, McpServerState, McpServerStoppedState, PendingMessage, PendingMessageKind, ResourceWatchState, ResponsePart, RootState, SessionInputRequest, SessionLifecycle, SessionState, SessionStatus, TerminalCommandPart, @@ -496,36 +497,76 @@ fn container_children_mut(c: &mut Customization) -> Option<&mut Vec bool { + match enablement.first() { + Some(CustomizationEnablement::Global { enabled }) => *enabled, + Some(CustomizationEnablement::Workspace { enabled, .. }) => *enabled, + Some(CustomizationEnablement::Session { enabled }) => *enabled, + None => true, + } +} + +fn apply_container_enablement(c: &mut Customization, enablement: &[CustomizationEnablement]) { + let enabled = effective_enablement(enablement); + let provenance = (!enablement.is_empty()).then(|| enablement.to_vec()); match c { - Customization::Plugin(p) => p.enabled = enabled, - Customization::Directory(d) => d.enabled = enabled, - Customization::McpServer(m) => m.enabled = enabled, + Customization::Plugin(p) => { + p.enabled = enabled; + p.enablement = provenance; + } + Customization::Directory(d) => { + d.enabled = enabled; + d.enablement = provenance; + } + Customization::McpServer(m) => { + m.enabled = enabled; + m.enablement = provenance; + } Customization::Unknown(_) => {} } } -fn set_child_enabled(c: &mut ChildCustomization, enabled: bool) { +fn apply_child_enablement(c: &mut ChildCustomization, enablement: &[CustomizationEnablement]) { + let enabled = effective_enablement(enablement); + let provenance = (!enablement.is_empty()).then(|| enablement.to_vec()); 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::Agent(x) => { + x.enabled = Some(enabled); + x.enablement = provenance; + } + ChildCustomization::Skill(x) => { + x.enabled = Some(enabled); + x.enablement = provenance; + } + ChildCustomization::Prompt(x) => { + x.enabled = Some(enabled); + x.enablement = provenance; + } + ChildCustomization::Rule(x) => { + x.enabled = Some(enabled); + x.enablement = provenance; + } + ChildCustomization::Hook(x) => { + x.enabled = Some(enabled); + x.enablement = provenance; + } + ChildCustomization::McpServer(x) => { + x.enabled = enabled; + x.enablement = provenance; + } ChildCustomization::Unknown(_) => {} } } -fn apply_toggle(list: &mut [Customization], id: &str, enabled: bool) -> bool { +fn apply_toggle(list: &mut [Customization], id: &str, enablement: &[CustomizationEnablement]) -> bool { if let Some(container) = list.iter_mut().find(|c| customization_id(c) == Some(id)) { - set_container_enabled(container, enabled); + apply_container_enablement(container, enablement); 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); + apply_child_enablement(child, enablement); return true; } } @@ -841,7 +882,7 @@ pub fn apply_action_to_session(state: &mut SessionState, action: &StateAction) - let Some(list) = state.customizations.as_mut() else { return ReduceOutcome::NoOp; }; - if apply_toggle(list, &a.id, a.enabled) { + if apply_toggle(list, &a.id, &a.enablement) { ReduceOutcome::Applied } else { ReduceOutcome::NoOp diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index da101cfd4..d7220981a 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -1354,19 +1354,19 @@ public struct SessionCustomizationsChangedAction: Codable, Sendable { public struct SessionCustomizationToggledAction: Codable, Sendable { public var type: ActionType - /// The id of the container or child to toggle. + /// The id of the container or child to update. public var id: String - /// Whether to enable or disable the targeted customization. - public var enabled: Bool + /// The complete set of explicit decisions, replacing any existing set. + public var enablement: [CustomizationEnablement] public init( type: ActionType, id: String, - enabled: Bool + enablement: [CustomizationEnablement] ) { self.type = type self.id = id - self.enabled = enabled + self.enablement = enablement } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 7ec7dd9b2..eca013fee 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -294,6 +294,13 @@ public enum CustomizationType: String, Codable, Sendable { case mcpServer = "mcpServer" } +/// Scope at which customization enablement is decided. +public enum CustomizationEnablementKind: String, Codable, Sendable { + case global = "global" + case workspace = "workspace" + case session = "session" +} + /// Discriminant values for {@link CustomizationLoadState}. public enum CustomizationLoadStatus: String, Codable, Sendable { case loading = "loading" @@ -3497,6 +3504,18 @@ public struct PluginCustomization: Codable, Sendable { public var uri: String /// Human-readable name. public var name: String + /// Explicit enablement decisions for this customization, one entry per scope + /// that has one. This is a wire contract: producers MUST publish entries + /// sorted by descending specificity (Session, Workspace, then Global). + /// The agent host emits at most one Workspace entry, for the session's primary + /// working directory. Consumers MAY treat + /// `enablement[0]` as the decisive decision and + /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + /// absent or empty array means no explicit decision exists, so the + /// customization is enabled by default. + /// + /// Only the agent host publishes this; clients treat it as read-only provenance. + public var enablement: [CustomizationEnablement]? /// Icons for UI display. public var icons: [Icon]? /// Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -3537,6 +3556,7 @@ public struct PluginCustomization: Codable, Sendable { case id case uri case name + case enablement case icons case range case meta = "_meta" @@ -3552,6 +3572,7 @@ public struct PluginCustomization: Codable, Sendable { id: String, uri: String, name: String, + enablement: [CustomizationEnablement]? = nil, icons: [Icon]? = nil, range: TextRange? = nil, meta: [String: AnyCodable]? = nil, @@ -3565,6 +3586,7 @@ public struct PluginCustomization: Codable, Sendable { self.id = id self.uri = uri self.name = name + self.enablement = enablement self.icons = icons self.range = range self.meta = meta @@ -3592,6 +3614,18 @@ public struct ClientPluginCustomization: Codable, Sendable { public var uri: String /// Human-readable name. public var name: String + /// Explicit enablement decisions for this customization, one entry per scope + /// that has one. This is a wire contract: producers MUST publish entries + /// sorted by descending specificity (Session, Workspace, then Global). + /// The agent host emits at most one Workspace entry, for the session's primary + /// working directory. Consumers MAY treat + /// `enablement[0]` as the decisive decision and + /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + /// absent or empty array means no explicit decision exists, so the + /// customization is enabled by default. + /// + /// Only the agent host publishes this; clients treat it as read-only provenance. + public var enablement: [CustomizationEnablement]? /// Icons for UI display. public var icons: [Icon]? /// Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -3634,6 +3668,7 @@ public struct ClientPluginCustomization: Codable, Sendable { case id case uri case name + case enablement case icons case range case meta = "_meta" @@ -3650,6 +3685,7 @@ public struct ClientPluginCustomization: Codable, Sendable { id: String, uri: String, name: String, + enablement: [CustomizationEnablement]? = nil, icons: [Icon]? = nil, range: TextRange? = nil, meta: [String: AnyCodable]? = nil, @@ -3664,6 +3700,7 @@ public struct ClientPluginCustomization: Codable, Sendable { self.id = id self.uri = uri self.name = name + self.enablement = enablement self.icons = icons self.range = range self.meta = meta @@ -3692,6 +3729,18 @@ public struct DirectoryCustomization: Codable, Sendable { public var uri: String /// Human-readable name. public var name: String + /// Explicit enablement decisions for this customization, one entry per scope + /// that has one. This is a wire contract: producers MUST publish entries + /// sorted by descending specificity (Session, Workspace, then Global). + /// The agent host emits at most one Workspace entry, for the session's primary + /// working directory. Consumers MAY treat + /// `enablement[0]` as the decisive decision and + /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + /// absent or empty array means no explicit decision exists, so the + /// customization is enabled by default. + /// + /// Only the agent host publishes this; clients treat it as read-only provenance. + public var enablement: [CustomizationEnablement]? /// Icons for UI display. public var icons: [Icon]? /// Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -3729,6 +3778,7 @@ public struct DirectoryCustomization: Codable, Sendable { case id case uri case name + case enablement case icons case range case meta = "_meta" @@ -3745,6 +3795,7 @@ public struct DirectoryCustomization: Codable, Sendable { id: String, uri: String, name: String, + enablement: [CustomizationEnablement]? = nil, icons: [Icon]? = nil, range: TextRange? = nil, meta: [String: AnyCodable]? = nil, @@ -3759,6 +3810,7 @@ public struct DirectoryCustomization: Codable, Sendable { self.id = id self.uri = uri self.name = name + self.enablement = enablement self.icons = icons self.range = range self.meta = meta @@ -3787,6 +3839,18 @@ public struct AgentCustomization: Codable, Sendable { public var uri: String /// Human-readable name. public var name: String + /// Explicit enablement decisions for this customization, one entry per scope + /// that has one. This is a wire contract: producers MUST publish entries + /// sorted by descending specificity (Session, Workspace, then Global). + /// The agent host emits at most one Workspace entry, for the session's primary + /// working directory. Consumers MAY treat + /// `enablement[0]` as the decisive decision and + /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + /// absent or empty array means no explicit decision exists, so the + /// customization is enabled by default. + /// + /// Only the agent host publishes this; clients treat it as read-only provenance. + public var enablement: [CustomizationEnablement]? /// Icons for UI display. public var icons: [Icon]? /// Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -3841,6 +3905,7 @@ public struct AgentCustomization: Codable, Sendable { case id case uri case name + case enablement case icons case range case meta = "_meta" @@ -3857,6 +3922,7 @@ public struct AgentCustomization: Codable, Sendable { id: String, uri: String, name: String, + enablement: [CustomizationEnablement]? = nil, icons: [Icon]? = nil, range: TextRange? = nil, meta: [String: AnyCodable]? = nil, @@ -3871,6 +3937,7 @@ public struct AgentCustomization: Codable, Sendable { self.id = id self.uri = uri self.name = name + self.enablement = enablement self.icons = icons self.range = range self.meta = meta @@ -3899,6 +3966,18 @@ public struct SkillCustomization: Codable, Sendable { public var uri: String /// Human-readable name. public var name: String + /// Explicit enablement decisions for this customization, one entry per scope + /// that has one. This is a wire contract: producers MUST publish entries + /// sorted by descending specificity (Session, Workspace, then Global). + /// The agent host emits at most one Workspace entry, for the session's primary + /// working directory. Consumers MAY treat + /// `enablement[0]` as the decisive decision and + /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + /// absent or empty array means no explicit decision exists, so the + /// customization is enabled by default. + /// + /// Only the agent host publishes this; clients treat it as read-only provenance. + public var enablement: [CustomizationEnablement]? /// Icons for UI display. public var icons: [Icon]? /// Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -3941,6 +4020,7 @@ public struct SkillCustomization: Codable, Sendable { case id case uri case name + case enablement case icons case range case meta = "_meta" @@ -3955,6 +4035,7 @@ public struct SkillCustomization: Codable, Sendable { id: String, uri: String, name: String, + enablement: [CustomizationEnablement]? = nil, icons: [Icon]? = nil, range: TextRange? = nil, meta: [String: AnyCodable]? = nil, @@ -3967,6 +4048,7 @@ public struct SkillCustomization: Codable, Sendable { self.id = id self.uri = uri self.name = name + self.enablement = enablement self.icons = icons self.range = range self.meta = meta @@ -3993,6 +4075,18 @@ public struct PromptCustomization: Codable, Sendable { public var uri: String /// Human-readable name. public var name: String + /// Explicit enablement decisions for this customization, one entry per scope + /// that has one. This is a wire contract: producers MUST publish entries + /// sorted by descending specificity (Session, Workspace, then Global). + /// The agent host emits at most one Workspace entry, for the session's primary + /// working directory. Consumers MAY treat + /// `enablement[0]` as the decisive decision and + /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + /// absent or empty array means no explicit decision exists, so the + /// customization is enabled by default. + /// + /// Only the agent host publishes this; clients treat it as read-only provenance. + public var enablement: [CustomizationEnablement]? /// Icons for UI display. public var icons: [Icon]? /// Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -4026,6 +4120,7 @@ public struct PromptCustomization: Codable, Sendable { case id case uri case name + case enablement case icons case range case meta = "_meta" @@ -4038,6 +4133,7 @@ public struct PromptCustomization: Codable, Sendable { id: String, uri: String, name: String, + enablement: [CustomizationEnablement]? = nil, icons: [Icon]? = nil, range: TextRange? = nil, meta: [String: AnyCodable]? = nil, @@ -4048,6 +4144,7 @@ public struct PromptCustomization: Codable, Sendable { self.id = id self.uri = uri self.name = name + self.enablement = enablement self.icons = icons self.range = range self.meta = meta @@ -4072,6 +4169,18 @@ public struct RuleCustomization: Codable, Sendable { public var uri: String /// Human-readable name. public var name: String + /// Explicit enablement decisions for this customization, one entry per scope + /// that has one. This is a wire contract: producers MUST publish entries + /// sorted by descending specificity (Session, Workspace, then Global). + /// The agent host emits at most one Workspace entry, for the session's primary + /// working directory. Consumers MAY treat + /// `enablement[0]` as the decisive decision and + /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + /// absent or empty array means no explicit decision exists, so the + /// customization is enabled by default. + /// + /// Only the agent host publishes this; clients treat it as read-only provenance. + public var enablement: [CustomizationEnablement]? /// Icons for UI display. public var icons: [Icon]? /// Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -4112,6 +4221,7 @@ public struct RuleCustomization: Codable, Sendable { case id case uri case name + case enablement case icons case range case meta = "_meta" @@ -4126,6 +4236,7 @@ public struct RuleCustomization: Codable, Sendable { id: String, uri: String, name: String, + enablement: [CustomizationEnablement]? = nil, icons: [Icon]? = nil, range: TextRange? = nil, meta: [String: AnyCodable]? = nil, @@ -4138,6 +4249,7 @@ public struct RuleCustomization: Codable, Sendable { self.id = id self.uri = uri self.name = name + self.enablement = enablement self.icons = icons self.range = range self.meta = meta @@ -4164,6 +4276,18 @@ public struct HookCustomization: Codable, Sendable { public var uri: String /// Human-readable name. public var name: String + /// Explicit enablement decisions for this customization, one entry per scope + /// that has one. This is a wire contract: producers MUST publish entries + /// sorted by descending specificity (Session, Workspace, then Global). + /// The agent host emits at most one Workspace entry, for the session's primary + /// working directory. Consumers MAY treat + /// `enablement[0]` as the decisive decision and + /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + /// absent or empty array means no explicit decision exists, so the + /// customization is enabled by default. + /// + /// Only the agent host publishes this; clients treat it as read-only provenance. + public var enablement: [CustomizationEnablement]? /// Icons for UI display. public var icons: [Icon]? /// Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -4195,6 +4319,7 @@ public struct HookCustomization: Codable, Sendable { case id case uri case name + case enablement case icons case range case meta = "_meta" @@ -4206,6 +4331,7 @@ public struct HookCustomization: Codable, Sendable { id: String, uri: String, name: String, + enablement: [CustomizationEnablement]? = nil, icons: [Icon]? = nil, range: TextRange? = nil, meta: [String: AnyCodable]? = nil, @@ -4215,6 +4341,7 @@ public struct HookCustomization: Codable, Sendable { self.id = id self.uri = uri self.name = name + self.enablement = enablement self.icons = icons self.range = range self.meta = meta @@ -4238,6 +4365,18 @@ public struct McpServerCustomization: Codable, Sendable { public var uri: String /// Human-readable name. public var name: String + /// Explicit enablement decisions for this customization, one entry per scope + /// that has one. This is a wire contract: producers MUST publish entries + /// sorted by descending specificity (Session, Workspace, then Global). + /// The agent host emits at most one Workspace entry, for the session's primary + /// working directory. Consumers MAY treat + /// `enablement[0]` as the decisive decision and + /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + /// absent or empty array means no explicit decision exists, so the + /// customization is enabled by default. + /// + /// Only the agent host publishes this; clients treat it as read-only provenance. + public var enablement: [CustomizationEnablement]? /// Icons for UI display. public var icons: [Icon]? /// Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -4252,7 +4391,8 @@ public struct McpServerCustomization: Codable, Sendable { /// out-of-band. public var meta: [String: AnyCodable]? public var type: CustomizationType - /// Whether this MCP server is currently enabled. + /// Whether this MCP server is effectively enabled after resolving all scopes. + /// {@link CustomizationBase.enablement | `enablement`} records its inputs. public var enabled: Bool /// Current lifecycle state of the MCP server. public var state: McpServerState @@ -4280,6 +4420,7 @@ public struct McpServerCustomization: Codable, Sendable { case id case uri case name + case enablement case icons case range case meta = "_meta" @@ -4294,6 +4435,7 @@ public struct McpServerCustomization: Codable, Sendable { id: String, uri: String, name: String, + enablement: [CustomizationEnablement]? = nil, icons: [Icon]? = nil, range: TextRange? = nil, meta: [String: AnyCodable]? = nil, @@ -4306,6 +4448,7 @@ public struct McpServerCustomization: Codable, Sendable { self.id = id self.uri = uri self.name = name + self.enablement = enablement self.icons = icons self.range = range self.meta = meta @@ -5227,6 +5370,70 @@ public struct ResourceChange: Codable, Sendable { } } +// MARK: - Customization Enablement Union + +/// A single explicit customization enablement decision. +public enum CustomizationEnablement: Codable, Sendable { + case global(CustomizationEnablementGlobal) + case workspace(CustomizationEnablementWorkspace) + case session(CustomizationEnablementSession) + + private enum DiscriminantKey: String, CodingKey { + case kind + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DiscriminantKey.self) + switch try container.decode(String.self, forKey: .kind) { + case "global": + self = .global(try CustomizationEnablementGlobal(from: decoder)) + case "workspace": + self = .workspace(try CustomizationEnablementWorkspace(from: decoder)) + case "session": + self = .session(try CustomizationEnablementSession(from: decoder)) + default: + throw DecodingError.dataCorruptedError(forKey: .kind, in: container, debugDescription: "Unknown CustomizationEnablement kind") + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .global(let value): try value.encode(to: encoder) + case .workspace(let value): try value.encode(to: encoder) + case .session(let value): try value.encode(to: encoder) + } + } +} + +public struct CustomizationEnablementGlobal: Codable, Sendable { + public var kind: String = "global" + public var enabled: Bool + + public init(enabled: Bool) { + self.enabled = enabled + } +} + +public struct CustomizationEnablementWorkspace: Codable, Sendable { + public var kind: String = "workspace" + public var uri: URI + public var enabled: Bool + + public init(uri: URI, enabled: Bool) { + self.uri = uri + self.enabled = enabled + } +} + +public struct CustomizationEnablementSession: Codable, Sendable { + public var kind: String = "session" + public var enabled: Bool + + public init(enabled: Bool) { + self.enabled = enabled + } +} + // MARK: - Tool Input /// Raw tool input represented inline or by content reference. diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift index e3e10ef65..cca688da2 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift @@ -216,16 +216,30 @@ func setCustomizationChildren(_ c: inout Customization, _ children: [ChildCustom } } -func setCustomizationEnabled(_ c: inout Customization, _ enabled: Bool) { +func effectiveEnablement(_ enablement: [CustomizationEnablement]) -> Bool { + guard let decision = enablement.first else { return true } + switch decision { + case .global(let value): return value.enabled + case .workspace(let value): return value.enabled + case .session(let value): return value.enabled + } +} + +func applyCustomizationEnablement(_ c: inout Customization, _ enablement: [CustomizationEnablement]) { + let enabled = effectiveEnablement(enablement) + let provenance = enablement.isEmpty ? nil : enablement switch c { case .plugin(var p): p.enabled = enabled + p.enablement = provenance c = .plugin(p) case .directory(var d): d.enabled = enabled + d.enablement = provenance c = .directory(d) case .mcpServer(var m): m.enabled = enabled + m.enablement = provenance c = .mcpServer(m) // Unknown/future customization: opaque payload, nothing to mutate. case .unknown: @@ -233,25 +247,33 @@ func setCustomizationEnabled(_ c: inout Customization, _ enabled: Bool) { } } -func setChildCustomizationEnabled(_ c: inout ChildCustomization, _ enabled: Bool) { +func applyChildCustomizationEnablement(_ c: inout ChildCustomization, _ enablement: [CustomizationEnablement]) { + let enabled = effectiveEnablement(enablement) + let provenance = enablement.isEmpty ? nil : enablement switch c { case .agent(var x): x.enabled = enabled + x.enablement = provenance c = .agent(x) case .skill(var x): x.enabled = enabled + x.enablement = provenance c = .skill(x) case .prompt(var x): x.enabled = enabled + x.enablement = provenance c = .prompt(x) case .rule(var x): x.enabled = enabled + x.enablement = provenance c = .rule(x) case .hook(var x): x.enabled = enabled + x.enablement = provenance c = .hook(x) case .mcpServer(var x): x.enabled = enabled + x.enablement = provenance c = .mcpServer(x) // Unknown/future child customization: opaque payload, nothing to mutate. case .unknown: @@ -259,11 +281,11 @@ func setChildCustomizationEnabled(_ c: inout ChildCustomization, _ enabled: Bool } } -func toggleCustomization(in list: inout [Customization], id: String, enabled: Bool) -> Bool { +func toggleCustomization(in list: inout [Customization], id: String, enablement: [CustomizationEnablement]) -> Bool { for i in list.indices { if customizationId(list[i]) == id { var entry = list[i] - setCustomizationEnabled(&entry, enabled) + applyCustomizationEnablement(&entry, enablement) list[i] = entry return true } @@ -273,7 +295,7 @@ func toggleCustomization(in list: inout [Customization], id: String, enabled: Bo 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) + applyChildCustomizationEnablement(&child, enablement) children[childIdx] = child setCustomizationChildren(&container, children) list[containerIdx] = container diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index 4dfa1c2c7..9e46edc9e 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -817,7 +817,7 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS case .sessionCustomizationToggled(let a): guard var list = state.customizations else { return state } - guard toggleCustomization(in: &list, id: a.id, enabled: a.enabled) else { return state } + guard toggleCustomization(in: &list, id: a.id, enablement: a.enablement) else { return state } var next = state next.customizations = list return next diff --git a/docs/.changes/20260804-scoped-customization-enablement.json b/docs/.changes/20260804-scoped-customization-enablement.json new file mode 100644 index 000000000..74cb1816a --- /dev/null +++ b/docs/.changes/20260804-scoped-customization-enablement.json @@ -0,0 +1,4 @@ +{ + "type": "changed", + "message": "`Customization` enablement now carries scoped host-published provenance, and `session/customizationToggled` replaces its complete decision set." +} diff --git a/docs/guide/actions.md b/docs/guide/actions.md index b863ff52e..db0ad6a88 100644 --- a/docs/guide/actions.md +++ b/docs/guide/actions.md @@ -125,7 +125,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 container or child customization on or off by id | +| `session/customizationToggled` | **Yes** | Client replaced a customization's explicit enablement decisions 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) | @@ -193,7 +193,7 @@ The client applies the action **optimistically** to its local state before sendi | `chat/pendingMessageSet` | Stores a steering or queued message (upsert); if queued and idle, auto-starts a turn | | `chat/pendingMessageRemoved` | Cancels a pending message before it is consumed | | `chat/queuedMessagesReordered` | Reorders queued messages; unknown IDs ignored, unmentioned messages kept at end | -| `session/customizationToggled` | Toggles a container or child customization on or off by id | +| `session/customizationToggled` | Replaces a customization's explicit enablement decisions by id | | `session/isReadChanged` | Marks the session as read or unread | | `session/isArchivedChanged` | Archives or unarchives the session | diff --git a/docs/guide/customizations.md b/docs/guide/customizations.md index 4b51c1a6b..c4412829d 100644 --- a/docs/guide/customizations.md +++ b/docs/guide/customizations.md @@ -5,7 +5,7 @@ Customizations extend agent sessions with additional capabilities — agents, sk - **Top-level entries are typically containers**: a `PluginCustomization` (an [Open Plugins](https://open-plugins.com/) package) or a `DirectoryCustomization` (a directory the host watches on disk). The host MAY also surface a bare `McpServerCustomization` at the top level (for example, a globally-configured MCP server that isn't bundled in a plugin). - **Other children live inside a container**: `AgentCustomization`, `SkillCustomization`, `PromptCustomization`, `RuleCustomization`, `HookCustomization`, `McpServerCustomization`. MCP servers can therefore appear in either position. -The agent host is authoritative on the effective tree. Clients publish plugins, the host expands them into children, and the host owns disk-backed directories and bare top-level MCP servers. +The agent host is authoritative on the effective tree and its enablement. Clients publish plugins, the host expands them into children, and the host owns disk-backed directories and bare top-level MCP servers. For MCP-specific behaviour (server lifecycle, authentication, App support), see [MCP Servers](/guide/mcp). @@ -58,6 +58,7 @@ PluginCustomization { name: string icons?: Icon[] enabled: boolean + enablement?: CustomizationEnablement[] // host-published explicit decisions clientId?: string // set when published by a client load?: CustomizationLoadState // host-reported parse/load state children?: ChildCustomization[] @@ -70,6 +71,7 @@ DirectoryCustomization { name: string icons?: Icon[] enabled: boolean + enablement?: CustomizationEnablement[] clientId?: string load?: CustomizationLoadState children?: ChildCustomization[] @@ -108,7 +110,7 @@ stateDiagram-v2 ## 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. +Every child carries the same base fields (`id`, `uri`, `name`, optional `icons`, optional `enablement`) 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): @@ -133,19 +135,51 @@ state.customizations .filter(c => c.type === CustomizationType.Agent) ``` +## Enablement + +Every customization may carry an `enablement` array of explicit decisions: + +```typescript +CustomizationEnablement = + | { kind: 'global'; enabled: boolean } + | { kind: 'workspace'; uri: URI; enabled: boolean } + | { kind: 'session'; enabled: boolean } +``` + +The agent host is the only publisher of this read-only provenance. Producers +MUST publish the entries in descending specificity: session, workspace, then +global. The host emits at most one workspace decision, for the session's +primary working directory. Consumers MAY use `enablement[0]` as the decisive +decision, with `enablement?.[0]?.enabled ?? true` as the effective value. An +absent or empty array has no explicit decision and means enabled by default. + +`enabled` remains the display-ready, effective value. Both containers and +children carry it; a child's final state is +`container.enabled && (child.enabled ?? true)`, so disabling a container still +disables all of its children. + ## Toggling -Any client can enable or disable any customization by dispatching `session/customizationToggled` with that entry's `id`: +Any client can request an enablement update with +`session/customizationToggled`. It carries the complete set of explicit +decisions for the entry: ```typescript { type: 'session/customizationToggled' id: string // any customization id - enabled: boolean + enablement: CustomizationEnablement[] } ``` -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. +The action replaces the entry's array wholesale rather than merging a single +scope. A caller changing one scope must include every decision it intends to +preserve. 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, replaces the matched entry's `enablement`, +and recomputes its `enabled` from the first decision. An empty array clears +the explicit provenance and restores the default enabled value. The action is +a no-op if no customization has that id. ```mermaid sequenceDiagram @@ -154,7 +188,7 @@ sequenceDiagram Note over Server: customizations: [Plugin A (enabled), Plugin B (enabled)] - Client->>Server: customizationToggled (id: plugin-a, enabled: false) + Client->>Server: customizationToggled (id: plugin-a, enablement: [session: false]) Server->>Client: action echoed Note over Server: customizations: [Plugin A (disabled), Plugin B (enabled)] ``` diff --git a/schema/actions.schema.json b/schema/actions.schema.json index fffbb4603..67725d087 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -495,24 +495,27 @@ }, "SessionCustomizationToggledAction": { "type": "object", - "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`.", + "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`.\n\nThe `enablement` array completely replaces all explicit decisions. A caller\nchanging one scope must include every decision it intends to preserve.", "properties": { "type": { "const": "session/customizationToggled" }, "id": { "type": "string", - "description": "The id of the container or child to toggle." + "description": "The id of the container or child to update." }, - "enabled": { - "type": "boolean", - "description": "Whether to enable or disable the targeted customization." + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "The complete set of explicit decisions, replacing any existing set." } }, "required": [ "type", "id", - "enabled" + "enablement" ] }, "SessionCustomizationUpdatedAction": { @@ -3535,6 +3538,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -3632,6 +3642,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -3691,6 +3708,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -3758,6 +3782,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -3829,6 +3860,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -3902,6 +3940,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -3945,6 +3990,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -4015,6 +4067,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -4074,6 +4133,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -4125,6 +4191,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -4187,6 +4260,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -4234,6 +4314,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -4255,7 +4342,7 @@ }, "enabled": { "type": "boolean", - "description": "Whether this MCP server is currently enabled." + "description": "Whether this MCP server is effectively enabled after resolving all scopes.\n{@link CustomizationBase.enablement | `enablement`} records its inputs." }, "state": { "$ref": "#/$defs/McpServerState", @@ -7127,6 +7214,60 @@ ], "description": "One outstanding piece of input a session is blocked on, aggregated across all\nchats in {@link SessionState.inputNeeded}.\n\nEach entry is self-sufficient: it carries the owning\n{@link SessionInputRequestBase.chat | `chat`} URI plus every identifier needed\nto construct the response, so a client can answer by dispatching the ordinary\n`chat/*` action (`chat/inputCompleted`, `chat/toolCallConfirmed`,\n`chat/toolCallComplete`, …) to that chat's channel **without having subscribed\nto the chat** — except {@link SessionToolAuthenticationRequest}, which is\nresolved via the `authenticate` command instead. The host removes the entry\nwith `session/inputNeededRemoved` once the underlying request resolves." }, + "CustomizationEnablement": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "const": "global" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "kind", + "enabled" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "const": "workspace" + }, + "uri": { + "$ref": "#/$defs/URI" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "kind", + "uri", + "enabled" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "const": "session" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "kind", + "enabled" + ] + } + ], + "description": "A single explicit enablement decision." + }, "ChildCustomizationType": { "oneOf": [ { diff --git a/schema/commands.schema.json b/schema/commands.schema.json index e65cb0f59..1ab5db2ae 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -2844,6 +2844,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -2941,6 +2948,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -3000,6 +3014,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -3067,6 +3088,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -3138,6 +3166,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -3211,6 +3246,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -3254,6 +3296,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -3324,6 +3373,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -3383,6 +3439,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -3434,6 +3497,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -3496,6 +3566,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -3543,6 +3620,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -3564,7 +3648,7 @@ }, "enabled": { "type": "boolean", - "description": "Whether this MCP server is currently enabled." + "description": "Whether this MCP server is effectively enabled after resolving all scopes.\n{@link CustomizationBase.enablement | `enablement`} records its inputs." }, "state": { "$ref": "#/$defs/McpServerState", @@ -6873,24 +6957,27 @@ }, "SessionCustomizationToggledAction": { "type": "object", - "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`.", + "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`.\n\nThe `enablement` array completely replaces all explicit decisions. A caller\nchanging one scope must include every decision it intends to preserve.", "properties": { "type": { "const": "session/customizationToggled" }, "id": { "type": "string", - "description": "The id of the container or child to toggle." + "description": "The id of the container or child to update." }, - "enabled": { - "type": "boolean", - "description": "Whether to enable or disable the targeted customization." + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "The complete set of explicit decisions, replacing any existing set." } }, "required": [ "type", "id", - "enabled" + "enablement" ] }, "SessionCustomizationUpdatedAction": { @@ -8991,6 +9078,60 @@ ], "description": "Discriminated union of all tool call lifecycle states.\n\nSee the [state model guide](/guide/state-model.html#tool-call-lifecycle)\nfor the full state machine diagram." }, + "CustomizationEnablement": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "const": "global" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "kind", + "enabled" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "const": "workspace" + }, + "uri": { + "$ref": "#/$defs/URI" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "kind", + "uri", + "enabled" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "const": "session" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "kind", + "enabled" + ] + } + ], + "description": "A single explicit enablement decision." + }, "CustomizationLoadState": { "oneOf": [ { diff --git a/schema/errors.schema.json b/schema/errors.schema.json index b1b9745fb..a4c92125f 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -1440,6 +1440,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -1537,6 +1544,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -1596,6 +1610,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -1663,6 +1684,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -1734,6 +1762,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -1807,6 +1842,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -1850,6 +1892,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -1920,6 +1969,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -1979,6 +2035,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -2030,6 +2093,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -2092,6 +2162,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -2139,6 +2216,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -2160,7 +2244,7 @@ }, "enabled": { "type": "boolean", - "description": "Whether this MCP server is currently enabled." + "description": "Whether this MCP server is effectively enabled after resolving all scopes.\n{@link CustomizationBase.enablement | `enablement`} records its inputs." }, "state": { "$ref": "#/$defs/McpServerState", @@ -6591,6 +6675,60 @@ ], "description": "Discriminated union of all tool call lifecycle states.\n\nSee the [state model guide](/guide/state-model.html#tool-call-lifecycle)\nfor the full state machine diagram." }, + "CustomizationEnablement": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "const": "global" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "kind", + "enabled" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "const": "workspace" + }, + "uri": { + "$ref": "#/$defs/URI" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "kind", + "uri", + "enabled" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "const": "session" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "kind", + "enabled" + ] + } + ], + "description": "A single explicit enablement decision." + }, "CustomizationLoadState": { "oneOf": [ { @@ -7831,24 +7969,27 @@ }, "SessionCustomizationToggledAction": { "type": "object", - "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`.", + "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`.\n\nThe `enablement` array completely replaces all explicit decisions. A caller\nchanging one scope must include every decision it intends to preserve.", "properties": { "type": { "const": "session/customizationToggled" }, "id": { "type": "string", - "description": "The id of the container or child to toggle." + "description": "The id of the container or child to update." }, - "enabled": { - "type": "boolean", - "description": "Whether to enable or disable the targeted customization." + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "The complete set of explicit decisions, replacing any existing set." } }, "required": [ "type", "id", - "enabled" + "enablement" ] }, "SessionCustomizationUpdatedAction": { diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 1c977584a..3570c4d83 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -1603,6 +1603,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -1700,6 +1707,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -1759,6 +1773,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -1826,6 +1847,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -1897,6 +1925,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -1970,6 +2005,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -2013,6 +2055,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -2083,6 +2132,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -2142,6 +2198,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -2193,6 +2256,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -2255,6 +2325,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -2302,6 +2379,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -2323,7 +2407,7 @@ }, "enabled": { "type": "boolean", - "description": "Whether this MCP server is currently enabled." + "description": "Whether this MCP server is effectively enabled after resolving all scopes.\n{@link CustomizationBase.enablement | `enablement`} records its inputs." }, "state": { "$ref": "#/$defs/McpServerState", @@ -5269,6 +5353,60 @@ ], "description": "Discriminated union of all tool call lifecycle states.\n\nSee the [state model guide](/guide/state-model.html#tool-call-lifecycle)\nfor the full state machine diagram." }, + "CustomizationEnablement": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "const": "global" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "kind", + "enabled" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "const": "workspace" + }, + "uri": { + "$ref": "#/$defs/URI" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "kind", + "uri", + "enabled" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "const": "session" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "kind", + "enabled" + ] + } + ], + "description": "A single explicit enablement decision." + }, "CustomizationLoadState": { "oneOf": [ { diff --git a/schema/state.schema.json b/schema/state.schema.json index d594f6b42..077f98701 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -1351,6 +1351,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -1448,6 +1455,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -1507,6 +1521,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -1574,6 +1595,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -1645,6 +1673,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -1718,6 +1753,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -1761,6 +1803,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -1831,6 +1880,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -1890,6 +1946,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -1941,6 +2004,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -2003,6 +2073,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -2050,6 +2127,13 @@ "type": "string", "description": "Human-readable name." }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." + }, "icons": { "type": "array", "items": { @@ -2071,7 +2155,7 @@ }, "enabled": { "type": "boolean", - "description": "Whether this MCP server is currently enabled." + "description": "Whether this MCP server is effectively enabled after resolving all scopes.\n{@link CustomizationBase.enablement | `enablement`} records its inputs." }, "state": { "$ref": "#/$defs/McpServerState", @@ -4943,6 +5027,60 @@ ], "description": "One outstanding piece of input a session is blocked on, aggregated across all\nchats in {@link SessionState.inputNeeded}.\n\nEach entry is self-sufficient: it carries the owning\n{@link SessionInputRequestBase.chat | `chat`} URI plus every identifier needed\nto construct the response, so a client can answer by dispatching the ordinary\n`chat/*` action (`chat/inputCompleted`, `chat/toolCallConfirmed`,\n`chat/toolCallComplete`, …) to that chat's channel **without having subscribed\nto the chat** — except {@link SessionToolAuthenticationRequest}, which is\nresolved via the `authenticate` command instead. The host removes the entry\nwith `session/inputNeededRemoved` once the underlying request resolves." }, + "CustomizationEnablement": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "const": "global" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "kind", + "enabled" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "const": "workspace" + }, + "uri": { + "$ref": "#/$defs/URI" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "kind", + "uri", + "enabled" + ] + }, + { + "type": "object", + "properties": { + "kind": { + "const": "session" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "kind", + "enabled" + ] + } + ], + "description": "A single explicit enablement decision." + }, "ChildCustomizationType": { "oneOf": [ { diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 34e8eabd1..63b508b23 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -695,7 +695,7 @@ const STATE_ENUMS = [ 'ToolCallRiskAssessmentStatus', 'ToolCallCancellationReason', 'ConfirmationOptionKind', 'ToolCallContributorKind', - 'ToolResultContentType', 'CustomizationType', 'CustomizationLoadStatus', 'TerminalClaimKind', + 'ToolResultContentType', 'CustomizationType', 'CustomizationEnablementKind', 'CustomizationLoadStatus', 'TerminalClaimKind', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', ]; @@ -1315,6 +1315,11 @@ function generateStateFile(project: Project): string { } } + lines.push('// ─── Customization Enablement Union ───────────────────────────────────────'); + lines.push(''); + lines.push(generateCustomizationEnablementGo()); + lines.push(''); + lines.push(generateToolInput()); lines.push(''); @@ -1610,6 +1615,74 @@ const CHAT_SOURCE_UNION: UnionConfig = { ], }; +function generateCustomizationEnablementGo(): string { + return `// CustomizationEnablement is a single explicit customization enablement decision. +type CustomizationEnablement struct { +\tValue isCustomizationEnablement +} + +type isCustomizationEnablement interface{ isCustomizationEnablement() } + +type CustomizationEnablementGlobal struct { +\tKind string \`json:"kind"\` +\tEnabled bool \`json:"enabled"\` +} + +func (*CustomizationEnablementGlobal) isCustomizationEnablement() {} + +type CustomizationEnablementWorkspace struct { +\tKind string \`json:"kind"\` +\tURI URI \`json:"uri"\` +\tEnabled bool \`json:"enabled"\` +} + +func (*CustomizationEnablementWorkspace) isCustomizationEnablement() {} + +type CustomizationEnablementSession struct { +\tKind string \`json:"kind"\` +\tEnabled bool \`json:"enabled"\` +} + +func (*CustomizationEnablementSession) isCustomizationEnablement() {} + +func (e *CustomizationEnablement) UnmarshalJSON(data []byte) error { +\tdisc, _, err := readDiscriminator(data, "kind") +\tif err != nil { +\t\treturn err +\t} +\tswitch disc { +\tcase "global": +\t\tvar value CustomizationEnablementGlobal +\t\tif err := json.Unmarshal(data, &value); err != nil { +\t\t\treturn err +\t\t} +\t\te.Value = &value +\tcase "workspace": +\t\tvar value CustomizationEnablementWorkspace +\t\tif err := json.Unmarshal(data, &value); err != nil { +\t\t\treturn err +\t\t} +\t\te.Value = &value +\tcase "session": +\t\tvar value CustomizationEnablementSession +\t\tif err := json.Unmarshal(data, &value); err != nil { +\t\t\treturn err +\t\t} +\t\te.Value = &value +\tdefault: +\t\treturn &json.UnmarshalTypeError{Value: "CustomizationEnablement"} +\t} +\treturn nil +} + +func (e CustomizationEnablement) MarshalJSON() ([]byte, error) { +\tif e.Value == nil { +\t\treturn []byte("null"), nil +\t} +\treturn json.Marshal(e.Value) +}`; +} + function generateChangesetOperationTargetGo(): string { return `// ChangesetOperationTarget identifies the file or range a // ChangesetOperation should act on. @@ -2095,6 +2168,7 @@ function checkExhaustiveness(project: Project): void { 'AhpErrorCodeWithData', 'JsonRpcErrorCode', 'ChangesetOperationTarget', + 'CustomizationEnablement', ]); const missing = [...imported].filter((n) => !coveredByLists.has(n) && !knownSpecial.has(n)); diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 81a192deb..2c2f17cb0 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -900,7 +900,7 @@ const STATE_ENUMS = [ 'ToolCallRiskAssessmentStatus', 'ToolCallCancellationReason', 'ConfirmationOptionKind', 'ToolCallContributorKind', - 'ToolResultContentType', 'CustomizationType', 'CustomizationLoadStatus', 'TerminalClaimKind', + 'ToolResultContentType', 'CustomizationType', 'CustomizationEnablementKind', 'CustomizationLoadStatus', 'TerminalClaimKind', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', ]; @@ -1256,6 +1256,11 @@ function generateStateFile(project: Project): string { } } + lines.push('// ─── Customization Enablement Union ─────────────────────────────────────'); + lines.push(''); + lines.push(generateCustomizationEnablementKotlin()); + lines.push(''); + lines.push('// ─── Tool Input ──────────────────────────────────────────────────────────────'); lines.push(''); lines.push(generateToolInput()); @@ -1600,6 +1605,76 @@ const CHAT_SOURCE_UNION: UnionConfig = { ], }; +function generateCustomizationEnablementKotlin(): string { + return `/** + * A single explicit customization enablement decision. + */ +@Serializable(with = CustomizationEnablementSerializer::class) +sealed interface CustomizationEnablement { + @JvmInline value class Global(val value: CustomizationEnablementGlobal) : CustomizationEnablement + @JvmInline value class Workspace(val value: CustomizationEnablementWorkspace) : CustomizationEnablement + @JvmInline value class Session(val value: CustomizationEnablementSession) : CustomizationEnablement +} + +@Serializable +data class CustomizationEnablementGlobal( + val enabled: Boolean, + val kind: String = "global", +) + +@Serializable +data class CustomizationEnablementWorkspace( + val uri: URI, + val enabled: Boolean, + val kind: String = "workspace", +) + +@Serializable +data class CustomizationEnablementSession( + val enabled: Boolean, + val kind: String = "session", +) + +internal object CustomizationEnablementSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("CustomizationEnablement") + + override fun deserialize(decoder: Decoder): CustomizationEnablement { + val input = decoder as? JsonDecoder + ?: error("CustomizationEnablement can only be deserialized from JSON") + val element = input.decodeJsonElement() + val obj = element as? JsonObject + ?: error("Expected JsonObject for CustomizationEnablement") + return when ((obj["kind"] as? JsonPrimitive)?.contentOrNull) { + "global" -> CustomizationEnablement.Global( + input.json.decodeFromJsonElement(CustomizationEnablementGlobal.serializer(), element), + ) + "workspace" -> CustomizationEnablement.Workspace( + input.json.decodeFromJsonElement(CustomizationEnablementWorkspace.serializer(), element), + ) + "session" -> CustomizationEnablement.Session( + input.json.decodeFromJsonElement(CustomizationEnablementSession.serializer(), element), + ) + else -> error("Unknown CustomizationEnablement kind") + } + } + + override fun serialize(encoder: Encoder, value: CustomizationEnablement) { + val output = encoder as? JsonEncoder + ?: error("CustomizationEnablement can only be serialized to JSON") + val element: JsonElement = when (value) { + is CustomizationEnablement.Global -> + output.json.encodeToJsonElement(CustomizationEnablementGlobal.serializer(), value.value) + is CustomizationEnablement.Workspace -> + output.json.encodeToJsonElement(CustomizationEnablementWorkspace.serializer(), value.value) + is CustomizationEnablement.Session -> + output.json.encodeToJsonElement(CustomizationEnablementSession.serializer(), value.value) + } + output.encodeJsonElement(element) + } +}`; +} + /** * ChangesetOperationTarget — TS discriminated union over `{ kind: "resource" }` * and `{ kind: "range" }`. The variant structs are inline-only in TS (not @@ -2120,6 +2195,7 @@ function checkExhaustiveness(project: Project): void { 'ForkChatSource', // generateFixedChatSourceBranchKotlin() 'SideChatSource', // generateFixedChatSourceBranchKotlin() 'ChangesetOperationTarget', // generateChangesetOperationTargetKotlin() + 'CustomizationEnablement', // generateCustomizationEnablementKotlin() ]); const missing = [...imported].filter(n => !coveredByLists.has(n) && !knownSpecial.has(n)); diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 11d46c18e..9efbe80d3 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -657,7 +657,7 @@ const STATE_ENUMS = [ 'ToolCallRiskAssessmentStatus', 'ToolCallCancellationReason', 'ConfirmationOptionKind', 'ToolCallContributorKind', - 'ToolResultContentType', 'CustomizationType', 'CustomizationLoadStatus', 'TerminalClaimKind', + 'ToolResultContentType', 'CustomizationType', 'CustomizationEnablementKind', 'CustomizationLoadStatus', 'TerminalClaimKind', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', ]; @@ -1151,6 +1151,10 @@ function generateStateFile(project: Project): string { } } + lines.push('// ─── Customization Enablement Union ───────────────────────────────────────\n'); + lines.push(generateCustomizationEnablementRust()); + lines.push(''); + lines.push(generateToolInput()); lines.push(''); @@ -1330,7 +1334,7 @@ pub struct ${scope}ToolCallConfirmedAction { function generateActionsFile(project: Project): string { const lines: string[] = [GENERATED_HEADER]; lines.push('#[allow(unused_imports)]'); - lines.push('use crate::state::{AgentInfo, AgentSelection, Annotation, AnnotationEntry, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ConfirmationOption, ContentRef, Customization, ErrorInfo, McpAuthRequirement, McpServerState, ModelSelection, ResponsePart, SessionActiveClient, SessionInputRequest, SideChatSelection, TerminalClaim, TerminalInfo, TextRange, ToolCallContributor, ToolCallResult, ToolCallRiskAssessment, ToolCallConfirmationReason, ToolCallCancellationReason, ToolDefinition, ToolInput, ToolResultContent, UsageInfo, Message, PendingMessageKind, Turn, ChangesetStatus, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, Changeset, ChatSummary};'); + lines.push('use crate::state::{AgentInfo, AgentSelection, Annotation, AnnotationEntry, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ConfirmationOption, ContentRef, Customization, CustomizationEnablement, ErrorInfo, McpAuthRequirement, McpServerState, ModelSelection, ResponsePart, SessionActiveClient, SessionInputRequest, SideChatSelection, TerminalClaim, TerminalInfo, TextRange, ToolCallContributor, ToolCallResult, ToolCallRiskAssessment, ToolCallConfirmationReason, ToolCallCancellationReason, ToolDefinition, ToolInput, ToolResultContent, UsageInfo, Message, PendingMessageKind, Turn, ChangesetStatus, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, Changeset, ChatSummary};'); lines.push(''); // ActionType enum @@ -1538,6 +1542,27 @@ function generateCommandsFile(project: Project): string { return lines.join('\n'); } +function generateCustomizationEnablementRust(): string { + return `/// A single explicit customization enablement decision. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "kind")] +pub enum CustomizationEnablement { + #[serde(rename = "global")] + Global { + enabled: bool, + }, + #[serde(rename = "workspace")] + Workspace { + uri: Uri, + enabled: bool, + }, + #[serde(rename = "session")] + Session { + enabled: bool, + }, +}`; +} + function generateSubscribeParamsImplRust(): string { return `impl SubscribeParams { /// Create subscribe params with default delivery behavior. @@ -1927,6 +1952,7 @@ function checkExhaustiveness(project: Project): void { 'AhpErrorCodeWithData', 'JsonRpcErrorCode', 'ChangesetOperationTarget', + 'CustomizationEnablement', ]); const missing = [...imported].filter(n => !coveredByLists.has(n) && !knownSpecial.has(n)); diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 6b486893e..1893f4e5d 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -609,7 +609,7 @@ const STATE_ENUMS = [ 'ToolCallRiskAssessmentStatus', 'ToolCallCancellationReason', 'ConfirmationOptionKind', 'ToolCallContributorKind', - 'ToolResultContentType', 'CustomizationType', 'CustomizationLoadStatus', 'TerminalClaimKind', + 'ToolResultContentType', 'CustomizationType', 'CustomizationEnablementKind', 'CustomizationLoadStatus', 'TerminalClaimKind', 'McpServerStatus', 'McpAuthRequiredReason', 'ChangesetStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType', ]; @@ -1151,6 +1151,10 @@ function generateStateFile(project: Project): string { } } + lines.push('// MARK: - Customization Enablement Union\n'); + lines.push(generateCustomizationEnablementSwift()); + lines.push(''); + lines.push('// MARK: - Tool Input\n'); lines.push(generateToolInput()); lines.push(''); @@ -1559,6 +1563,70 @@ function generateCommandsFile(project: Project): string { return lines.join('\n'); } +function generateCustomizationEnablementSwift(): string { + return `/// A single explicit customization enablement decision. +public enum CustomizationEnablement: Codable, Sendable { + case global(CustomizationEnablementGlobal) + case workspace(CustomizationEnablementWorkspace) + case session(CustomizationEnablementSession) + + private enum DiscriminantKey: String, CodingKey { + case kind + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DiscriminantKey.self) + switch try container.decode(String.self, forKey: .kind) { + case "global": + self = .global(try CustomizationEnablementGlobal(from: decoder)) + case "workspace": + self = .workspace(try CustomizationEnablementWorkspace(from: decoder)) + case "session": + self = .session(try CustomizationEnablementSession(from: decoder)) + default: + throw DecodingError.dataCorruptedError(forKey: .kind, in: container, debugDescription: "Unknown CustomizationEnablement kind") + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .global(let value): try value.encode(to: encoder) + case .workspace(let value): try value.encode(to: encoder) + case .session(let value): try value.encode(to: encoder) + } + } +} + +public struct CustomizationEnablementGlobal: Codable, Sendable { + public var kind: String = "global" + public var enabled: Bool + + public init(enabled: Bool) { + self.enabled = enabled + } +} + +public struct CustomizationEnablementWorkspace: Codable, Sendable { + public var kind: String = "workspace" + public var uri: URI + public var enabled: Bool + + public init(uri: URI, enabled: Bool) { + self.uri = uri + self.enabled = enabled + } +} + +public struct CustomizationEnablementSession: Codable, Sendable { + public var kind: String = "session" + public var enabled: Bool + + public init(enabled: Bool) { + self.enabled = enabled + } +}`; +} + function generateChangesetOperationTargetSwift(): string { return `/// Identifies the file or range a \`ChangesetOperation\` should act on. public enum ChangesetOperationTarget: Codable, Sendable { @@ -2139,6 +2207,7 @@ function checkExhaustiveness(project: Project): void { 'ForkChatSource', // generateFixedChatSourceBranchSwift() 'SideChatSource', // generateFixedChatSourceBranchSwift() 'ChangesetOperationTarget', // TS discriminated union; consumers should add a Swift case-iterable enum + 'CustomizationEnablement', // generateCustomizationEnablementSwift() ]); const missing = [...imported].filter(n => !coveredByLists.has(n) && !knownSpecial.has(n)); diff --git a/types/channels-session/actions.ts b/types/channels-session/actions.ts index 5ba4ef058..714070c19 100644 --- a/types/channels-session/actions.ts +++ b/types/channels-session/actions.ts @@ -11,6 +11,7 @@ import type { SessionActiveClient, SessionInputRequest, Customization, + CustomizationEnablement, McpServerState, } from './state.js'; import type { URI } from '../common/state.js'; @@ -370,16 +371,19 @@ export interface SessionCustomizationsChangedAction { * only matters while its container is enabled. Is a no-op when no * customization has the given `id`. * + * The `enablement` array completely replaces all explicit decisions. A caller + * changing one scope must include every decision it intends to preserve. + * * @category Session Actions * @version 1 * @clientDispatchable */ export interface SessionCustomizationToggledAction { type: ActionType.SessionCustomizationToggled; - /** The id of the container or child to toggle. */ + /** The id of the container or child to update. */ id: string; - /** Whether to enable or disable the targeted customization. */ - enabled: boolean; + /** The complete set of explicit decisions, replacing any existing set. */ + enablement: CustomizationEnablement[]; } /** diff --git a/types/channels-session/reducer.ts b/types/channels-session/reducer.ts index c62517b28..ebedd0d01 100644 --- a/types/channels-session/reducer.ts +++ b/types/channels-session/reducer.ts @@ -8,6 +8,9 @@ import { ActionType } from '../common/actions.js'; import type { SessionState, SessionInputRequest, + ChildCustomization, + Customization, + CustomizationEnablement, McpServerCustomization, } from './state.js'; import { @@ -105,6 +108,23 @@ function updateMcpServerCustomization( return { ...state, customizations: updated }; } +/** + * Replaces a customization's explicit enablement decisions and recomputes its + * effective {@link CustomizationBase.enabled | `enabled`} value. An empty set + * drops the field entirely, so a customization at its default carries no + * provenance. + */ +function applyCustomizationEnablement(customization: T, enablement: readonly CustomizationEnablement[]): T { + const next = { ...customization }; + next.enabled = enablement[0]?.enabled ?? true; + if (enablement.length > 0) { + next.enablement = [...enablement]; + } else { + delete next.enablement; + } + return next; +} + // ─── Session Reducer ───────────────────────────────────────────────────────── /** @@ -308,7 +328,7 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: const topIdx = list.findIndex(c => c.id === action.id); if (topIdx >= 0) { const updated = list.slice(); - updated[topIdx] = { ...list[topIdx], enabled: action.enabled }; + updated[topIdx] = applyCustomizationEnablement(list[topIdx], action.enablement); return { ...state, customizations: updated }; } for (let i = 0; i < list.length; i++) { @@ -325,7 +345,7 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: continue; } const newChildren = children.slice(); - newChildren[childIdx] = { ...children[childIdx], enabled: action.enabled }; + newChildren[childIdx] = applyCustomizationEnablement(children[childIdx], action.enablement); const updated = list.slice(); updated[i] = { ...container, children: newChildren }; return { ...state, customizations: updated }; diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index e26fecfc7..8b8b21033 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -645,6 +645,23 @@ export const enum CustomizationType { McpServer = 'mcpServer', } +/** + * Scope at which customization enablement is decided. + * + * @category Customization Types + */ +export const enum CustomizationEnablementKind { + Global = 'global', + Workspace = 'workspace', + Session = 'session', +} + +/** A single explicit enablement decision. */ +export type CustomizationEnablement = + | { kind: CustomizationEnablementKind.Global; enabled: boolean } + | { kind: CustomizationEnablementKind.Workspace; uri: URI; enabled: boolean } + | { kind: CustomizationEnablementKind.Session; enabled: boolean }; + /** * Customization types that appear as children of a * {@link PluginCustomization} or {@link DirectoryCustomization}. @@ -683,6 +700,20 @@ interface CustomizationBase { uri: URI; /** Human-readable name. */ name: string; + /** + * Explicit enablement decisions for this customization, one entry per scope + * that has one. This is a wire contract: producers MUST publish entries + * sorted by descending specificity (Session, Workspace, then Global). + * The agent host emits at most one Workspace entry, for the session's primary + * working directory. Consumers MAY treat + * `enablement[0]` as the decisive decision and + * `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + * absent or empty array means no explicit decision exists, so the + * customization is enabled by default. + * + * Only the agent host publishes this; clients treat it as read-only provenance. + */ + enablement?: CustomizationEnablement[]; /** Icons for UI display. */ icons?: Icon[]; /** @@ -1024,7 +1055,8 @@ export interface HookCustomization extends ChildCustomizationBase { export interface McpServerCustomization extends CustomizationBase { type: CustomizationType.McpServer; /** - * Whether this MCP server is currently enabled. + * Whether this MCP server is effectively enabled after resolving all scopes. + * {@link CustomizationBase.enablement | `enablement`} records its inputs. */ enabled: boolean; /** diff --git a/types/test-cases/reducers/060-session-customizationtoggled-toggles-by-id.json b/types/test-cases/reducers/060-session-customizationtoggled-toggles-by-id.json index dfdcb10fc..3a9bfe202 100644 --- a/types/test-cases/reducers/060-session-customizationtoggled-toggles-by-id.json +++ b/types/test-cases/reducers/060-session-customizationtoggled-toggles-by-id.json @@ -29,7 +29,21 @@ { "type": "session/customizationToggled", "id": "plugin-a", - "enabled": false + "enablement": [ + { + "kind": "session", + "enabled": false + }, + { + "kind": "workspace", + "uri": "file:///workspace", + "enabled": true + }, + { + "kind": "global", + "enabled": true + } + ] } ], "expected": { @@ -43,7 +57,22 @@ "id": "plugin-a", "uri": "https://plugins.example/a", "name": "Plugin A", - "enabled": false + "enabled": false, + "enablement": [ + { + "kind": "session", + "enabled": false + }, + { + "kind": "workspace", + "uri": "file:///workspace", + "enabled": true + }, + { + "kind": "global", + "enabled": true + } + ] }, { "type": "plugin", diff --git a/types/test-cases/reducers/061-session-customizationtoggled-is-no-op-for-unknown-id.json b/types/test-cases/reducers/061-session-customizationtoggled-is-no-op-for-unknown-id.json index e46cb7d13..23eb0bf26 100644 --- a/types/test-cases/reducers/061-session-customizationtoggled-is-no-op-for-unknown-id.json +++ b/types/test-cases/reducers/061-session-customizationtoggled-is-no-op-for-unknown-id.json @@ -22,7 +22,12 @@ { "type": "session/customizationToggled", "id": "plugin-unknown", - "enabled": false + "enablement": [ + { + "kind": "session", + "enabled": false + } + ] } ], "expected": { diff --git a/types/test-cases/reducers/062-session-customizationtoggled-is-no-op-when-customizations-undefined.json b/types/test-cases/reducers/062-session-customizationtoggled-is-no-op-when-customizations-undefined.json index fa303b4f0..98a48cf1b 100644 --- a/types/test-cases/reducers/062-session-customizationtoggled-is-no-op-when-customizations-undefined.json +++ b/types/test-cases/reducers/062-session-customizationtoggled-is-no-op-when-customizations-undefined.json @@ -13,7 +13,12 @@ { "type": "session/customizationToggled", "id": "plugin-a", - "enabled": false + "enablement": [ + { + "kind": "session", + "enabled": false + } + ] } ], "expected": { 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 index 839c8155d..c095907e3 100644 --- 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 @@ -47,7 +47,12 @@ { "type": "session/customizationToggled", "id": "skill-1", - "enabled": false + "enablement": [ + { + "kind": "session", + "enabled": false + } + ] } ], "expected": { @@ -79,6 +84,12 @@ "uri": "https://plugins.example/a#skills/lint", "name": "lint", "enabled": false, + "enablement": [ + { + "kind": "session", + "enabled": false + } + ], "disableUserInvocation": true }, { 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 index 549241466..5dd151d49 100644 --- 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 @@ -30,7 +30,12 @@ { "type": "session/customizationToggled", "id": "does-not-exist", - "enabled": false + "enablement": [ + { + "kind": "session", + "enabled": false + } + ] } ], "expected": { diff --git a/types/test-cases/reducers/263-session-customizationtoggled-clears-enablement.json b/types/test-cases/reducers/263-session-customizationtoggled-clears-enablement.json new file mode 100644 index 000000000..6e63c9fdd --- /dev/null +++ b/types/test-cases/reducers/263-session-customizationtoggled-clears-enablement.json @@ -0,0 +1,62 @@ +{ + "description": "session/customizationToggled clears enablement and restores the default", + "reducer": "session", + "initial": { + "provider": "copilot", + "title": "Test Session", + "status": 1, + "lifecycle": "creating", + "customizations": [ + { + "type": "mcpServer", + "id": "server-a", + "uri": "file:///workspace/.vscode/mcp.json", + "name": "Server A", + "enabled": false, + "enablement": [ + { + "kind": "workspace", + "uri": "file:///workspace", + "enabled": false + }, + { + "kind": "global", + "enabled": true + } + ], + "state": { + "kind": "stopped" + } + } + ], + "activeClients": [], + "chats": [] + }, + "actions": [ + { + "type": "session/customizationToggled", + "id": "server-a", + "enablement": [] + } + ], + "expected": { + "provider": "copilot", + "title": "Test Session", + "status": 1, + "lifecycle": "creating", + "customizations": [ + { + "type": "mcpServer", + "id": "server-a", + "uri": "file:///workspace/.vscode/mcp.json", + "name": "Server A", + "enabled": true, + "state": { + "kind": "stopped" + } + } + ], + "activeClients": [], + "chats": [] + } +} From e639de7d815a77fb21abef8d3fb6ac5b7a5cb2b8 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 12 Aug 2026 23:11:57 -0700 Subject: [PATCH 2/4] session: scoped customization enablement Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- clients/go/ahp/reducers.go | 9 - clients/go/ahptypes/actions.generated.go | 15 +- clients/go/ahptypes/state.generated.go | 193 +++++-------- .../microsoft/agenthostprotocol/Reducers.kt | 18 +- .../generated/Actions.generated.kt | 2 +- .../generated/State.generated.kt | 231 +++++----------- clients/rust/crates/ahp-types/src/actions.rs | 15 +- clients/rust/crates/ahp-types/src/state.rs | 208 +++++--------- clients/rust/crates/ahp/src/reducers.rs | 9 - .../Generated/Actions.generated.swift | 2 +- .../Generated/State.generated.swift | 255 ++++++------------ .../AgentHostProtocol/NativeReducer.swift | 9 - ...ient-bundled-customization-enablement.json | 4 + docs/guide/customizations.md | 56 ++-- docs/guide/mcp.md | 13 +- schema/actions.schema.json | 169 ++++-------- schema/commands.schema.json | 249 +++++++---------- schema/errors.schema.json | 249 +++++++---------- schema/notifications.schema.json | 245 +++++++---------- schema/state.schema.json | 165 ++++-------- types/channels-session/actions.ts | 15 +- types/channels-session/reducer.ts | 28 +- types/channels-session/state.ts | 74 +++-- ...mizationschanged-replaces-entire-list.json | 8 +- ...nged-replaces-existing-customizations.json | 9 +- ...on-customizationtoggled-toggles-by-id.json | 10 +- ...zationtoggled-is-no-op-for-unknown-id.json | 6 +- ...onupdated-replaces-existing-container.json | 5 - ...stomizationupdated-appends-unknown-id.json | 8 +- ...emoved-removes-container-and-children.json | 7 +- ...on-customizationremoved-removes-child.json | 2 - ...-customizationremoved-noop-unknown-id.json | 6 +- ...statechanged-upserts-top-level-server.json | 2 - ...rstatechanged-upserts-container-child.json | 4 - ...mcpserverstatechanged-noop-unknown-id.json | 2 - ...mcpserverstatechanged-noop-non-mcp-id.json | 2 - ...tomizationtoggled-toggles-child-by-id.json | 10 - ...toggled-is-no-op-for-unknown-child-id.json | 2 - ...tartrequested-starts-top-level-server.json | 2 - ...prequested-stops-auth-required-server.json | 2 - ...erstoprequested-stops-container-child.json | 4 - ...serverstartrequested-no-op-unknown-id.json | 10 +- ...ustomizationtoggled-clears-enablement.json | 2 - ...7-agent-customization-model-and-tools.json | 2 - 44 files changed, 826 insertions(+), 1512 deletions(-) create mode 100644 docs/.changes/20260812-client-bundled-customization-enablement.json diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index 7b093c9c0..d8bff27a0 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -388,13 +388,10 @@ func applyContainerEnablement(c *ahptypes.Customization, enablement []ahptypes.C provenance := append([]ahptypes.CustomizationEnablement(nil), enablement...) switch v := c.Value.(type) { case *ahptypes.PluginCustomization: - v.Enabled = enabled v.Enablement = provenance case *ahptypes.DirectoryCustomization: v.Enabled = enabled - v.Enablement = provenance case *ahptypes.McpServerCustomization: - v.Enabled = enabled v.Enablement = provenance } } @@ -405,21 +402,15 @@ func applyChildEnablement(c *ahptypes.ChildCustomization, enablement []ahptypes. switch v := c.Value.(type) { case *ahptypes.AgentCustomization: v.Enabled = &enabled - v.Enablement = provenance case *ahptypes.SkillCustomization: v.Enabled = &enabled - v.Enablement = provenance case *ahptypes.PromptCustomization: v.Enabled = &enabled - v.Enablement = provenance case *ahptypes.RuleCustomization: v.Enabled = &enabled - v.Enablement = provenance case *ahptypes.HookCustomization: v.Enabled = &enabled - v.Enablement = provenance case *ahptypes.McpServerCustomization: - v.Enabled = enabled v.Enablement = provenance } } diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index d9a98a24d..8e8d296e7 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -982,15 +982,16 @@ type SessionCustomizationsChangedAction struct { Customizations []Customization `json:"customizations"` } -// A client toggled a customization on or off. +// A client updated a customization's enablement decisions. // // 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 +// children inside each container (a skill, agent, or other entry). Plugins +// and MCP servers retain the matched entry's explicit decisions; other +// entries update their `enabled` flag. Disabling a plugin still disables all +// of its children — the effective state of a plugin child is the plugin's +// derived enabled value and `(child.enabled ?? true)` — so toggling a child +// only matters while its plugin is enabled. Is a no-op when no // customization has the given `id`. // // The `enablement` array completely replaces all explicit decisions. A caller @@ -999,7 +1000,7 @@ type SessionCustomizationToggledAction struct { Type ActionType `json:"type"` // The id of the container or child to update. Id string `json:"id"` - // The complete set of explicit decisions, replacing any existing set. + // Explicit enablement decisions, replacing the previous list entirely. Enablement []CustomizationEnablement `json:"enablement"` } diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 182d11b37..ce14d63c6 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -2393,18 +2393,6 @@ type PluginCustomization struct { Uri URI `json:"uri"` // Human-readable name. Name string `json:"name"` - // Explicit enablement decisions for this customization, one entry per scope - // that has one. This is a wire contract: producers MUST publish entries - // sorted by descending specificity (Session, Workspace, then Global). - // The agent host emits at most one Workspace entry, for the session's primary - // working directory. Consumers MAY treat - // `enablement[0]` as the decisive decision and - // `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - // absent or empty array means no explicit decision exists, so the - // customization is enabled by default. - // - // Only the agent host publishes this; clients treat it as read-only provenance. - Enablement []CustomizationEnablement `json:"enablement,omitempty"` // Icons for UI display. Icons []Icon `json:"icons,omitempty"` // Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -2418,8 +2406,6 @@ type PluginCustomization struct { // protocol; producers and consumers agree on its contents // out-of-band. Meta map[string]json.RawMessage `json:"_meta,omitempty"` - // Whether this container is currently enabled. - Enabled bool `json:"enabled"` // `clientId` of the client that contributed this container. Absent for // server-originated entries. ClientId *string `json:"clientId,omitempty"` @@ -2433,6 +2419,8 @@ type PluginCustomization struct { // nothing. Children []ChildCustomization `json:"children,omitempty"` Type CustomizationType `json:"type"` + // Explicit enablement decisions. See {@link McpServerCustomization.enablement}. + Enablement []CustomizationEnablement `json:"enablement,omitempty"` // Version of the plugin, sourced from the // [Open Plugins](https://open-plugins.com/) manifest's optional // `version` field (semver, e.g. `"1.2.0"`). Absent when the manifest @@ -2466,18 +2454,6 @@ type ClientPluginCustomization struct { Uri URI `json:"uri"` // Human-readable name. Name string `json:"name"` - // Explicit enablement decisions for this customization, one entry per scope - // that has one. This is a wire contract: producers MUST publish entries - // sorted by descending specificity (Session, Workspace, then Global). - // The agent host emits at most one Workspace entry, for the session's primary - // working directory. Consumers MAY treat - // `enablement[0]` as the decisive decision and - // `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - // absent or empty array means no explicit decision exists, so the - // customization is enabled by default. - // - // Only the agent host publishes this; clients treat it as read-only provenance. - Enablement []CustomizationEnablement `json:"enablement,omitempty"` // Icons for UI display. Icons []Icon `json:"icons,omitempty"` // Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -2491,8 +2467,6 @@ type ClientPluginCustomization struct { // protocol; producers and consumers agree on its contents // out-of-band. Meta map[string]json.RawMessage `json:"_meta,omitempty"` - // Whether this container is currently enabled. - Enabled bool `json:"enabled"` // `clientId` of the client that contributed this container. Absent for // server-originated entries. ClientId *string `json:"clientId,omitempty"` @@ -2506,6 +2480,8 @@ type ClientPluginCustomization struct { // nothing. Children []ChildCustomization `json:"children,omitempty"` Type CustomizationType `json:"type"` + // Explicit enablement decisions. See {@link McpServerCustomization.enablement}. + Enablement []CustomizationEnablement `json:"enablement,omitempty"` // Version of the plugin, sourced from the // [Open Plugins](https://open-plugins.com/) manifest's optional // `version` field (semver, e.g. `"1.2.0"`). Absent when the manifest @@ -2515,6 +2491,15 @@ type ClientPluginCustomization struct { Version *string `json:"version,omitempty"` // Opaque version token used by the host to detect changes. Nonce *string `json:"nonce,omitempty"` + // Explicit enablement decisions for children this plugin contributes, + // keyed by child name (for MCP servers, the server name as it appears in + // the bundled `.mcp.json`). + // + // Bundled children are discovered by the host rather than published by the + // client, so the client cannot attach `enablement` to them directly. This + // carries the client's global decision for each one; the host applies it + // under the child's durable key. + ChildEnablement map[string][]CustomizationEnablement `json:"childEnablement,omitempty"` } // A directory the host watches for this session. @@ -2541,18 +2526,6 @@ type DirectoryCustomization struct { Uri URI `json:"uri"` // Human-readable name. Name string `json:"name"` - // Explicit enablement decisions for this customization, one entry per scope - // that has one. This is a wire contract: producers MUST publish entries - // sorted by descending specificity (Session, Workspace, then Global). - // The agent host emits at most one Workspace entry, for the session's primary - // working directory. Consumers MAY treat - // `enablement[0]` as the decisive decision and - // `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - // absent or empty array means no explicit decision exists, so the - // customization is enabled by default. - // - // Only the agent host publishes this; clients treat it as read-only provenance. - Enablement []CustomizationEnablement `json:"enablement,omitempty"` // Icons for UI display. Icons []Icon `json:"icons,omitempty"` // Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -2566,8 +2539,6 @@ type DirectoryCustomization struct { // protocol; producers and consumers agree on its contents // out-of-band. Meta map[string]json.RawMessage `json:"_meta,omitempty"` - // Whether this container is currently enabled. - Enabled bool `json:"enabled"` // `clientId` of the client that contributed this container. Absent for // server-originated entries. ClientId *string `json:"clientId,omitempty"` @@ -2581,6 +2552,8 @@ type DirectoryCustomization struct { // nothing. Children []ChildCustomization `json:"children,omitempty"` Type CustomizationType `json:"type"` + // Whether this container is currently enabled. + Enabled bool `json:"enabled"` // Which child customization type this directory holds. Contents CustomizationType `json:"contents"` // Whether clients may write into this directory. @@ -2607,18 +2580,6 @@ type AgentCustomization struct { Uri URI `json:"uri"` // Human-readable name. Name string `json:"name"` - // Explicit enablement decisions for this customization, one entry per scope - // that has one. This is a wire contract: producers MUST publish entries - // sorted by descending specificity (Session, Workspace, then Global). - // The agent host emits at most one Workspace entry, for the session's primary - // working directory. Consumers MAY treat - // `enablement[0]` as the decisive decision and - // `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - // absent or empty array means no explicit decision exists, so the - // customization is enabled by default. - // - // Only the agent host publishes this; clients treat it as read-only provenance. - Enablement []CustomizationEnablement `json:"enablement,omitempty"` // Icons for UI display. Icons []Icon `json:"icons,omitempty"` // Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -2637,9 +2598,10 @@ type AgentCustomization struct { // 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. + // enabled state of a plugin child is the plugin's derived enabled value and + // `(child.enabled ?? true)`, so a disabled plugin disables every child + // regardless of each child's own flag. A directory child instead uses the + // directory's `enabled` value and its own flag. // // A child is turned on or off by id with // {@link SessionCustomizationToggledAction | `session/customizationToggled`}. @@ -2691,18 +2653,6 @@ type SkillCustomization struct { Uri URI `json:"uri"` // Human-readable name. Name string `json:"name"` - // Explicit enablement decisions for this customization, one entry per scope - // that has one. This is a wire contract: producers MUST publish entries - // sorted by descending specificity (Session, Workspace, then Global). - // The agent host emits at most one Workspace entry, for the session's primary - // working directory. Consumers MAY treat - // `enablement[0]` as the decisive decision and - // `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - // absent or empty array means no explicit decision exists, so the - // customization is enabled by default. - // - // Only the agent host publishes this; clients treat it as read-only provenance. - Enablement []CustomizationEnablement `json:"enablement,omitempty"` // Icons for UI display. Icons []Icon `json:"icons,omitempty"` // Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -2721,9 +2671,10 @@ type SkillCustomization struct { // 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. + // enabled state of a plugin child is the plugin's derived enabled value and + // `(child.enabled ?? true)`, so a disabled plugin disables every child + // regardless of each child's own flag. A directory child instead uses the + // directory's `enabled` value and its own flag. // // A child is turned on or off by id with // {@link SessionCustomizationToggledAction | `session/customizationToggled`}. @@ -2758,18 +2709,6 @@ type PromptCustomization struct { Uri URI `json:"uri"` // Human-readable name. Name string `json:"name"` - // Explicit enablement decisions for this customization, one entry per scope - // that has one. This is a wire contract: producers MUST publish entries - // sorted by descending specificity (Session, Workspace, then Global). - // The agent host emits at most one Workspace entry, for the session's primary - // working directory. Consumers MAY treat - // `enablement[0]` as the decisive decision and - // `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - // absent or empty array means no explicit decision exists, so the - // customization is enabled by default. - // - // Only the agent host publishes this; clients treat it as read-only provenance. - Enablement []CustomizationEnablement `json:"enablement,omitempty"` // Icons for UI display. Icons []Icon `json:"icons,omitempty"` // Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -2788,9 +2727,10 @@ type PromptCustomization struct { // 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. + // enabled state of a plugin child is the plugin's derived enabled value and + // `(child.enabled ?? true)`, so a disabled plugin disables every child + // regardless of each child's own flag. A directory child instead uses the + // directory's `enabled` value and its own flag. // // A child is turned on or off by id with // {@link SessionCustomizationToggledAction | `session/customizationToggled`}. @@ -2824,18 +2764,6 @@ type RuleCustomization struct { Uri URI `json:"uri"` // Human-readable name. Name string `json:"name"` - // Explicit enablement decisions for this customization, one entry per scope - // that has one. This is a wire contract: producers MUST publish entries - // sorted by descending specificity (Session, Workspace, then Global). - // The agent host emits at most one Workspace entry, for the session's primary - // working directory. Consumers MAY treat - // `enablement[0]` as the decisive decision and - // `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - // absent or empty array means no explicit decision exists, so the - // customization is enabled by default. - // - // Only the agent host publishes this; clients treat it as read-only provenance. - Enablement []CustomizationEnablement `json:"enablement,omitempty"` // Icons for UI display. Icons []Icon `json:"icons,omitempty"` // Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -2854,9 +2782,10 @@ type RuleCustomization struct { // 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. + // enabled state of a plugin child is the plugin's derived enabled value and + // `(child.enabled ?? true)`, so a disabled plugin disables every child + // regardless of each child's own flag. A directory child instead uses the + // directory's `enabled` value and its own flag. // // A child is turned on or off by id with // {@link SessionCustomizationToggledAction | `session/customizationToggled`}. @@ -2889,18 +2818,6 @@ type HookCustomization struct { Uri URI `json:"uri"` // Human-readable name. Name string `json:"name"` - // Explicit enablement decisions for this customization, one entry per scope - // that has one. This is a wire contract: producers MUST publish entries - // sorted by descending specificity (Session, Workspace, then Global). - // The agent host emits at most one Workspace entry, for the session's primary - // working directory. Consumers MAY treat - // `enablement[0]` as the decisive decision and - // `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - // absent or empty array means no explicit decision exists, so the - // customization is enabled by default. - // - // Only the agent host publishes this; clients treat it as read-only provenance. - Enablement []CustomizationEnablement `json:"enablement,omitempty"` // Icons for UI display. Icons []Icon `json:"icons,omitempty"` // Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -2919,9 +2836,10 @@ type HookCustomization struct { // 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. + // enabled state of a plugin child is the plugin's derived enabled value and + // `(child.enabled ?? true)`, so a disabled plugin disables every child + // regardless of each child's own flag. A directory child instead uses the + // directory's `enabled` value and its own flag. // // A child is turned on or off by id with // {@link SessionCustomizationToggledAction | `session/customizationToggled`}. @@ -2952,18 +2870,6 @@ type McpServerCustomization struct { Uri URI `json:"uri"` // Human-readable name. Name string `json:"name"` - // Explicit enablement decisions for this customization, one entry per scope - // that has one. This is a wire contract: producers MUST publish entries - // sorted by descending specificity (Session, Workspace, then Global). - // The agent host emits at most one Workspace entry, for the session's primary - // working directory. Consumers MAY treat - // `enablement[0]` as the decisive decision and - // `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - // absent or empty array means no explicit decision exists, so the - // customization is enabled by default. - // - // Only the agent host publishes this; clients treat it as read-only provenance. - Enablement []CustomizationEnablement `json:"enablement,omitempty"` // Icons for UI display. Icons []Icon `json:"icons,omitempty"` // Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -2978,9 +2884,32 @@ type McpServerCustomization struct { // out-of-band. Meta map[string]json.RawMessage `json:"_meta,omitempty"` Type CustomizationType `json:"type"` - // Whether this MCP server is effectively enabled after resolving all scopes. - // {@link CustomizationBase.enablement | `enablement`} records its inputs. - Enabled bool `json:"enabled"` + // Source URI of the plugin that contributes this server. A plugin-provided + // server keeps this durable identity while temporarily published top-level; + // its durable enablement key is derived from this URI. + // + // Absent means this is an unowned server, whose durable key is + // `mcpServers#`. + OwningPluginUri *URI `json:"owningPluginUri,omitempty"` + // Explicit enablement decisions for this customization, one entry per scope + // that has one. This is a wire contract: producers MUST publish entries + // sorted by descending specificity (Session, Workspace, then Global). + // The agent host emits at most one Workspace entry, for the session's primary + // working directory. Consumers MAY treat + // `enablement[0]` as the decisive decision and + // `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + // absent or empty array means no explicit decision exists, so the + // customization is enabled by default. + // + // Flows in both directions. A client publishes this alongside a customization + // to assert its global decision, which is authoritative for the Global scope; + // a client always includes its global entry, even when enabled. The host + // publishes the fully resolved set across all scopes, and consumers derive + // the effective enabled value from that set. + Enablement []CustomizationEnablement `json:"enablement,omitempty"` + // Whether the client explicitly bundled this server and owns its Global + // enablement decision. + IsClientBundled *bool `json:"isClientBundled,omitempty"` // Current lifecycle state of the MCP server. State McpServerState `json:"state"` // An `mcp://`-protocol channel the client uses to side-channel traffic 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 0ab85ee99..b23d47d41 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -282,9 +282,9 @@ private fun withCustomizationEnablement(c: Customization, enablement: List CustomizationPlugin(c.value.copy(enabled = enabled, enablement = provenance)) - is CustomizationDirectory -> CustomizationDirectory(c.value.copy(enabled = enabled, enablement = provenance)) - is CustomizationMcpServer -> CustomizationMcpServer(c.value.copy(enabled = enabled, enablement = provenance)) + is CustomizationPlugin -> CustomizationPlugin(c.value.copy(enablement = provenance)) + is CustomizationDirectory -> CustomizationDirectory(c.value.copy(enabled = enabled)) + is CustomizationMcpServer -> CustomizationMcpServer(c.value.copy(enablement = provenance)) is CustomizationUnknown -> c } } @@ -293,12 +293,12 @@ private fun withChildCustomizationEnablement(c: ChildCustomization, enablement: val enabled = effectiveEnablement(enablement) val provenance = enablement.takeIf { it.isNotEmpty() }?.toList() return when (c) { - is ChildCustomizationAgent -> ChildCustomizationAgent(c.value.copy(enabled = enabled, enablement = provenance)) - is ChildCustomizationSkill -> ChildCustomizationSkill(c.value.copy(enabled = enabled, enablement = provenance)) - is ChildCustomizationPrompt -> ChildCustomizationPrompt(c.value.copy(enabled = enabled, enablement = provenance)) - is ChildCustomizationRule -> ChildCustomizationRule(c.value.copy(enabled = enabled, enablement = provenance)) - is ChildCustomizationHook -> ChildCustomizationHook(c.value.copy(enabled = enabled, enablement = provenance)) - is ChildCustomizationMcpServer -> ChildCustomizationMcpServer(c.value.copy(enabled = enabled, enablement = provenance)) + 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(enablement = provenance)) is ChildCustomizationUnknown -> c } } 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 b0bc05d55..b28cc47d1 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 @@ -1062,7 +1062,7 @@ data class SessionCustomizationToggledAction( */ val id: String, /** - * The complete set of explicit decisions, replacing any existing set. + * Explicit enablement decisions, replacing the previous list entirely. */ val enablement: List ) 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 638fc681f..dfb9d49ed 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 @@ -3269,20 +3269,6 @@ data class PluginCustomization( * Human-readable name. */ val name: String, - /** - * Explicit enablement decisions for this customization, one entry per scope - * that has one. This is a wire contract: producers MUST publish entries - * sorted by descending specificity (Session, Workspace, then Global). - * The agent host emits at most one Workspace entry, for the session's primary - * working directory. Consumers MAY treat - * `enablement[0]` as the decisive decision and - * `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - * absent or empty array means no explicit decision exists, so the - * customization is enabled by default. - * - * Only the agent host publishes this; clients treat it as read-only provenance. - */ - val enablement: List? = null, /** * Icons for UI display. */ @@ -3303,10 +3289,6 @@ data class PluginCustomization( */ @SerialName("_meta") val meta: Map? = null, - /** - * Whether this container is currently enabled. - */ - val enabled: Boolean, /** * `clientId` of the client that contributed this container. Absent for * server-originated entries. @@ -3326,6 +3308,10 @@ data class PluginCustomization( */ val children: List? = null, val type: CustomizationType, + /** + * Explicit enablement decisions. See {@link McpServerCustomization.enablement}. + */ + val enablement: List? = null, /** * Version of the plugin, sourced from the * [Open Plugins](https://open-plugins.com/) manifest's optional @@ -3359,20 +3345,6 @@ data class ClientPluginCustomization( * Human-readable name. */ val name: String, - /** - * Explicit enablement decisions for this customization, one entry per scope - * that has one. This is a wire contract: producers MUST publish entries - * sorted by descending specificity (Session, Workspace, then Global). - * The agent host emits at most one Workspace entry, for the session's primary - * working directory. Consumers MAY treat - * `enablement[0]` as the decisive decision and - * `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - * absent or empty array means no explicit decision exists, so the - * customization is enabled by default. - * - * Only the agent host publishes this; clients treat it as read-only provenance. - */ - val enablement: List? = null, /** * Icons for UI display. */ @@ -3393,10 +3365,6 @@ data class ClientPluginCustomization( */ @SerialName("_meta") val meta: Map? = null, - /** - * Whether this container is currently enabled. - */ - val enabled: Boolean, /** * `clientId` of the client that contributed this container. Absent for * server-originated entries. @@ -3416,6 +3384,10 @@ data class ClientPluginCustomization( */ val children: List? = null, val type: CustomizationType, + /** + * Explicit enablement decisions. See {@link McpServerCustomization.enablement}. + */ + val enablement: List? = null, /** * Version of the plugin, sourced from the * [Open Plugins](https://open-plugins.com/) manifest's optional @@ -3428,7 +3400,18 @@ data class ClientPluginCustomization( /** * Opaque version token used by the host to detect changes. */ - val nonce: String? = null + val nonce: String? = null, + /** + * Explicit enablement decisions for children this plugin contributes, + * keyed by child name (for MCP servers, the server name as it appears in + * the bundled `.mcp.json`). + * + * Bundled children are discovered by the host rather than published by the + * client, so the client cannot attach `enablement` to them directly. This + * carries the client's global decision for each one; the host applies it + * under the child's durable key. + */ + val childEnablement: Map>? = null ) @Serializable @@ -3453,20 +3436,6 @@ data class DirectoryCustomization( * Human-readable name. */ val name: String, - /** - * Explicit enablement decisions for this customization, one entry per scope - * that has one. This is a wire contract: producers MUST publish entries - * sorted by descending specificity (Session, Workspace, then Global). - * The agent host emits at most one Workspace entry, for the session's primary - * working directory. Consumers MAY treat - * `enablement[0]` as the decisive decision and - * `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - * absent or empty array means no explicit decision exists, so the - * customization is enabled by default. - * - * Only the agent host publishes this; clients treat it as read-only provenance. - */ - val enablement: List? = null, /** * Icons for UI display. */ @@ -3487,10 +3456,6 @@ data class DirectoryCustomization( */ @SerialName("_meta") val meta: Map? = null, - /** - * Whether this container is currently enabled. - */ - val enabled: Boolean, /** * `clientId` of the client that contributed this container. Absent for * server-originated entries. @@ -3510,6 +3475,10 @@ data class DirectoryCustomization( */ val children: List? = null, val type: CustomizationType, + /** + * Whether this container is currently enabled. + */ + val enabled: Boolean, /** * Which child customization type this directory holds. */ @@ -3542,20 +3511,6 @@ data class AgentCustomization( * Human-readable name. */ val name: String, - /** - * Explicit enablement decisions for this customization, one entry per scope - * that has one. This is a wire contract: producers MUST publish entries - * sorted by descending specificity (Session, Workspace, then Global). - * The agent host emits at most one Workspace entry, for the session's primary - * working directory. Consumers MAY treat - * `enablement[0]` as the decisive decision and - * `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - * absent or empty array means no explicit decision exists, so the - * customization is enabled by default. - * - * Only the agent host publishes this; clients treat it as read-only provenance. - */ - val enablement: List? = null, /** * Icons for UI display. */ @@ -3582,9 +3537,10 @@ data class AgentCustomization( * 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. + * enabled state of a plugin child is the plugin's derived enabled value and + * `(child.enabled ?? true)`, so a disabled plugin disables every child + * regardless of each child's own flag. A directory child instead uses the + * directory's `enabled` value and its own flag. * * A child is turned on or off by id with * {@link SessionCustomizationToggledAction | `session/customizationToggled`}. @@ -3648,20 +3604,6 @@ data class SkillCustomization( * Human-readable name. */ val name: String, - /** - * Explicit enablement decisions for this customization, one entry per scope - * that has one. This is a wire contract: producers MUST publish entries - * sorted by descending specificity (Session, Workspace, then Global). - * The agent host emits at most one Workspace entry, for the session's primary - * working directory. Consumers MAY treat - * `enablement[0]` as the decisive decision and - * `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - * absent or empty array means no explicit decision exists, so the - * customization is enabled by default. - * - * Only the agent host publishes this; clients treat it as read-only provenance. - */ - val enablement: List? = null, /** * Icons for UI display. */ @@ -3688,9 +3630,10 @@ data class SkillCustomization( * 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. + * enabled state of a plugin child is the plugin's derived enabled value and + * `(child.enabled ?? true)`, so a disabled plugin disables every child + * regardless of each child's own flag. A directory child instead uses the + * directory's `enabled` value and its own flag. * * A child is turned on or off by id with * {@link SessionCustomizationToggledAction | `session/customizationToggled`}. @@ -3738,20 +3681,6 @@ data class PromptCustomization( * Human-readable name. */ val name: String, - /** - * Explicit enablement decisions for this customization, one entry per scope - * that has one. This is a wire contract: producers MUST publish entries - * sorted by descending specificity (Session, Workspace, then Global). - * The agent host emits at most one Workspace entry, for the session's primary - * working directory. Consumers MAY treat - * `enablement[0]` as the decisive decision and - * `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - * absent or empty array means no explicit decision exists, so the - * customization is enabled by default. - * - * Only the agent host publishes this; clients treat it as read-only provenance. - */ - val enablement: List? = null, /** * Icons for UI display. */ @@ -3778,9 +3707,10 @@ data class PromptCustomization( * 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. + * enabled state of a plugin child is the plugin's derived enabled value and + * `(child.enabled ?? true)`, so a disabled plugin disables every child + * regardless of each child's own flag. A directory child instead uses the + * directory's `enabled` value and its own flag. * * A child is turned on or off by id with * {@link SessionCustomizationToggledAction | `session/customizationToggled`}. @@ -3815,20 +3745,6 @@ data class RuleCustomization( * Human-readable name. */ val name: String, - /** - * Explicit enablement decisions for this customization, one entry per scope - * that has one. This is a wire contract: producers MUST publish entries - * sorted by descending specificity (Session, Workspace, then Global). - * The agent host emits at most one Workspace entry, for the session's primary - * working directory. Consumers MAY treat - * `enablement[0]` as the decisive decision and - * `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - * absent or empty array means no explicit decision exists, so the - * customization is enabled by default. - * - * Only the agent host publishes this; clients treat it as read-only provenance. - */ - val enablement: List? = null, /** * Icons for UI display. */ @@ -3855,9 +3771,10 @@ data class RuleCustomization( * 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. + * enabled state of a plugin child is the plugin's derived enabled value and + * `(child.enabled ?? true)`, so a disabled plugin disables every child + * regardless of each child's own flag. A directory child instead uses the + * directory's `enabled` value and its own flag. * * A child is turned on or off by id with * {@link SessionCustomizationToggledAction | `session/customizationToggled`}. @@ -3903,20 +3820,6 @@ data class HookCustomization( * Human-readable name. */ val name: String, - /** - * Explicit enablement decisions for this customization, one entry per scope - * that has one. This is a wire contract: producers MUST publish entries - * sorted by descending specificity (Session, Workspace, then Global). - * The agent host emits at most one Workspace entry, for the session's primary - * working directory. Consumers MAY treat - * `enablement[0]` as the decisive decision and - * `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - * absent or empty array means no explicit decision exists, so the - * customization is enabled by default. - * - * Only the agent host publishes this; clients treat it as read-only provenance. - */ - val enablement: List? = null, /** * Icons for UI display. */ @@ -3943,9 +3846,10 @@ data class HookCustomization( * 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. + * enabled state of a plugin child is the plugin's derived enabled value and + * `(child.enabled ?? true)`, so a disabled plugin disables every child + * regardless of each child's own flag. A directory child instead uses the + * directory's `enabled` value and its own flag. * * A child is turned on or off by id with * {@link SessionCustomizationToggledAction | `session/customizationToggled`}. @@ -3976,20 +3880,6 @@ data class McpServerCustomization( * Human-readable name. */ val name: String, - /** - * Explicit enablement decisions for this customization, one entry per scope - * that has one. This is a wire contract: producers MUST publish entries - * sorted by descending specificity (Session, Workspace, then Global). - * The agent host emits at most one Workspace entry, for the session's primary - * working directory. Consumers MAY treat - * `enablement[0]` as the decisive decision and - * `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - * absent or empty array means no explicit decision exists, so the - * customization is enabled by default. - * - * Only the agent host publishes this; clients treat it as read-only provenance. - */ - val enablement: List? = null, /** * Icons for UI display. */ @@ -4012,10 +3902,37 @@ data class McpServerCustomization( val meta: Map? = null, val type: CustomizationType, /** - * Whether this MCP server is effectively enabled after resolving all scopes. - * {@link CustomizationBase.enablement | `enablement`} records its inputs. + * Source URI of the plugin that contributes this server. A plugin-provided + * server keeps this durable identity while temporarily published top-level; + * its durable enablement key is derived from this URI. + * + * Absent means this is an unowned server, whose durable key is + * `mcpServers#`. */ - val enabled: Boolean, + val owningPluginUri: String? = null, + /** + * Explicit enablement decisions for this customization, one entry per scope + * that has one. This is a wire contract: producers MUST publish entries + * sorted by descending specificity (Session, Workspace, then Global). + * The agent host emits at most one Workspace entry, for the session's primary + * working directory. Consumers MAY treat + * `enablement[0]` as the decisive decision and + * `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + * absent or empty array means no explicit decision exists, so the + * customization is enabled by default. + * + * Flows in both directions. A client publishes this alongside a customization + * to assert its global decision, which is authoritative for the Global scope; + * a client always includes its global entry, even when enabled. The host + * publishes the fully resolved set across all scopes, and consumers derive + * the effective enabled value from that set. + */ + val enablement: List? = null, + /** + * Whether the client explicitly bundled this server and owns its Global + * enablement decision. + */ + val isClientBundled: Boolean? = null, /** * Current lifecycle state of the MCP server. */ diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index 0f4f83af6..e4369dda1 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -1159,15 +1159,16 @@ pub struct SessionCustomizationsChangedAction { pub customizations: Vec, } -/// A client toggled a customization on or off. +/// A client updated a customization's enablement decisions. /// /// 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 +/// children inside each container (a skill, agent, or other entry). Plugins +/// and MCP servers retain the matched entry's explicit decisions; other +/// entries update their `enabled` flag. Disabling a plugin still disables all +/// of its children — the effective state of a plugin child is the plugin's +/// derived enabled value and `(child.enabled ?? true)` — so toggling a child +/// only matters while its plugin is enabled. Is a no-op when no /// customization has the given `id`. /// /// The `enablement` array completely replaces all explicit decisions. A caller @@ -1177,7 +1178,7 @@ pub struct SessionCustomizationsChangedAction { pub struct SessionCustomizationToggledAction { /// The id of the container or child to update. pub id: String, - /// The complete set of explicit decisions, replacing any existing set. + /// Explicit enablement decisions, replacing the previous list entirely. pub enablement: Vec, } diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index bdedec6dc..64a7a2c06 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -2924,19 +2924,6 @@ pub struct PluginCustomization { pub uri: Uri, /// Human-readable name. pub name: String, - /// Explicit enablement decisions for this customization, one entry per scope - /// that has one. This is a wire contract: producers MUST publish entries - /// sorted by descending specificity (Session, Workspace, then Global). - /// The agent host emits at most one Workspace entry, for the session's primary - /// working directory. Consumers MAY treat - /// `enablement[0]` as the decisive decision and - /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - /// absent or empty array means no explicit decision exists, so the - /// customization is enabled by default. - /// - /// Only the agent host publishes this; clients treat it as read-only provenance. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub enablement: Option>, /// Icons for UI display. #[serde(default, skip_serializing_if = "Option::is_none")] pub icons: Option>, @@ -2953,8 +2940,6 @@ pub struct PluginCustomization { /// out-of-band. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, - /// Whether this container is currently enabled. - pub enabled: bool, /// `clientId` of the client that contributed this container. Absent for /// server-originated entries. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -2970,6 +2955,9 @@ pub struct PluginCustomization { /// nothing. #[serde(default, skip_serializing_if = "Option::is_none")] pub children: Option>, + /// Explicit enablement decisions. See {@link McpServerCustomization.enablement}. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enablement: Option>, /// Version of the plugin, sourced from the /// [Open Plugins](https://open-plugins.com/) manifest's optional /// `version` field (semver, e.g. `"1.2.0"`). Absent when the manifest @@ -3006,19 +2994,6 @@ pub struct ClientPluginCustomization { pub uri: Uri, /// Human-readable name. pub name: String, - /// Explicit enablement decisions for this customization, one entry per scope - /// that has one. This is a wire contract: producers MUST publish entries - /// sorted by descending specificity (Session, Workspace, then Global). - /// The agent host emits at most one Workspace entry, for the session's primary - /// working directory. Consumers MAY treat - /// `enablement[0]` as the decisive decision and - /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - /// absent or empty array means no explicit decision exists, so the - /// customization is enabled by default. - /// - /// Only the agent host publishes this; clients treat it as read-only provenance. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub enablement: Option>, /// Icons for UI display. #[serde(default, skip_serializing_if = "Option::is_none")] pub icons: Option>, @@ -3035,8 +3010,6 @@ pub struct ClientPluginCustomization { /// out-of-band. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, - /// Whether this container is currently enabled. - pub enabled: bool, /// `clientId` of the client that contributed this container. Absent for /// server-originated entries. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -3052,6 +3025,9 @@ pub struct ClientPluginCustomization { /// nothing. #[serde(default, skip_serializing_if = "Option::is_none")] pub children: Option>, + /// Explicit enablement decisions. See {@link McpServerCustomization.enablement}. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enablement: Option>, /// Version of the plugin, sourced from the /// [Open Plugins](https://open-plugins.com/) manifest's optional /// `version` field (semver, e.g. `"1.2.0"`). Absent when the manifest @@ -3063,6 +3039,16 @@ pub struct ClientPluginCustomization { /// Opaque version token used by the host to detect changes. #[serde(default, skip_serializing_if = "Option::is_none")] pub nonce: Option, + /// Explicit enablement decisions for children this plugin contributes, + /// keyed by child name (for MCP servers, the server name as it appears in + /// the bundled `.mcp.json`). + /// + /// Bundled children are discovered by the host rather than published by the + /// client, so the client cannot attach `enablement` to them directly. This + /// carries the client's global decision for each one; the host applies it + /// under the child's durable key. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub child_enablement: Option>>, } /// A directory the host watches for this session. @@ -3091,19 +3077,6 @@ pub struct DirectoryCustomization { pub uri: Uri, /// Human-readable name. pub name: String, - /// Explicit enablement decisions for this customization, one entry per scope - /// that has one. This is a wire contract: producers MUST publish entries - /// sorted by descending specificity (Session, Workspace, then Global). - /// The agent host emits at most one Workspace entry, for the session's primary - /// working directory. Consumers MAY treat - /// `enablement[0]` as the decisive decision and - /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - /// absent or empty array means no explicit decision exists, so the - /// customization is enabled by default. - /// - /// Only the agent host publishes this; clients treat it as read-only provenance. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub enablement: Option>, /// Icons for UI display. #[serde(default, skip_serializing_if = "Option::is_none")] pub icons: Option>, @@ -3120,8 +3093,6 @@ pub struct DirectoryCustomization { /// out-of-band. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, - /// Whether this container is currently enabled. - pub enabled: bool, /// `clientId` of the client that contributed this container. Absent for /// server-originated entries. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -3137,6 +3108,8 @@ pub struct DirectoryCustomization { /// nothing. #[serde(default, skip_serializing_if = "Option::is_none")] pub children: Option>, + /// Whether this container is currently enabled. + pub enabled: bool, /// Which child customization type this directory holds. pub contents: CustomizationType, /// Whether clients may write into this directory. @@ -3165,19 +3138,6 @@ pub struct AgentCustomization { pub uri: Uri, /// Human-readable name. pub name: String, - /// Explicit enablement decisions for this customization, one entry per scope - /// that has one. This is a wire contract: producers MUST publish entries - /// sorted by descending specificity (Session, Workspace, then Global). - /// The agent host emits at most one Workspace entry, for the session's primary - /// working directory. Consumers MAY treat - /// `enablement[0]` as the decisive decision and - /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - /// absent or empty array means no explicit decision exists, so the - /// customization is enabled by default. - /// - /// Only the agent host publishes this; clients treat it as read-only provenance. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub enablement: Option>, /// Icons for UI display. #[serde(default, skip_serializing_if = "Option::is_none")] pub icons: Option>, @@ -3199,9 +3159,10 @@ pub struct AgentCustomization { /// 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. + /// enabled state of a plugin child is the plugin's derived enabled value and + /// `(child.enabled ?? true)`, so a disabled plugin disables every child + /// regardless of each child's own flag. A directory child instead uses the + /// directory's `enabled` value and its own flag. /// /// A child is turned on or off by id with /// {@link SessionCustomizationToggledAction | `session/customizationToggled`}. @@ -3260,19 +3221,6 @@ pub struct SkillCustomization { pub uri: Uri, /// Human-readable name. pub name: String, - /// Explicit enablement decisions for this customization, one entry per scope - /// that has one. This is a wire contract: producers MUST publish entries - /// sorted by descending specificity (Session, Workspace, then Global). - /// The agent host emits at most one Workspace entry, for the session's primary - /// working directory. Consumers MAY treat - /// `enablement[0]` as the decisive decision and - /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - /// absent or empty array means no explicit decision exists, so the - /// customization is enabled by default. - /// - /// Only the agent host publishes this; clients treat it as read-only provenance. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub enablement: Option>, /// Icons for UI display. #[serde(default, skip_serializing_if = "Option::is_none")] pub icons: Option>, @@ -3294,9 +3242,10 @@ pub struct SkillCustomization { /// 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. + /// enabled state of a plugin child is the plugin's derived enabled value and + /// `(child.enabled ?? true)`, so a disabled plugin disables every child + /// regardless of each child's own flag. A directory child instead uses the + /// directory's `enabled` value and its own flag. /// /// A child is turned on or off by id with /// {@link SessionCustomizationToggledAction | `session/customizationToggled`}. @@ -3336,19 +3285,6 @@ pub struct PromptCustomization { pub uri: Uri, /// Human-readable name. pub name: String, - /// Explicit enablement decisions for this customization, one entry per scope - /// that has one. This is a wire contract: producers MUST publish entries - /// sorted by descending specificity (Session, Workspace, then Global). - /// The agent host emits at most one Workspace entry, for the session's primary - /// working directory. Consumers MAY treat - /// `enablement[0]` as the decisive decision and - /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - /// absent or empty array means no explicit decision exists, so the - /// customization is enabled by default. - /// - /// Only the agent host publishes this; clients treat it as read-only provenance. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub enablement: Option>, /// Icons for UI display. #[serde(default, skip_serializing_if = "Option::is_none")] pub icons: Option>, @@ -3370,9 +3306,10 @@ pub struct PromptCustomization { /// 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. + /// enabled state of a plugin child is the plugin's derived enabled value and + /// `(child.enabled ?? true)`, so a disabled plugin disables every child + /// regardless of each child's own flag. A directory child instead uses the + /// directory's `enabled` value and its own flag. /// /// A child is turned on or off by id with /// {@link SessionCustomizationToggledAction | `session/customizationToggled`}. @@ -3409,19 +3346,6 @@ pub struct RuleCustomization { pub uri: Uri, /// Human-readable name. pub name: String, - /// Explicit enablement decisions for this customization, one entry per scope - /// that has one. This is a wire contract: producers MUST publish entries - /// sorted by descending specificity (Session, Workspace, then Global). - /// The agent host emits at most one Workspace entry, for the session's primary - /// working directory. Consumers MAY treat - /// `enablement[0]` as the decisive decision and - /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - /// absent or empty array means no explicit decision exists, so the - /// customization is enabled by default. - /// - /// Only the agent host publishes this; clients treat it as read-only provenance. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub enablement: Option>, /// Icons for UI display. #[serde(default, skip_serializing_if = "Option::is_none")] pub icons: Option>, @@ -3443,9 +3367,10 @@ pub struct RuleCustomization { /// 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. + /// enabled state of a plugin child is the plugin's derived enabled value and + /// `(child.enabled ?? true)`, so a disabled plugin disables every child + /// regardless of each child's own flag. A directory child instead uses the + /// directory's `enabled` value and its own flag. /// /// A child is turned on or off by id with /// {@link SessionCustomizationToggledAction | `session/customizationToggled`}. @@ -3483,19 +3408,6 @@ pub struct HookCustomization { pub uri: Uri, /// Human-readable name. pub name: String, - /// Explicit enablement decisions for this customization, one entry per scope - /// that has one. This is a wire contract: producers MUST publish entries - /// sorted by descending specificity (Session, Workspace, then Global). - /// The agent host emits at most one Workspace entry, for the session's primary - /// working directory. Consumers MAY treat - /// `enablement[0]` as the decisive decision and - /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - /// absent or empty array means no explicit decision exists, so the - /// customization is enabled by default. - /// - /// Only the agent host publishes this; clients treat it as read-only provenance. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub enablement: Option>, /// Icons for UI display. #[serde(default, skip_serializing_if = "Option::is_none")] pub icons: Option>, @@ -3517,9 +3429,10 @@ pub struct HookCustomization { /// 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. + /// enabled state of a plugin child is the plugin's derived enabled value and + /// `(child.enabled ?? true)`, so a disabled plugin disables every child + /// regardless of each child's own flag. A directory child instead uses the + /// directory's `enabled` value and its own flag. /// /// A child is turned on or off by id with /// {@link SessionCustomizationToggledAction | `session/customizationToggled`}. @@ -3552,19 +3465,6 @@ pub struct McpServerCustomization { pub uri: Uri, /// Human-readable name. pub name: String, - /// Explicit enablement decisions for this customization, one entry per scope - /// that has one. This is a wire contract: producers MUST publish entries - /// sorted by descending specificity (Session, Workspace, then Global). - /// The agent host emits at most one Workspace entry, for the session's primary - /// working directory. Consumers MAY treat - /// `enablement[0]` as the decisive decision and - /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - /// absent or empty array means no explicit decision exists, so the - /// customization is enabled by default. - /// - /// Only the agent host publishes this; clients treat it as read-only provenance. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub enablement: Option>, /// Icons for UI display. #[serde(default, skip_serializing_if = "Option::is_none")] pub icons: Option>, @@ -3581,9 +3481,35 @@ pub struct McpServerCustomization { /// out-of-band. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, - /// Whether this MCP server is effectively enabled after resolving all scopes. - /// {@link CustomizationBase.enablement | `enablement`} records its inputs. - pub enabled: bool, + /// Source URI of the plugin that contributes this server. A plugin-provided + /// server keeps this durable identity while temporarily published top-level; + /// its durable enablement key is derived from this URI. + /// + /// Absent means this is an unowned server, whose durable key is + /// `mcpServers#`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub owning_plugin_uri: Option, + /// Explicit enablement decisions for this customization, one entry per scope + /// that has one. This is a wire contract: producers MUST publish entries + /// sorted by descending specificity (Session, Workspace, then Global). + /// The agent host emits at most one Workspace entry, for the session's primary + /// working directory. Consumers MAY treat + /// `enablement[0]` as the decisive decision and + /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + /// absent or empty array means no explicit decision exists, so the + /// customization is enabled by default. + /// + /// Flows in both directions. A client publishes this alongside a customization + /// to assert its global decision, which is authoritative for the Global scope; + /// a client always includes its global entry, even when enabled. The host + /// publishes the fully resolved set across all scopes, and consumers derive + /// the effective enabled value from that set. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enablement: Option>, + /// Whether the client explicitly bundled this server and owns its Global + /// enablement decision. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub is_client_bundled: Option, /// Current lifecycle state of the MCP server. pub state: McpServerState, /// An `mcp://`-protocol channel the client uses to side-channel traffic diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 2a6ef0955..c25118796 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -511,15 +511,12 @@ fn apply_container_enablement(c: &mut Customization, enablement: &[Customization let provenance = (!enablement.is_empty()).then(|| enablement.to_vec()); match c { Customization::Plugin(p) => { - p.enabled = enabled; p.enablement = provenance; } Customization::Directory(d) => { d.enabled = enabled; - d.enablement = provenance; } Customization::McpServer(m) => { - m.enabled = enabled; m.enablement = provenance; } Customization::Unknown(_) => {} @@ -532,26 +529,20 @@ fn apply_child_enablement(c: &mut ChildCustomization, enablement: &[Customizatio match c { ChildCustomization::Agent(x) => { x.enabled = Some(enabled); - x.enablement = provenance; } ChildCustomization::Skill(x) => { x.enabled = Some(enabled); - x.enablement = provenance; } ChildCustomization::Prompt(x) => { x.enabled = Some(enabled); - x.enablement = provenance; } ChildCustomization::Rule(x) => { x.enabled = Some(enabled); - x.enablement = provenance; } ChildCustomization::Hook(x) => { x.enabled = Some(enabled); - x.enablement = provenance; } ChildCustomization::McpServer(x) => { - x.enabled = enabled; x.enablement = provenance; } ChildCustomization::Unknown(_) => {} diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index d7220981a..f439fcb23 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -1356,7 +1356,7 @@ public struct SessionCustomizationToggledAction: Codable, Sendable { public var type: ActionType /// The id of the container or child to update. public var id: String - /// The complete set of explicit decisions, replacing any existing set. + /// Explicit enablement decisions, replacing the previous list entirely. public var enablement: [CustomizationEnablement] 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 eca013fee..bdbeacfa2 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -3504,18 +3504,6 @@ public struct PluginCustomization: Codable, Sendable { public var uri: String /// Human-readable name. public var name: String - /// Explicit enablement decisions for this customization, one entry per scope - /// that has one. This is a wire contract: producers MUST publish entries - /// sorted by descending specificity (Session, Workspace, then Global). - /// The agent host emits at most one Workspace entry, for the session's primary - /// working directory. Consumers MAY treat - /// `enablement[0]` as the decisive decision and - /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - /// absent or empty array means no explicit decision exists, so the - /// customization is enabled by default. - /// - /// Only the agent host publishes this; clients treat it as read-only provenance. - public var enablement: [CustomizationEnablement]? /// Icons for UI display. public var icons: [Icon]? /// Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -3529,8 +3517,6 @@ public struct PluginCustomization: Codable, Sendable { /// protocol; producers and consumers agree on its contents /// out-of-band. public var meta: [String: AnyCodable]? - /// Whether this container is currently enabled. - public var enabled: Bool /// `clientId` of the client that contributed this container. Absent for /// server-originated entries. public var clientId: String? @@ -3544,6 +3530,8 @@ public struct PluginCustomization: Codable, Sendable { /// nothing. public var children: [ChildCustomization]? public var type: CustomizationType + /// Explicit enablement decisions. See {@link McpServerCustomization.enablement}. + public var enablement: [CustomizationEnablement]? /// Version of the plugin, sourced from the /// [Open Plugins](https://open-plugins.com/) manifest's optional /// `version` field (semver, e.g. `"1.2.0"`). Absent when the manifest @@ -3556,15 +3544,14 @@ public struct PluginCustomization: Codable, Sendable { case id case uri case name - case enablement case icons case range case meta = "_meta" - case enabled case clientId case load case children case type + case enablement case version } @@ -3572,29 +3559,27 @@ public struct PluginCustomization: Codable, Sendable { id: String, uri: String, name: String, - enablement: [CustomizationEnablement]? = nil, icons: [Icon]? = nil, range: TextRange? = nil, meta: [String: AnyCodable]? = nil, - enabled: Bool, clientId: String? = nil, load: CustomizationLoadState? = nil, children: [ChildCustomization]? = nil, type: CustomizationType, + enablement: [CustomizationEnablement]? = nil, version: String? = nil ) { self.id = id self.uri = uri self.name = name - self.enablement = enablement self.icons = icons self.range = range self.meta = meta - self.enabled = enabled self.clientId = clientId self.load = load self.children = children self.type = type + self.enablement = enablement self.version = version } } @@ -3614,18 +3599,6 @@ public struct ClientPluginCustomization: Codable, Sendable { public var uri: String /// Human-readable name. public var name: String - /// Explicit enablement decisions for this customization, one entry per scope - /// that has one. This is a wire contract: producers MUST publish entries - /// sorted by descending specificity (Session, Workspace, then Global). - /// The agent host emits at most one Workspace entry, for the session's primary - /// working directory. Consumers MAY treat - /// `enablement[0]` as the decisive decision and - /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - /// absent or empty array means no explicit decision exists, so the - /// customization is enabled by default. - /// - /// Only the agent host publishes this; clients treat it as read-only provenance. - public var enablement: [CustomizationEnablement]? /// Icons for UI display. public var icons: [Icon]? /// Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -3639,8 +3612,6 @@ public struct ClientPluginCustomization: Codable, Sendable { /// protocol; producers and consumers agree on its contents /// out-of-band. public var meta: [String: AnyCodable]? - /// Whether this container is currently enabled. - public var enabled: Bool /// `clientId` of the client that contributed this container. Absent for /// server-originated entries. public var clientId: String? @@ -3654,6 +3625,8 @@ public struct ClientPluginCustomization: Codable, Sendable { /// nothing. public var children: [ChildCustomization]? public var type: CustomizationType + /// Explicit enablement decisions. See {@link McpServerCustomization.enablement}. + public var enablement: [CustomizationEnablement]? /// Version of the plugin, sourced from the /// [Open Plugins](https://open-plugins.com/) manifest's optional /// `version` field (semver, e.g. `"1.2.0"`). Absent when the manifest @@ -3663,54 +3636,63 @@ public struct ClientPluginCustomization: Codable, Sendable { public var version: String? /// Opaque version token used by the host to detect changes. public var nonce: String? + /// Explicit enablement decisions for children this plugin contributes, + /// keyed by child name (for MCP servers, the server name as it appears in + /// the bundled `.mcp.json`). + /// + /// Bundled children are discovered by the host rather than published by the + /// client, so the client cannot attach `enablement` to them directly. This + /// carries the client's global decision for each one; the host applies it + /// under the child's durable key. + public var childEnablement: [String: [CustomizationEnablement]]? enum CodingKeys: String, CodingKey { case id case uri case name - case enablement case icons case range case meta = "_meta" - case enabled case clientId case load case children case type + case enablement case version case nonce + case childEnablement } public init( id: String, uri: String, name: String, - enablement: [CustomizationEnablement]? = nil, icons: [Icon]? = nil, range: TextRange? = nil, meta: [String: AnyCodable]? = nil, - enabled: Bool, clientId: String? = nil, load: CustomizationLoadState? = nil, children: [ChildCustomization]? = nil, type: CustomizationType, + enablement: [CustomizationEnablement]? = nil, version: String? = nil, - nonce: String? = nil + nonce: String? = nil, + childEnablement: [String: [CustomizationEnablement]]? = nil ) { self.id = id self.uri = uri self.name = name - self.enablement = enablement self.icons = icons self.range = range self.meta = meta - self.enabled = enabled self.clientId = clientId self.load = load self.children = children self.type = type + self.enablement = enablement self.version = version self.nonce = nonce + self.childEnablement = childEnablement } } @@ -3729,18 +3711,6 @@ public struct DirectoryCustomization: Codable, Sendable { public var uri: String /// Human-readable name. public var name: String - /// Explicit enablement decisions for this customization, one entry per scope - /// that has one. This is a wire contract: producers MUST publish entries - /// sorted by descending specificity (Session, Workspace, then Global). - /// The agent host emits at most one Workspace entry, for the session's primary - /// working directory. Consumers MAY treat - /// `enablement[0]` as the decisive decision and - /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - /// absent or empty array means no explicit decision exists, so the - /// customization is enabled by default. - /// - /// Only the agent host publishes this; clients treat it as read-only provenance. - public var enablement: [CustomizationEnablement]? /// Icons for UI display. public var icons: [Icon]? /// Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -3754,8 +3724,6 @@ public struct DirectoryCustomization: Codable, Sendable { /// protocol; producers and consumers agree on its contents /// out-of-band. public var meta: [String: AnyCodable]? - /// Whether this container is currently enabled. - public var enabled: Bool /// `clientId` of the client that contributed this container. Absent for /// server-originated entries. public var clientId: String? @@ -3769,6 +3737,8 @@ public struct DirectoryCustomization: Codable, Sendable { /// nothing. public var children: [ChildCustomization]? public var type: CustomizationType + /// Whether this container is currently enabled. + public var enabled: Bool /// Which child customization type this directory holds. public var contents: CustomizationType /// Whether clients may write into this directory. @@ -3778,15 +3748,14 @@ public struct DirectoryCustomization: Codable, Sendable { case id case uri case name - case enablement case icons case range case meta = "_meta" - case enabled case clientId case load case children case type + case enabled case contents case writable } @@ -3795,30 +3764,28 @@ public struct DirectoryCustomization: Codable, Sendable { id: String, uri: String, name: String, - enablement: [CustomizationEnablement]? = nil, icons: [Icon]? = nil, range: TextRange? = nil, meta: [String: AnyCodable]? = nil, - enabled: Bool, clientId: String? = nil, load: CustomizationLoadState? = nil, children: [ChildCustomization]? = nil, type: CustomizationType, + enabled: Bool, contents: CustomizationType, writable: Bool ) { self.id = id self.uri = uri self.name = name - self.enablement = enablement self.icons = icons self.range = range self.meta = meta - self.enabled = enabled self.clientId = clientId self.load = load self.children = children self.type = type + self.enabled = enabled self.contents = contents self.writable = writable } @@ -3839,18 +3806,6 @@ public struct AgentCustomization: Codable, Sendable { public var uri: String /// Human-readable name. public var name: String - /// Explicit enablement decisions for this customization, one entry per scope - /// that has one. This is a wire contract: producers MUST publish entries - /// sorted by descending specificity (Session, Workspace, then Global). - /// The agent host emits at most one Workspace entry, for the session's primary - /// working directory. Consumers MAY treat - /// `enablement[0]` as the decisive decision and - /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - /// absent or empty array means no explicit decision exists, so the - /// customization is enabled by default. - /// - /// Only the agent host publishes this; clients treat it as read-only provenance. - public var enablement: [CustomizationEnablement]? /// Icons for UI display. public var icons: [Icon]? /// Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -3869,9 +3824,10 @@ public struct AgentCustomization: Codable, Sendable { /// 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. + /// enabled state of a plugin child is the plugin's derived enabled value and + /// `(child.enabled ?? true)`, so a disabled plugin disables every child + /// regardless of each child's own flag. A directory child instead uses the + /// directory's `enabled` value and its own flag. /// /// A child is turned on or off by id with /// {@link SessionCustomizationToggledAction | `session/customizationToggled`}. @@ -3905,7 +3861,6 @@ public struct AgentCustomization: Codable, Sendable { case id case uri case name - case enablement case icons case range case meta = "_meta" @@ -3922,7 +3877,6 @@ public struct AgentCustomization: Codable, Sendable { id: String, uri: String, name: String, - enablement: [CustomizationEnablement]? = nil, icons: [Icon]? = nil, range: TextRange? = nil, meta: [String: AnyCodable]? = nil, @@ -3937,7 +3891,6 @@ public struct AgentCustomization: Codable, Sendable { self.id = id self.uri = uri self.name = name - self.enablement = enablement self.icons = icons self.range = range self.meta = meta @@ -3966,18 +3919,6 @@ public struct SkillCustomization: Codable, Sendable { public var uri: String /// Human-readable name. public var name: String - /// Explicit enablement decisions for this customization, one entry per scope - /// that has one. This is a wire contract: producers MUST publish entries - /// sorted by descending specificity (Session, Workspace, then Global). - /// The agent host emits at most one Workspace entry, for the session's primary - /// working directory. Consumers MAY treat - /// `enablement[0]` as the decisive decision and - /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - /// absent or empty array means no explicit decision exists, so the - /// customization is enabled by default. - /// - /// Only the agent host publishes this; clients treat it as read-only provenance. - public var enablement: [CustomizationEnablement]? /// Icons for UI display. public var icons: [Icon]? /// Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -3996,9 +3937,10 @@ public struct SkillCustomization: Codable, Sendable { /// 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. + /// enabled state of a plugin child is the plugin's derived enabled value and + /// `(child.enabled ?? true)`, so a disabled plugin disables every child + /// regardless of each child's own flag. A directory child instead uses the + /// directory's `enabled` value and its own flag. /// /// A child is turned on or off by id with /// {@link SessionCustomizationToggledAction | `session/customizationToggled`}. @@ -4020,7 +3962,6 @@ public struct SkillCustomization: Codable, Sendable { case id case uri case name - case enablement case icons case range case meta = "_meta" @@ -4035,7 +3976,6 @@ public struct SkillCustomization: Codable, Sendable { id: String, uri: String, name: String, - enablement: [CustomizationEnablement]? = nil, icons: [Icon]? = nil, range: TextRange? = nil, meta: [String: AnyCodable]? = nil, @@ -4048,7 +3988,6 @@ public struct SkillCustomization: Codable, Sendable { self.id = id self.uri = uri self.name = name - self.enablement = enablement self.icons = icons self.range = range self.meta = meta @@ -4075,18 +4014,6 @@ public struct PromptCustomization: Codable, Sendable { public var uri: String /// Human-readable name. public var name: String - /// Explicit enablement decisions for this customization, one entry per scope - /// that has one. This is a wire contract: producers MUST publish entries - /// sorted by descending specificity (Session, Workspace, then Global). - /// The agent host emits at most one Workspace entry, for the session's primary - /// working directory. Consumers MAY treat - /// `enablement[0]` as the decisive decision and - /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - /// absent or empty array means no explicit decision exists, so the - /// customization is enabled by default. - /// - /// Only the agent host publishes this; clients treat it as read-only provenance. - public var enablement: [CustomizationEnablement]? /// Icons for UI display. public var icons: [Icon]? /// Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -4105,9 +4032,10 @@ public struct PromptCustomization: Codable, Sendable { /// 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. + /// enabled state of a plugin child is the plugin's derived enabled value and + /// `(child.enabled ?? true)`, so a disabled plugin disables every child + /// regardless of each child's own flag. A directory child instead uses the + /// directory's `enabled` value and its own flag. /// /// A child is turned on or off by id with /// {@link SessionCustomizationToggledAction | `session/customizationToggled`}. @@ -4120,7 +4048,6 @@ public struct PromptCustomization: Codable, Sendable { case id case uri case name - case enablement case icons case range case meta = "_meta" @@ -4133,7 +4060,6 @@ public struct PromptCustomization: Codable, Sendable { id: String, uri: String, name: String, - enablement: [CustomizationEnablement]? = nil, icons: [Icon]? = nil, range: TextRange? = nil, meta: [String: AnyCodable]? = nil, @@ -4144,7 +4070,6 @@ public struct PromptCustomization: Codable, Sendable { self.id = id self.uri = uri self.name = name - self.enablement = enablement self.icons = icons self.range = range self.meta = meta @@ -4169,18 +4094,6 @@ public struct RuleCustomization: Codable, Sendable { public var uri: String /// Human-readable name. public var name: String - /// Explicit enablement decisions for this customization, one entry per scope - /// that has one. This is a wire contract: producers MUST publish entries - /// sorted by descending specificity (Session, Workspace, then Global). - /// The agent host emits at most one Workspace entry, for the session's primary - /// working directory. Consumers MAY treat - /// `enablement[0]` as the decisive decision and - /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - /// absent or empty array means no explicit decision exists, so the - /// customization is enabled by default. - /// - /// Only the agent host publishes this; clients treat it as read-only provenance. - public var enablement: [CustomizationEnablement]? /// Icons for UI display. public var icons: [Icon]? /// Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -4199,9 +4112,10 @@ public struct RuleCustomization: Codable, Sendable { /// 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. + /// enabled state of a plugin child is the plugin's derived enabled value and + /// `(child.enabled ?? true)`, so a disabled plugin disables every child + /// regardless of each child's own flag. A directory child instead uses the + /// directory's `enabled` value and its own flag. /// /// A child is turned on or off by id with /// {@link SessionCustomizationToggledAction | `session/customizationToggled`}. @@ -4221,7 +4135,6 @@ public struct RuleCustomization: Codable, Sendable { case id case uri case name - case enablement case icons case range case meta = "_meta" @@ -4236,7 +4149,6 @@ public struct RuleCustomization: Codable, Sendable { id: String, uri: String, name: String, - enablement: [CustomizationEnablement]? = nil, icons: [Icon]? = nil, range: TextRange? = nil, meta: [String: AnyCodable]? = nil, @@ -4249,7 +4161,6 @@ public struct RuleCustomization: Codable, Sendable { self.id = id self.uri = uri self.name = name - self.enablement = enablement self.icons = icons self.range = range self.meta = meta @@ -4276,18 +4187,6 @@ public struct HookCustomization: Codable, Sendable { public var uri: String /// Human-readable name. public var name: String - /// Explicit enablement decisions for this customization, one entry per scope - /// that has one. This is a wire contract: producers MUST publish entries - /// sorted by descending specificity (Session, Workspace, then Global). - /// The agent host emits at most one Workspace entry, for the session's primary - /// working directory. Consumers MAY treat - /// `enablement[0]` as the decisive decision and - /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - /// absent or empty array means no explicit decision exists, so the - /// customization is enabled by default. - /// - /// Only the agent host publishes this; clients treat it as read-only provenance. - public var enablement: [CustomizationEnablement]? /// Icons for UI display. public var icons: [Icon]? /// Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -4306,9 +4205,10 @@ public struct HookCustomization: Codable, Sendable { /// 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. + /// enabled state of a plugin child is the plugin's derived enabled value and + /// `(child.enabled ?? true)`, so a disabled plugin disables every child + /// regardless of each child's own flag. A directory child instead uses the + /// directory's `enabled` value and its own flag. /// /// A child is turned on or off by id with /// {@link SessionCustomizationToggledAction | `session/customizationToggled`}. @@ -4319,7 +4219,6 @@ public struct HookCustomization: Codable, Sendable { case id case uri case name - case enablement case icons case range case meta = "_meta" @@ -4331,7 +4230,6 @@ public struct HookCustomization: Codable, Sendable { id: String, uri: String, name: String, - enablement: [CustomizationEnablement]? = nil, icons: [Icon]? = nil, range: TextRange? = nil, meta: [String: AnyCodable]? = nil, @@ -4341,7 +4239,6 @@ public struct HookCustomization: Codable, Sendable { self.id = id self.uri = uri self.name = name - self.enablement = enablement self.icons = icons self.range = range self.meta = meta @@ -4365,18 +4262,6 @@ public struct McpServerCustomization: Codable, Sendable { public var uri: String /// Human-readable name. public var name: String - /// Explicit enablement decisions for this customization, one entry per scope - /// that has one. This is a wire contract: producers MUST publish entries - /// sorted by descending specificity (Session, Workspace, then Global). - /// The agent host emits at most one Workspace entry, for the session's primary - /// working directory. Consumers MAY treat - /// `enablement[0]` as the decisive decision and - /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - /// absent or empty array means no explicit decision exists, so the - /// customization is enabled by default. - /// - /// Only the agent host publishes this; clients treat it as read-only provenance. - public var enablement: [CustomizationEnablement]? /// Icons for UI display. public var icons: [Icon]? /// Optional span within {@link CustomizationBase.uri | `uri`} when this @@ -4391,9 +4276,32 @@ public struct McpServerCustomization: Codable, Sendable { /// out-of-band. public var meta: [String: AnyCodable]? public var type: CustomizationType - /// Whether this MCP server is effectively enabled after resolving all scopes. - /// {@link CustomizationBase.enablement | `enablement`} records its inputs. - public var enabled: Bool + /// Source URI of the plugin that contributes this server. A plugin-provided + /// server keeps this durable identity while temporarily published top-level; + /// its durable enablement key is derived from this URI. + /// + /// Absent means this is an unowned server, whose durable key is + /// `mcpServers#`. + public var owningPluginUri: String? + /// Explicit enablement decisions for this customization, one entry per scope + /// that has one. This is a wire contract: producers MUST publish entries + /// sorted by descending specificity (Session, Workspace, then Global). + /// The agent host emits at most one Workspace entry, for the session's primary + /// working directory. Consumers MAY treat + /// `enablement[0]` as the decisive decision and + /// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + /// absent or empty array means no explicit decision exists, so the + /// customization is enabled by default. + /// + /// Flows in both directions. A client publishes this alongside a customization + /// to assert its global decision, which is authoritative for the Global scope; + /// a client always includes its global entry, even when enabled. The host + /// publishes the fully resolved set across all scopes, and consumers derive + /// the effective enabled value from that set. + public var enablement: [CustomizationEnablement]? + /// Whether the client explicitly bundled this server and owns its Global + /// enablement decision. + public var isClientBundled: Bool? /// Current lifecycle state of the MCP server. public var state: McpServerState /// An `mcp://`-protocol channel the client uses to side-channel traffic @@ -4420,12 +4328,13 @@ public struct McpServerCustomization: Codable, Sendable { case id case uri case name - case enablement case icons case range case meta = "_meta" case type - case enabled + case owningPluginUri + case enablement + case isClientBundled case state case channel case mcpApp @@ -4435,12 +4344,13 @@ public struct McpServerCustomization: Codable, Sendable { id: String, uri: String, name: String, - enablement: [CustomizationEnablement]? = nil, icons: [Icon]? = nil, range: TextRange? = nil, meta: [String: AnyCodable]? = nil, type: CustomizationType, - enabled: Bool, + owningPluginUri: String? = nil, + enablement: [CustomizationEnablement]? = nil, + isClientBundled: Bool? = nil, state: McpServerState, channel: String? = nil, mcpApp: McpServerCustomizationApps? = nil @@ -4448,12 +4358,13 @@ public struct McpServerCustomization: Codable, Sendable { self.id = id self.uri = uri self.name = name - self.enablement = enablement self.icons = icons self.range = range self.meta = meta self.type = type - self.enabled = enabled + self.owningPluginUri = owningPluginUri + self.enablement = enablement + self.isClientBundled = isClientBundled self.state = state self.channel = channel self.mcpApp = mcpApp diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift index cca688da2..9032fd1e8 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift @@ -230,15 +230,12 @@ func applyCustomizationEnablement(_ c: inout Customization, _ enablement: [Custo let provenance = enablement.isEmpty ? nil : enablement switch c { case .plugin(var p): - p.enabled = enabled p.enablement = provenance c = .plugin(p) case .directory(var d): d.enabled = enabled - d.enablement = provenance c = .directory(d) case .mcpServer(var m): - m.enabled = enabled m.enablement = provenance c = .mcpServer(m) // Unknown/future customization: opaque payload, nothing to mutate. @@ -253,26 +250,20 @@ func applyChildCustomizationEnablement(_ c: inout ChildCustomization, _ enableme switch c { case .agent(var x): x.enabled = enabled - x.enablement = provenance c = .agent(x) case .skill(var x): x.enabled = enabled - x.enablement = provenance c = .skill(x) case .prompt(var x): x.enabled = enabled - x.enablement = provenance c = .prompt(x) case .rule(var x): x.enabled = enabled - x.enablement = provenance c = .rule(x) case .hook(var x): x.enabled = enabled - x.enablement = provenance c = .hook(x) case .mcpServer(var x): - x.enabled = enabled x.enablement = provenance c = .mcpServer(x) // Unknown/future child customization: opaque payload, nothing to mutate. diff --git a/docs/.changes/20260812-client-bundled-customization-enablement.json b/docs/.changes/20260812-client-bundled-customization-enablement.json new file mode 100644 index 000000000..35655ab9e --- /dev/null +++ b/docs/.changes/20260812-client-bundled-customization-enablement.json @@ -0,0 +1,4 @@ +{ + "type": "added", + "message": "`McpServerCustomization` now exposes plugin ownership and client-bundled enablement metadata, and `ClientPluginCustomization` can publish child enablement decisions." +} diff --git a/docs/guide/customizations.md b/docs/guide/customizations.md index c4412829d..17df81dbd 100644 --- a/docs/guide/customizations.md +++ b/docs/guide/customizations.md @@ -57,8 +57,7 @@ PluginCustomization { uri: URI // plugin URL or marketplace id name: string icons?: Icon[] - enabled: boolean - enablement?: CustomizationEnablement[] // host-published explicit decisions + enablement?: CustomizationEnablement[] // explicit scoped decisions clientId?: string // set when published by a client load?: CustomizationLoadState // host-reported parse/load state children?: ChildCustomization[] @@ -71,7 +70,6 @@ DirectoryCustomization { name: string icons?: Icon[] enabled: boolean - enablement?: CustomizationEnablement[] clientId?: string load?: CustomizationLoadState children?: ChildCustomization[] @@ -110,7 +108,7 @@ stateDiagram-v2 ## Children -Every child carries the same base fields (`id`, `uri`, `name`, optional `icons`, optional `enablement`) 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. +Every child carries the same base fields (`id`, `uri`, `name`, optional `icons`). The five leaf children carry an optional `enabled` flag (absent means enabled), while `McpServerCustomization` carries explicit scoped `enablement` decisions. 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): @@ -120,7 +118,7 @@ SkillCustomization { type: 'skill'; description?, disableModelInvoc 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 +McpServerCustomization { type: 'mcpServer'; owningPluginUri?, enablement?, isClientBundled?, 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. @@ -137,7 +135,7 @@ state.customizations ## Enablement -Every customization may carry an `enablement` array of explicit decisions: +Plugins and MCP servers may carry an `enablement` array of explicit decisions: ```typescript CustomizationEnablement = @@ -146,17 +144,27 @@ CustomizationEnablement = | { kind: 'session'; enabled: boolean } ``` -The agent host is the only publisher of this read-only provenance. Producers -MUST publish the entries in descending specificity: session, workspace, then -global. The host emits at most one workspace decision, for the session's -primary working directory. Consumers MAY use `enablement[0]` as the decisive -decision, with `enablement?.[0]?.enabled ?? true` as the effective value. An -absent or empty array has no explicit decision and means enabled by default. - -`enabled` remains the display-ready, effective value. Both containers and -children carry it; a child's final state is -`container.enabled && (child.enabled ?? true)`, so disabling a container still -disables all of its children. +Producers MUST publish the entries in descending specificity: session, +workspace, then global. The host emits at most one workspace decision, for the +session's primary working directory. Consumers MAY use `enablement[0]` as the +decisive decision, with `enablement?.[0]?.enabled ?? true` as the effective +value. An absent or empty array has no explicit decision and means enabled by +default. + +For MCP servers, enablement flows both ways. A client publishing a server +includes its global decision (even when enabled), and the host publishes the +fully resolved decisions across all scopes. Client-published plugins can supply +global decisions for their discovered children through +`ClientPluginCustomization.childEnablement`, keyed by child name. The host +applies these under each child's durable key. A plugin-provided MCP server +retains `owningPluginUri` while temporarily surfaced at the top level; an +unowned server uses `mcpServers#` as its durable key. + +`DirectoryCustomization` and the five leaf child kinds retain their plain +`enabled` fields. A plugin's effective value is derived from its `enablement`; +a plugin child is effectively enabled when that derived value and +`(child.enabled ?? true)` are both true. A directory child instead uses +`directory.enabled && (child.enabled ?? true)`. ## Toggling @@ -176,10 +184,12 @@ The action replaces the entry's array wholesale rather than merging a single scope. A caller changing one scope must include every decision it intends to preserve. 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, replaces the matched entry's `enablement`, -and recomputes its `enabled` from the first decision. An empty array clears -the explicit provenance and restores the default enabled value. The action is -a no-op if no customization has that id. +children inside every container. For plugins and MCP servers it replaces the +matched entry's `enablement`; for directories and leaf children it updates the +plain `enabled` value from the first decision. An empty array clears explicit +provenance from plugins and MCP servers and restores the default enabled value +for directories and leaf children. The action is a no-op if no customization +has that id. ```mermaid sequenceDiagram @@ -229,7 +239,7 @@ dispatch({ id: 'client-plugin-1', uri: 'virtual://my-client/workspace-skills', name: 'Workspace Skills', - enabled: true, + enablement: [{ kind: 'global', enabled: true }], nonce: 'sha256:...', }, ], @@ -457,7 +467,7 @@ sequenceDiagram Note over Client,Server: 3. Client disables Plugin A - Client->>Server: customizationToggled (id: plugin-a, enabled: false) + Client->>Server: customizationToggled (id: plugin-a, enablement: [session: false]) Server->>Client: action echoed Note over Client,Server: 4. Active client disconnects diff --git a/docs/guide/mcp.md b/docs/guide/mcp.md index 50be30de5..1e4cbd187 100644 --- a/docs/guide/mcp.md +++ b/docs/guide/mcp.md @@ -2,7 +2,7 @@ [Model Context Protocol](https://modelcontextprotocol.io/) servers are surfaced in AHP as a [`McpServerCustomization`](/reference/session#mcpservercustomization) — a customization that represents one running (or registered) MCP server within a session. AHP intentionally does **not** re-spec MCP. It exposes: -- Enough state for clients to render the server (name, icon, enabled flag, runtime status). +- Enough state for clients to render the server (name, icon, scoped enablement, runtime status). - Enough state for clients to drive authentication when the server demands it. - An optional [`mcp://` side-channel](/specification/mcp-channel) the client can use to talk to the upstream server when it needs to render an [MCP App](#mcp-apps). @@ -33,14 +33,19 @@ McpServerCustomization { name: string icons?: Icon[] range?: TextRange // span inside `uri` for inline declarations - enabled: boolean // user-toggleable (see Customizations guide) + owningPluginUri?: URI // durable identity for a plugin-provided server + enablement?: CustomizationEnablement[] // user-toggleable (see Customizations guide) + isClientBundled?: boolean // client owns the Global enablement decision state: McpServerState // discriminated union — see below channel?: URI // optional mcp:// side-channel mcpApp?: McpServerCustomizationApps } ``` -`enabled` follows the same model as any other container — it's toggled with `session/customizationToggled`. Disabling a server signals the host to stop it; the host then transitions the runtime through `stopped` and removes it from the session (or leaves it as `stopped` until removal, host's choice). +`enablement` is toggled with `session/customizationToggled`; its first entry +is the effective decision. Disabling a server signals the host to stop it; the +host then transitions the runtime through `stopped` and removes it from the +session (or leaves it as `stopped` until removal, host's choice). Clients can also ask the host to manage the server process without changing the customization's enabled intent: @@ -49,7 +54,7 @@ Clients can also ask the host to manage the server process without changing the ## Runtime status -`state` is a [discriminated union on `kind`](/reference/session#mcpserverstatus). It is the host's view of the server's lifecycle, separate from `enabled` (which is the user's intent). +`state` is a [discriminated union on `kind`](/reference/session#mcpserverstatus). It is the host's view of the server's lifecycle, separate from `enablement` (which records the user's scoped intent). ```mermaid stateDiagram-v2 diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 67725d087..fb0e43b4f 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -495,7 +495,7 @@ }, "SessionCustomizationToggledAction": { "type": "object", - "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`.\n\nThe `enablement` array completely replaces all explicit decisions. A caller\nchanging one scope must include every decision it intends to preserve.", + "description": "A client updated a customization's enablement decisions.\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). Plugins\nand MCP servers retain the matched entry's explicit decisions; other\nentries update their `enabled` flag. Disabling a plugin still disables all\nof its children — the effective state of a plugin child is the plugin's\nderived enabled value and `(child.enabled ?? true)` — so toggling a child\nonly matters while its plugin is enabled. Is a no-op when no\ncustomization has the given `id`.\n\nThe `enablement` array completely replaces all explicit decisions. A caller\nchanging one scope must include every decision it intends to preserve.", "properties": { "type": { "const": "session/customizationToggled" @@ -509,7 +509,7 @@ "items": { "$ref": "#/$defs/CustomizationEnablement" }, - "description": "The complete set of explicit decisions, replacing any existing set." + "description": "Explicit enablement decisions, replacing the previous list entirely." } }, "required": [ @@ -3538,13 +3538,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -3642,13 +3635,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -3665,10 +3651,6 @@ "additionalProperties": {}, "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." - }, "clientId": { "type": "string", "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." @@ -3688,8 +3670,7 @@ "required": [ "id", "uri", - "name", - "enabled" + "name" ] }, "PluginCustomization": { @@ -3708,13 +3689,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -3731,10 +3705,6 @@ "additionalProperties": {}, "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." - }, "clientId": { "type": "string", "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." @@ -3753,6 +3723,13 @@ "type": { "const": "plugin" }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions. See {@link McpServerCustomization.enablement}." + }, "version": { "type": "string", "description": "Version of the plugin, sourced from the\n[Open Plugins](https://open-plugins.com/) manifest's optional\n`version` field (semver, e.g. `\"1.2.0\"`). Absent when the manifest\ndeclares no version — the field is optional there — or the source\nhas no version concept. Provenance / display only: the host neither\nparses nor enforces it." @@ -3762,7 +3739,6 @@ "id", "uri", "name", - "enabled", "type" ] }, @@ -3782,13 +3758,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -3805,10 +3774,6 @@ "additionalProperties": {}, "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." - }, "clientId": { "type": "string", "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." @@ -3827,6 +3792,13 @@ "type": { "const": "plugin" }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions. See {@link McpServerCustomization.enablement}." + }, "version": { "type": "string", "description": "Version of the plugin, sourced from the\n[Open Plugins](https://open-plugins.com/) manifest's optional\n`version` field (semver, e.g. `\"1.2.0\"`). Absent when the manifest\ndeclares no version — the field is optional there — or the source\nhas no version concept. Provenance / display only: the host neither\nparses nor enforces it." @@ -3834,13 +3806,22 @@ "nonce": { "type": "string", "description": "Opaque version token used by the host to detect changes." + }, + "childEnablement": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + } + }, + "description": "Explicit enablement decisions for children this plugin contributes,\nkeyed by child name (for MCP servers, the server name as it appears in\nthe bundled `.mcp.json`).\n\nBundled children are discovered by the host rather than published by the\nclient, so the client cannot attach `enablement` to them directly. This\ncarries the client's global decision for each one; the host applies it\nunder the child's durable key." } }, "required": [ "id", "uri", "name", - "enabled", "type" ] }, @@ -3860,13 +3841,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -3883,10 +3857,6 @@ "additionalProperties": {}, "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." - }, "clientId": { "type": "string", "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." @@ -3905,6 +3875,10 @@ "type": { "const": "directory" }, + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." + }, "contents": { "$ref": "#/$defs/ChildCustomizationType", "description": "Which child customization type this directory holds." @@ -3918,15 +3892,15 @@ "id", "uri", "name", - "enabled", "type", + "enabled", "contents", "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.", + "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 because it can appear as a top-level customization too.", "properties": { "id": { "type": "string", @@ -3940,13 +3914,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -3965,7 +3932,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." } }, "required": [ @@ -3990,13 +3957,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -4015,7 +3975,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "agent" @@ -4067,13 +4027,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -4092,7 +4045,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "skill" @@ -4133,13 +4086,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -4158,7 +4104,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "prompt" @@ -4191,13 +4137,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -4216,7 +4155,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "rule" @@ -4260,13 +4199,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -4285,7 +4217,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "hook" @@ -4314,13 +4246,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -4340,9 +4265,20 @@ "type": { "const": "mcpServer" }, - "enabled": { + "owningPluginUri": { + "$ref": "#/$defs/URI", + "description": "Source URI of the plugin that contributes this server. A plugin-provided\nserver keeps this durable identity while temporarily published top-level;\nits durable enablement key is derived from this URI.\n\nAbsent means this is an unowned server, whose durable key is\n`mcpServers#`." + }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nFlows in both directions. A client publishes this alongside a customization\nto assert its global decision, which is authoritative for the Global scope;\na client always includes its global entry, even when enabled. The host\npublishes the fully resolved set across all scopes, and consumers derive\nthe effective enabled value from that set." + }, + "isClientBundled": { "type": "boolean", - "description": "Whether this MCP server is effectively enabled after resolving all scopes.\n{@link CustomizationBase.enablement | `enablement`} records its inputs." + "description": "Whether the client explicitly bundled this server and owns its Global\nenablement decision." }, "state": { "$ref": "#/$defs/McpServerState", @@ -4362,7 +4298,6 @@ "uri", "name", "type", - "enabled", "state" ] }, diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 1ab5db2ae..31060e177 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -2844,13 +2844,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -2948,13 +2941,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -2971,10 +2957,6 @@ "additionalProperties": {}, "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." - }, "clientId": { "type": "string", "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." @@ -2994,8 +2976,7 @@ "required": [ "id", "uri", - "name", - "enabled" + "name" ] }, "PluginCustomization": { @@ -3014,13 +2995,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -3037,10 +3011,6 @@ "additionalProperties": {}, "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." - }, "clientId": { "type": "string", "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." @@ -3059,6 +3029,13 @@ "type": { "const": "plugin" }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions. See {@link McpServerCustomization.enablement}." + }, "version": { "type": "string", "description": "Version of the plugin, sourced from the\n[Open Plugins](https://open-plugins.com/) manifest's optional\n`version` field (semver, e.g. `\"1.2.0\"`). Absent when the manifest\ndeclares no version — the field is optional there — or the source\nhas no version concept. Provenance / display only: the host neither\nparses nor enforces it." @@ -3068,7 +3045,6 @@ "id", "uri", "name", - "enabled", "type" ] }, @@ -3088,13 +3064,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -3111,10 +3080,6 @@ "additionalProperties": {}, "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." - }, "clientId": { "type": "string", "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." @@ -3133,6 +3098,13 @@ "type": { "const": "plugin" }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions. See {@link McpServerCustomization.enablement}." + }, "version": { "type": "string", "description": "Version of the plugin, sourced from the\n[Open Plugins](https://open-plugins.com/) manifest's optional\n`version` field (semver, e.g. `\"1.2.0\"`). Absent when the manifest\ndeclares no version — the field is optional there — or the source\nhas no version concept. Provenance / display only: the host neither\nparses nor enforces it." @@ -3140,13 +3112,22 @@ "nonce": { "type": "string", "description": "Opaque version token used by the host to detect changes." + }, + "childEnablement": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + } + }, + "description": "Explicit enablement decisions for children this plugin contributes,\nkeyed by child name (for MCP servers, the server name as it appears in\nthe bundled `.mcp.json`).\n\nBundled children are discovered by the host rather than published by the\nclient, so the client cannot attach `enablement` to them directly. This\ncarries the client's global decision for each one; the host applies it\nunder the child's durable key." } }, "required": [ "id", "uri", "name", - "enabled", "type" ] }, @@ -3166,13 +3147,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -3189,10 +3163,6 @@ "additionalProperties": {}, "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." - }, "clientId": { "type": "string", "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." @@ -3211,6 +3181,10 @@ "type": { "const": "directory" }, + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." + }, "contents": { "$ref": "#/$defs/ChildCustomizationType", "description": "Which child customization type this directory holds." @@ -3224,15 +3198,15 @@ "id", "uri", "name", - "enabled", "type", + "enabled", "contents", "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.", + "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 because it can appear as a top-level customization too.", "properties": { "id": { "type": "string", @@ -3246,13 +3220,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -3271,7 +3238,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." } }, "required": [ @@ -3296,13 +3263,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -3321,7 +3281,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "agent" @@ -3373,13 +3333,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -3398,7 +3351,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "skill" @@ -3439,13 +3392,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -3464,7 +3410,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "prompt" @@ -3497,13 +3443,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -3522,7 +3461,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "rule" @@ -3566,13 +3505,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -3591,7 +3523,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "hook" @@ -3620,13 +3552,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -3646,9 +3571,20 @@ "type": { "const": "mcpServer" }, - "enabled": { + "owningPluginUri": { + "$ref": "#/$defs/URI", + "description": "Source URI of the plugin that contributes this server. A plugin-provided\nserver keeps this durable identity while temporarily published top-level;\nits durable enablement key is derived from this URI.\n\nAbsent means this is an unowned server, whose durable key is\n`mcpServers#`." + }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nFlows in both directions. A client publishes this alongside a customization\nto assert its global decision, which is authoritative for the Global scope;\na client always includes its global entry, even when enabled. The host\npublishes the fully resolved set across all scopes, and consumers derive\nthe effective enabled value from that set." + }, + "isClientBundled": { "type": "boolean", - "description": "Whether this MCP server is effectively enabled after resolving all scopes.\n{@link CustomizationBase.enablement | `enablement`} records its inputs." + "description": "Whether the client explicitly bundled this server and owns its Global\nenablement decision." }, "state": { "$ref": "#/$defs/McpServerState", @@ -3668,7 +3604,6 @@ "uri", "name", "type", - "enabled", "state" ] }, @@ -6957,7 +6892,7 @@ }, "SessionCustomizationToggledAction": { "type": "object", - "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`.\n\nThe `enablement` array completely replaces all explicit decisions. A caller\nchanging one scope must include every decision it intends to preserve.", + "description": "A client updated a customization's enablement decisions.\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). Plugins\nand MCP servers retain the matched entry's explicit decisions; other\nentries update their `enabled` flag. Disabling a plugin still disables all\nof its children — the effective state of a plugin child is the plugin's\nderived enabled value and `(child.enabled ?? true)` — so toggling a child\nonly matters while its plugin is enabled. Is a no-op when no\ncustomization has the given `id`.\n\nThe `enablement` array completely replaces all explicit decisions. A caller\nchanging one scope must include every decision it intends to preserve.", "properties": { "type": { "const": "session/customizationToggled" @@ -6971,7 +6906,7 @@ "items": { "$ref": "#/$defs/CustomizationEnablement" }, - "description": "The complete set of explicit decisions, replacing any existing set." + "description": "Explicit enablement decisions, replacing the previous list entirely." } }, "required": [ @@ -9078,6 +9013,46 @@ ], "description": "Discriminated union of all tool call lifecycle states.\n\nSee the [state model guide](/guide/state-model.html#tool-call-lifecycle)\nfor the full state machine diagram." }, + "CustomizationLoadState": { + "oneOf": [ + { + "$ref": "#/$defs/CustomizationLoadingState" + }, + { + "$ref": "#/$defs/CustomizationLoadedState" + }, + { + "$ref": "#/$defs/CustomizationDegradedState" + }, + { + "$ref": "#/$defs/CustomizationErrorState" + } + ], + "description": "Discriminated load state for a container customization\n({@link PluginCustomization} or {@link DirectoryCustomization})." + }, + "ChildCustomization": { + "oneOf": [ + { + "$ref": "#/$defs/AgentCustomization" + }, + { + "$ref": "#/$defs/SkillCustomization" + }, + { + "$ref": "#/$defs/PromptCustomization" + }, + { + "$ref": "#/$defs/RuleCustomization" + }, + { + "$ref": "#/$defs/HookCustomization" + }, + { + "$ref": "#/$defs/McpServerCustomization" + } + ], + "description": "Child customizations that live inside a {@link PluginCustomization} or\n{@link DirectoryCustomization}." + }, "CustomizationEnablement": { "oneOf": [ { @@ -9132,46 +9107,6 @@ ], "description": "A single explicit enablement decision." }, - "CustomizationLoadState": { - "oneOf": [ - { - "$ref": "#/$defs/CustomizationLoadingState" - }, - { - "$ref": "#/$defs/CustomizationLoadedState" - }, - { - "$ref": "#/$defs/CustomizationDegradedState" - }, - { - "$ref": "#/$defs/CustomizationErrorState" - } - ], - "description": "Discriminated load state for a container customization\n({@link PluginCustomization} or {@link DirectoryCustomization})." - }, - "ChildCustomization": { - "oneOf": [ - { - "$ref": "#/$defs/AgentCustomization" - }, - { - "$ref": "#/$defs/SkillCustomization" - }, - { - "$ref": "#/$defs/PromptCustomization" - }, - { - "$ref": "#/$defs/RuleCustomization" - }, - { - "$ref": "#/$defs/HookCustomization" - }, - { - "$ref": "#/$defs/McpServerCustomization" - } - ], - "description": "Child customizations that live inside a {@link PluginCustomization} or\n{@link DirectoryCustomization}." - }, "ChildCustomizationType": { "oneOf": [ { diff --git a/schema/errors.schema.json b/schema/errors.schema.json index a4c92125f..bf5c1e3d8 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -1440,13 +1440,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -1544,13 +1537,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -1567,10 +1553,6 @@ "additionalProperties": {}, "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." - }, "clientId": { "type": "string", "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." @@ -1590,8 +1572,7 @@ "required": [ "id", "uri", - "name", - "enabled" + "name" ] }, "PluginCustomization": { @@ -1610,13 +1591,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -1633,10 +1607,6 @@ "additionalProperties": {}, "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." - }, "clientId": { "type": "string", "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." @@ -1655,6 +1625,13 @@ "type": { "const": "plugin" }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions. See {@link McpServerCustomization.enablement}." + }, "version": { "type": "string", "description": "Version of the plugin, sourced from the\n[Open Plugins](https://open-plugins.com/) manifest's optional\n`version` field (semver, e.g. `\"1.2.0\"`). Absent when the manifest\ndeclares no version — the field is optional there — or the source\nhas no version concept. Provenance / display only: the host neither\nparses nor enforces it." @@ -1664,7 +1641,6 @@ "id", "uri", "name", - "enabled", "type" ] }, @@ -1684,13 +1660,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -1707,10 +1676,6 @@ "additionalProperties": {}, "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." - }, "clientId": { "type": "string", "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." @@ -1729,6 +1694,13 @@ "type": { "const": "plugin" }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions. See {@link McpServerCustomization.enablement}." + }, "version": { "type": "string", "description": "Version of the plugin, sourced from the\n[Open Plugins](https://open-plugins.com/) manifest's optional\n`version` field (semver, e.g. `\"1.2.0\"`). Absent when the manifest\ndeclares no version — the field is optional there — or the source\nhas no version concept. Provenance / display only: the host neither\nparses nor enforces it." @@ -1736,13 +1708,22 @@ "nonce": { "type": "string", "description": "Opaque version token used by the host to detect changes." + }, + "childEnablement": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + } + }, + "description": "Explicit enablement decisions for children this plugin contributes,\nkeyed by child name (for MCP servers, the server name as it appears in\nthe bundled `.mcp.json`).\n\nBundled children are discovered by the host rather than published by the\nclient, so the client cannot attach `enablement` to them directly. This\ncarries the client's global decision for each one; the host applies it\nunder the child's durable key." } }, "required": [ "id", "uri", "name", - "enabled", "type" ] }, @@ -1762,13 +1743,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -1785,10 +1759,6 @@ "additionalProperties": {}, "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." - }, "clientId": { "type": "string", "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." @@ -1807,6 +1777,10 @@ "type": { "const": "directory" }, + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." + }, "contents": { "$ref": "#/$defs/ChildCustomizationType", "description": "Which child customization type this directory holds." @@ -1820,15 +1794,15 @@ "id", "uri", "name", - "enabled", "type", + "enabled", "contents", "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.", + "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 because it can appear as a top-level customization too.", "properties": { "id": { "type": "string", @@ -1842,13 +1816,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -1867,7 +1834,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." } }, "required": [ @@ -1892,13 +1859,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -1917,7 +1877,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "agent" @@ -1969,13 +1929,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -1994,7 +1947,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "skill" @@ -2035,13 +1988,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -2060,7 +2006,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "prompt" @@ -2093,13 +2039,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -2118,7 +2057,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "rule" @@ -2162,13 +2101,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -2187,7 +2119,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "hook" @@ -2216,13 +2148,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -2242,9 +2167,20 @@ "type": { "const": "mcpServer" }, - "enabled": { + "owningPluginUri": { + "$ref": "#/$defs/URI", + "description": "Source URI of the plugin that contributes this server. A plugin-provided\nserver keeps this durable identity while temporarily published top-level;\nits durable enablement key is derived from this URI.\n\nAbsent means this is an unowned server, whose durable key is\n`mcpServers#`." + }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nFlows in both directions. A client publishes this alongside a customization\nto assert its global decision, which is authoritative for the Global scope;\na client always includes its global entry, even when enabled. The host\npublishes the fully resolved set across all scopes, and consumers derive\nthe effective enabled value from that set." + }, + "isClientBundled": { "type": "boolean", - "description": "Whether this MCP server is effectively enabled after resolving all scopes.\n{@link CustomizationBase.enablement | `enablement`} records its inputs." + "description": "Whether the client explicitly bundled this server and owns its Global\nenablement decision." }, "state": { "$ref": "#/$defs/McpServerState", @@ -2264,7 +2200,6 @@ "uri", "name", "type", - "enabled", "state" ] }, @@ -6675,6 +6610,46 @@ ], "description": "Discriminated union of all tool call lifecycle states.\n\nSee the [state model guide](/guide/state-model.html#tool-call-lifecycle)\nfor the full state machine diagram." }, + "CustomizationLoadState": { + "oneOf": [ + { + "$ref": "#/$defs/CustomizationLoadingState" + }, + { + "$ref": "#/$defs/CustomizationLoadedState" + }, + { + "$ref": "#/$defs/CustomizationDegradedState" + }, + { + "$ref": "#/$defs/CustomizationErrorState" + } + ], + "description": "Discriminated load state for a container customization\n({@link PluginCustomization} or {@link DirectoryCustomization})." + }, + "ChildCustomization": { + "oneOf": [ + { + "$ref": "#/$defs/AgentCustomization" + }, + { + "$ref": "#/$defs/SkillCustomization" + }, + { + "$ref": "#/$defs/PromptCustomization" + }, + { + "$ref": "#/$defs/RuleCustomization" + }, + { + "$ref": "#/$defs/HookCustomization" + }, + { + "$ref": "#/$defs/McpServerCustomization" + } + ], + "description": "Child customizations that live inside a {@link PluginCustomization} or\n{@link DirectoryCustomization}." + }, "CustomizationEnablement": { "oneOf": [ { @@ -6729,46 +6704,6 @@ ], "description": "A single explicit enablement decision." }, - "CustomizationLoadState": { - "oneOf": [ - { - "$ref": "#/$defs/CustomizationLoadingState" - }, - { - "$ref": "#/$defs/CustomizationLoadedState" - }, - { - "$ref": "#/$defs/CustomizationDegradedState" - }, - { - "$ref": "#/$defs/CustomizationErrorState" - } - ], - "description": "Discriminated load state for a container customization\n({@link PluginCustomization} or {@link DirectoryCustomization})." - }, - "ChildCustomization": { - "oneOf": [ - { - "$ref": "#/$defs/AgentCustomization" - }, - { - "$ref": "#/$defs/SkillCustomization" - }, - { - "$ref": "#/$defs/PromptCustomization" - }, - { - "$ref": "#/$defs/RuleCustomization" - }, - { - "$ref": "#/$defs/HookCustomization" - }, - { - "$ref": "#/$defs/McpServerCustomization" - } - ], - "description": "Child customizations that live inside a {@link PluginCustomization} or\n{@link DirectoryCustomization}." - }, "ChildCustomizationType": { "oneOf": [ { @@ -7969,7 +7904,7 @@ }, "SessionCustomizationToggledAction": { "type": "object", - "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`.\n\nThe `enablement` array completely replaces all explicit decisions. A caller\nchanging one scope must include every decision it intends to preserve.", + "description": "A client updated a customization's enablement decisions.\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). Plugins\nand MCP servers retain the matched entry's explicit decisions; other\nentries update their `enabled` flag. Disabling a plugin still disables all\nof its children — the effective state of a plugin child is the plugin's\nderived enabled value and `(child.enabled ?? true)` — so toggling a child\nonly matters while its plugin is enabled. Is a no-op when no\ncustomization has the given `id`.\n\nThe `enablement` array completely replaces all explicit decisions. A caller\nchanging one scope must include every decision it intends to preserve.", "properties": { "type": { "const": "session/customizationToggled" @@ -7983,7 +7918,7 @@ "items": { "$ref": "#/$defs/CustomizationEnablement" }, - "description": "The complete set of explicit decisions, replacing any existing set." + "description": "Explicit enablement decisions, replacing the previous list entirely." } }, "required": [ diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 3570c4d83..07eb29ec0 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -1603,13 +1603,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -1707,13 +1700,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -1730,10 +1716,6 @@ "additionalProperties": {}, "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." - }, "clientId": { "type": "string", "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." @@ -1753,8 +1735,7 @@ "required": [ "id", "uri", - "name", - "enabled" + "name" ] }, "PluginCustomization": { @@ -1773,13 +1754,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -1796,10 +1770,6 @@ "additionalProperties": {}, "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." - }, "clientId": { "type": "string", "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." @@ -1818,6 +1788,13 @@ "type": { "const": "plugin" }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions. See {@link McpServerCustomization.enablement}." + }, "version": { "type": "string", "description": "Version of the plugin, sourced from the\n[Open Plugins](https://open-plugins.com/) manifest's optional\n`version` field (semver, e.g. `\"1.2.0\"`). Absent when the manifest\ndeclares no version — the field is optional there — or the source\nhas no version concept. Provenance / display only: the host neither\nparses nor enforces it." @@ -1827,7 +1804,6 @@ "id", "uri", "name", - "enabled", "type" ] }, @@ -1847,13 +1823,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -1870,10 +1839,6 @@ "additionalProperties": {}, "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." - }, "clientId": { "type": "string", "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." @@ -1892,6 +1857,13 @@ "type": { "const": "plugin" }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions. See {@link McpServerCustomization.enablement}." + }, "version": { "type": "string", "description": "Version of the plugin, sourced from the\n[Open Plugins](https://open-plugins.com/) manifest's optional\n`version` field (semver, e.g. `\"1.2.0\"`). Absent when the manifest\ndeclares no version — the field is optional there — or the source\nhas no version concept. Provenance / display only: the host neither\nparses nor enforces it." @@ -1899,13 +1871,22 @@ "nonce": { "type": "string", "description": "Opaque version token used by the host to detect changes." + }, + "childEnablement": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + } + }, + "description": "Explicit enablement decisions for children this plugin contributes,\nkeyed by child name (for MCP servers, the server name as it appears in\nthe bundled `.mcp.json`).\n\nBundled children are discovered by the host rather than published by the\nclient, so the client cannot attach `enablement` to them directly. This\ncarries the client's global decision for each one; the host applies it\nunder the child's durable key." } }, "required": [ "id", "uri", "name", - "enabled", "type" ] }, @@ -1925,13 +1906,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -1948,10 +1922,6 @@ "additionalProperties": {}, "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." - }, "clientId": { "type": "string", "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." @@ -1970,6 +1940,10 @@ "type": { "const": "directory" }, + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." + }, "contents": { "$ref": "#/$defs/ChildCustomizationType", "description": "Which child customization type this directory holds." @@ -1983,15 +1957,15 @@ "id", "uri", "name", - "enabled", "type", + "enabled", "contents", "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.", + "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 because it can appear as a top-level customization too.", "properties": { "id": { "type": "string", @@ -2005,13 +1979,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -2030,7 +1997,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." } }, "required": [ @@ -2055,13 +2022,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -2080,7 +2040,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "agent" @@ -2132,13 +2092,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -2157,7 +2110,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "skill" @@ -2198,13 +2151,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -2223,7 +2169,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "prompt" @@ -2256,13 +2202,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -2281,7 +2220,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "rule" @@ -2325,13 +2264,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -2350,7 +2282,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "hook" @@ -2379,13 +2311,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -2405,9 +2330,20 @@ "type": { "const": "mcpServer" }, - "enabled": { + "owningPluginUri": { + "$ref": "#/$defs/URI", + "description": "Source URI of the plugin that contributes this server. A plugin-provided\nserver keeps this durable identity while temporarily published top-level;\nits durable enablement key is derived from this URI.\n\nAbsent means this is an unowned server, whose durable key is\n`mcpServers#`." + }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nFlows in both directions. A client publishes this alongside a customization\nto assert its global decision, which is authoritative for the Global scope;\na client always includes its global entry, even when enabled. The host\npublishes the fully resolved set across all scopes, and consumers derive\nthe effective enabled value from that set." + }, + "isClientBundled": { "type": "boolean", - "description": "Whether this MCP server is effectively enabled after resolving all scopes.\n{@link CustomizationBase.enablement | `enablement`} records its inputs." + "description": "Whether the client explicitly bundled this server and owns its Global\nenablement decision." }, "state": { "$ref": "#/$defs/McpServerState", @@ -2427,7 +2363,6 @@ "uri", "name", "type", - "enabled", "state" ] }, @@ -5353,6 +5288,46 @@ ], "description": "Discriminated union of all tool call lifecycle states.\n\nSee the [state model guide](/guide/state-model.html#tool-call-lifecycle)\nfor the full state machine diagram." }, + "CustomizationLoadState": { + "oneOf": [ + { + "$ref": "#/$defs/CustomizationLoadingState" + }, + { + "$ref": "#/$defs/CustomizationLoadedState" + }, + { + "$ref": "#/$defs/CustomizationDegradedState" + }, + { + "$ref": "#/$defs/CustomizationErrorState" + } + ], + "description": "Discriminated load state for a container customization\n({@link PluginCustomization} or {@link DirectoryCustomization})." + }, + "ChildCustomization": { + "oneOf": [ + { + "$ref": "#/$defs/AgentCustomization" + }, + { + "$ref": "#/$defs/SkillCustomization" + }, + { + "$ref": "#/$defs/PromptCustomization" + }, + { + "$ref": "#/$defs/RuleCustomization" + }, + { + "$ref": "#/$defs/HookCustomization" + }, + { + "$ref": "#/$defs/McpServerCustomization" + } + ], + "description": "Child customizations that live inside a {@link PluginCustomization} or\n{@link DirectoryCustomization}." + }, "CustomizationEnablement": { "oneOf": [ { @@ -5407,46 +5382,6 @@ ], "description": "A single explicit enablement decision." }, - "CustomizationLoadState": { - "oneOf": [ - { - "$ref": "#/$defs/CustomizationLoadingState" - }, - { - "$ref": "#/$defs/CustomizationLoadedState" - }, - { - "$ref": "#/$defs/CustomizationDegradedState" - }, - { - "$ref": "#/$defs/CustomizationErrorState" - } - ], - "description": "Discriminated load state for a container customization\n({@link PluginCustomization} or {@link DirectoryCustomization})." - }, - "ChildCustomization": { - "oneOf": [ - { - "$ref": "#/$defs/AgentCustomization" - }, - { - "$ref": "#/$defs/SkillCustomization" - }, - { - "$ref": "#/$defs/PromptCustomization" - }, - { - "$ref": "#/$defs/RuleCustomization" - }, - { - "$ref": "#/$defs/HookCustomization" - }, - { - "$ref": "#/$defs/McpServerCustomization" - } - ], - "description": "Child customizations that live inside a {@link PluginCustomization} or\n{@link DirectoryCustomization}." - }, "ChildCustomizationType": { "oneOf": [ { diff --git a/schema/state.schema.json b/schema/state.schema.json index 077f98701..21d0bae6b 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -1351,13 +1351,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -1455,13 +1448,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -1478,10 +1464,6 @@ "additionalProperties": {}, "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." - }, "clientId": { "type": "string", "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." @@ -1501,8 +1483,7 @@ "required": [ "id", "uri", - "name", - "enabled" + "name" ] }, "PluginCustomization": { @@ -1521,13 +1502,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -1544,10 +1518,6 @@ "additionalProperties": {}, "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." - }, "clientId": { "type": "string", "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." @@ -1566,6 +1536,13 @@ "type": { "const": "plugin" }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions. See {@link McpServerCustomization.enablement}." + }, "version": { "type": "string", "description": "Version of the plugin, sourced from the\n[Open Plugins](https://open-plugins.com/) manifest's optional\n`version` field (semver, e.g. `\"1.2.0\"`). Absent when the manifest\ndeclares no version — the field is optional there — or the source\nhas no version concept. Provenance / display only: the host neither\nparses nor enforces it." @@ -1575,7 +1552,6 @@ "id", "uri", "name", - "enabled", "type" ] }, @@ -1595,13 +1571,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -1618,10 +1587,6 @@ "additionalProperties": {}, "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." - }, "clientId": { "type": "string", "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." @@ -1640,6 +1605,13 @@ "type": { "const": "plugin" }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions. See {@link McpServerCustomization.enablement}." + }, "version": { "type": "string", "description": "Version of the plugin, sourced from the\n[Open Plugins](https://open-plugins.com/) manifest's optional\n`version` field (semver, e.g. `\"1.2.0\"`). Absent when the manifest\ndeclares no version — the field is optional there — or the source\nhas no version concept. Provenance / display only: the host neither\nparses nor enforces it." @@ -1647,13 +1619,22 @@ "nonce": { "type": "string", "description": "Opaque version token used by the host to detect changes." + }, + "childEnablement": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + } + }, + "description": "Explicit enablement decisions for children this plugin contributes,\nkeyed by child name (for MCP servers, the server name as it appears in\nthe bundled `.mcp.json`).\n\nBundled children are discovered by the host rather than published by the\nclient, so the client cannot attach `enablement` to them directly. This\ncarries the client's global decision for each one; the host applies it\nunder the child's durable key." } }, "required": [ "id", "uri", "name", - "enabled", "type" ] }, @@ -1673,13 +1654,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -1696,10 +1670,6 @@ "additionalProperties": {}, "description": "Additional provider-specific metadata for this customization.\n\nMirrors the MCP `_meta` convention. Optional and opaque to the\nprotocol; producers and consumers agree on its contents\nout-of-band." }, - "enabled": { - "type": "boolean", - "description": "Whether this container is currently enabled." - }, "clientId": { "type": "string", "description": "`clientId` of the client that contributed this container. Absent for\nserver-originated entries." @@ -1718,6 +1688,10 @@ "type": { "const": "directory" }, + "enabled": { + "type": "boolean", + "description": "Whether this container is currently enabled." + }, "contents": { "$ref": "#/$defs/ChildCustomizationType", "description": "Which child customization type this directory holds." @@ -1731,15 +1705,15 @@ "id", "uri", "name", - "enabled", "type", + "enabled", "contents", "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.", + "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 because it can appear as a top-level customization too.", "properties": { "id": { "type": "string", @@ -1753,13 +1727,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -1778,7 +1745,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." } }, "required": [ @@ -1803,13 +1770,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -1828,7 +1788,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "agent" @@ -1880,13 +1840,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -1905,7 +1858,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "skill" @@ -1946,13 +1899,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -1971,7 +1917,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "prompt" @@ -2004,13 +1950,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -2029,7 +1968,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "rule" @@ -2073,13 +2012,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -2098,7 +2030,7 @@ }, "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`}." + "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 plugin child is the plugin's derived enabled value and\n`(child.enabled ?? true)`, so a disabled plugin disables every child\nregardless of each child's own flag. A directory child instead uses the\ndirectory's `enabled` value and its own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." }, "type": { "const": "hook" @@ -2127,13 +2059,6 @@ "type": "string", "description": "Human-readable name." }, - "enablement": { - "type": "array", - "items": { - "$ref": "#/$defs/CustomizationEnablement" - }, - "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nOnly the agent host publishes this; clients treat it as read-only provenance." - }, "icons": { "type": "array", "items": { @@ -2153,9 +2078,20 @@ "type": { "const": "mcpServer" }, - "enabled": { + "owningPluginUri": { + "$ref": "#/$defs/URI", + "description": "Source URI of the plugin that contributes this server. A plugin-provided\nserver keeps this durable identity while temporarily published top-level;\nits durable enablement key is derived from this URI.\n\nAbsent means this is an unowned server, whose durable key is\n`mcpServers#`." + }, + "enablement": { + "type": "array", + "items": { + "$ref": "#/$defs/CustomizationEnablement" + }, + "description": "Explicit enablement decisions for this customization, one entry per scope\nthat has one. This is a wire contract: producers MUST publish entries\nsorted by descending specificity (Session, Workspace, then Global).\nThe agent host emits at most one Workspace entry, for the session's primary\nworking directory. Consumers MAY treat\n`enablement[0]` as the decisive decision and\n`enablement?.[0]?.enabled ?? true` as the effective enabled value. An\nabsent or empty array means no explicit decision exists, so the\ncustomization is enabled by default.\n\nFlows in both directions. A client publishes this alongside a customization\nto assert its global decision, which is authoritative for the Global scope;\na client always includes its global entry, even when enabled. The host\npublishes the fully resolved set across all scopes, and consumers derive\nthe effective enabled value from that set." + }, + "isClientBundled": { "type": "boolean", - "description": "Whether this MCP server is effectively enabled after resolving all scopes.\n{@link CustomizationBase.enablement | `enablement`} records its inputs." + "description": "Whether the client explicitly bundled this server and owns its Global\nenablement decision." }, "state": { "$ref": "#/$defs/McpServerState", @@ -2175,7 +2111,6 @@ "uri", "name", "type", - "enabled", "state" ] }, diff --git a/types/channels-session/actions.ts b/types/channels-session/actions.ts index 714070c19..97a7c33d5 100644 --- a/types/channels-session/actions.ts +++ b/types/channels-session/actions.ts @@ -360,15 +360,16 @@ export interface SessionCustomizationsChangedAction { } /** - * A client toggled a customization on or off. + * A client updated a customization's enablement decisions. * * 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 + * children inside each container (a skill, agent, or other entry). Plugins + * and MCP servers retain the matched entry's explicit decisions; other + * entries update their `enabled` flag. Disabling a plugin still disables all + * of its children — the effective state of a plugin child is the plugin's + * derived enabled value and `(child.enabled ?? true)` — so toggling a child + * only matters while its plugin is enabled. Is a no-op when no * customization has the given `id`. * * The `enablement` array completely replaces all explicit decisions. A caller @@ -382,7 +383,7 @@ export interface SessionCustomizationToggledAction { type: ActionType.SessionCustomizationToggled; /** The id of the container or child to update. */ id: string; - /** The complete set of explicit decisions, replacing any existing set. */ + /** Explicit enablement decisions, replacing the previous list entirely. */ enablement: CustomizationEnablement[]; } diff --git a/types/channels-session/reducer.ts b/types/channels-session/reducer.ts index ebedd0d01..277a3dd31 100644 --- a/types/channels-session/reducer.ts +++ b/types/channels-session/reducer.ts @@ -109,20 +109,24 @@ function updateMcpServerCustomization( } /** - * Replaces a customization's explicit enablement decisions and recomputes its - * effective {@link CustomizationBase.enabled | `enabled`} value. An empty set - * drops the field entirely, so a customization at its default carries no - * provenance. + * Replaces explicit decisions for plugins and MCP servers; other customizations + * retain their legacy `enabled` field, derived from the incoming decisions. */ -function applyCustomizationEnablement(customization: T, enablement: readonly CustomizationEnablement[]): T { - const next = { ...customization }; - next.enabled = enablement[0]?.enabled ?? true; - if (enablement.length > 0) { - next.enablement = [...enablement]; - } else { - delete next.enablement; +function applyCustomizationEnablement(customization: Customization, enablement: readonly CustomizationEnablement[]): Customization; +function applyCustomizationEnablement(customization: ChildCustomization, enablement: readonly CustomizationEnablement[]): ChildCustomization; +function applyCustomizationEnablement(customization: Customization | ChildCustomization, enablement: readonly CustomizationEnablement[]): Customization | ChildCustomization { + switch (customization.type) { + case CustomizationType.Plugin: + case CustomizationType.McpServer: { + if (enablement.length > 0) { + return { ...customization, enablement: [...enablement] }; + } + const { enablement: _enablement, ...withoutEnablement } = customization; + return withoutEnablement; + } + default: + return { ...customization, enabled: enablement[0]?.enabled ?? true }; } - return next; } // ─── Session Reducer ───────────────────────────────────────────────────────── diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index 8b8b21033..c3ad23108 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -700,20 +700,6 @@ interface CustomizationBase { uri: URI; /** Human-readable name. */ name: string; - /** - * Explicit enablement decisions for this customization, one entry per scope - * that has one. This is a wire contract: producers MUST publish entries - * sorted by descending specificity (Session, Workspace, then Global). - * The agent host emits at most one Workspace entry, for the session's primary - * working directory. Consumers MAY treat - * `enablement[0]` as the decisive decision and - * `enablement?.[0]?.enabled ?? true` as the effective enabled value. An - * absent or empty array means no explicit decision exists, so the - * customization is enabled by default. - * - * Only the agent host publishes this; clients treat it as read-only provenance. - */ - enablement?: CustomizationEnablement[]; /** Icons for UI display. */ icons?: Icon[]; /** @@ -803,8 +789,6 @@ export type CustomizationLoadState = * @category Customization Types */ interface ContainerCustomizationBase extends CustomizationBase { - /** Whether this container is currently enabled. */ - enabled: boolean; /** * `clientId` of the client that contributed this container. Absent for * server-originated entries. @@ -832,6 +816,8 @@ interface ContainerCustomizationBase extends CustomizationBase { */ export interface PluginCustomization extends ContainerCustomizationBase { type: CustomizationType.Plugin; + /** Explicit enablement decisions. See {@link McpServerCustomization.enablement}. */ + enablement?: CustomizationEnablement[]; /** * Version of the plugin, sourced from the * [Open Plugins](https://open-plugins.com/) manifest's optional @@ -859,6 +845,17 @@ export interface PluginCustomization extends ContainerCustomizationBase { export interface ClientPluginCustomization extends PluginCustomization { /** Opaque version token used by the host to detect changes. */ nonce?: string; + /** + * Explicit enablement decisions for children this plugin contributes, + * keyed by child name (for MCP servers, the server name as it appears in + * the bundled `.mcp.json`). + * + * Bundled children are discovered by the host rather than published by the + * client, so the client cannot attach `enablement` to them directly. This + * carries the client's global decision for each one; the host applies it + * under the child's durable key. + */ + childEnablement?: Record; } /** @@ -876,6 +873,8 @@ export interface ClientPluginCustomization extends PluginCustomization { */ export interface DirectoryCustomization extends ContainerCustomizationBase { type: CustomizationType.Directory; + /** Whether this container is currently enabled. */ + enabled: boolean; /** Which child customization type this directory holds. */ contents: ChildCustomizationType; /** Whether clients may write into this directory. */ @@ -889,8 +888,7 @@ export interface DirectoryCustomization extends ContainerCustomizationBase { * {@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. + * base because it can appear as a top-level customization too. * * @category Customization Types */ @@ -901,9 +899,10 @@ interface ChildCustomizationBase extends CustomizationBase { * 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. + * enabled state of a plugin child is the plugin's derived enabled value and + * `(child.enabled ?? true)`, so a disabled plugin disables every child + * regardless of each child's own flag. A directory child instead uses the + * directory's `enabled` value and its own flag. * * A child is turned on or off by id with * {@link SessionCustomizationToggledAction | `session/customizationToggled`}. @@ -1055,10 +1054,37 @@ export interface HookCustomization extends ChildCustomizationBase { export interface McpServerCustomization extends CustomizationBase { type: CustomizationType.McpServer; /** - * Whether this MCP server is effectively enabled after resolving all scopes. - * {@link CustomizationBase.enablement | `enablement`} records its inputs. + * Source URI of the plugin that contributes this server. A plugin-provided + * server keeps this durable identity while temporarily published top-level; + * its durable enablement key is derived from this URI. + * + * Absent means this is an unowned server, whose durable key is + * `mcpServers#`. */ - enabled: boolean; + owningPluginUri?: URI; + /** + * Explicit enablement decisions for this customization, one entry per scope + * that has one. This is a wire contract: producers MUST publish entries + * sorted by descending specificity (Session, Workspace, then Global). + * The agent host emits at most one Workspace entry, for the session's primary + * working directory. Consumers MAY treat + * `enablement[0]` as the decisive decision and + * `enablement?.[0]?.enabled ?? true` as the effective enabled value. An + * absent or empty array means no explicit decision exists, so the + * customization is enabled by default. + * + * Flows in both directions. A client publishes this alongside a customization + * to assert its global decision, which is authoritative for the Global scope; + * a client always includes its global entry, even when enabled. The host + * publishes the fully resolved set across all scopes, and consumers derive + * the effective enabled value from that set. + */ + enablement?: CustomizationEnablement[]; + /** + * Whether the client explicitly bundled this server and owns its Global + * enablement decision. + */ + isClientBundled?: boolean; /** * Current lifecycle state of the MCP server. */ diff --git a/types/test-cases/reducers/058-session-customizationschanged-replaces-entire-list.json b/types/test-cases/reducers/058-session-customizationschanged-replaces-entire-list.json index e29bc4e02..768a141f0 100644 --- a/types/test-cases/reducers/058-session-customizationschanged-replaces-entire-list.json +++ b/types/test-cases/reducers/058-session-customizationschanged-replaces-entire-list.json @@ -17,15 +17,13 @@ "type": "plugin", "id": "plugin-a", "uri": "https://plugins.example/a", - "name": "Plugin A", - "enabled": true + "name": "Plugin A" }, { "type": "plugin", "id": "plugin-b", "uri": "https://plugins.example/b", "name": "Plugin B", - "enabled": false, "clientId": "client-1" } ] @@ -41,15 +39,13 @@ "type": "plugin", "id": "plugin-a", "uri": "https://plugins.example/a", - "name": "Plugin A", - "enabled": true + "name": "Plugin A" }, { "type": "plugin", "id": "plugin-b", "uri": "https://plugins.example/b", "name": "Plugin B", - "enabled": false, "clientId": "client-1" } ], diff --git a/types/test-cases/reducers/059-session-customizationschanged-replaces-existing-customizations.json b/types/test-cases/reducers/059-session-customizationschanged-replaces-existing-customizations.json index c23d5faf2..092ed658a 100644 --- a/types/test-cases/reducers/059-session-customizationschanged-replaces-existing-customizations.json +++ b/types/test-cases/reducers/059-session-customizationschanged-replaces-existing-customizations.json @@ -11,8 +11,7 @@ "type": "plugin", "id": "plugin-a", "uri": "https://plugins.example/a", - "name": "Plugin A", - "enabled": true + "name": "Plugin A" } ], "activeClients": [], @@ -26,8 +25,7 @@ "type": "plugin", "id": "plugin-b", "uri": "https://plugins.example/b", - "name": "Plugin B", - "enabled": false + "name": "Plugin B" } ] } @@ -42,8 +40,7 @@ "type": "plugin", "id": "plugin-b", "uri": "https://plugins.example/b", - "name": "Plugin B", - "enabled": false + "name": "Plugin B" } ], "activeClients": [], diff --git a/types/test-cases/reducers/060-session-customizationtoggled-toggles-by-id.json b/types/test-cases/reducers/060-session-customizationtoggled-toggles-by-id.json index 3a9bfe202..7d39e7ba3 100644 --- a/types/test-cases/reducers/060-session-customizationtoggled-toggles-by-id.json +++ b/types/test-cases/reducers/060-session-customizationtoggled-toggles-by-id.json @@ -11,15 +11,13 @@ "type": "plugin", "id": "plugin-a", "uri": "https://plugins.example/a", - "name": "Plugin A", - "enabled": true + "name": "Plugin A" }, { "type": "plugin", "id": "plugin-b", "uri": "https://plugins.example/b", - "name": "Plugin B", - "enabled": true + "name": "Plugin B" } ], "activeClients": [], @@ -57,7 +55,6 @@ "id": "plugin-a", "uri": "https://plugins.example/a", "name": "Plugin A", - "enabled": false, "enablement": [ { "kind": "session", @@ -78,8 +75,7 @@ "type": "plugin", "id": "plugin-b", "uri": "https://plugins.example/b", - "name": "Plugin B", - "enabled": true + "name": "Plugin B" } ], "activeClients": [], diff --git a/types/test-cases/reducers/061-session-customizationtoggled-is-no-op-for-unknown-id.json b/types/test-cases/reducers/061-session-customizationtoggled-is-no-op-for-unknown-id.json index 23eb0bf26..23445c10f 100644 --- a/types/test-cases/reducers/061-session-customizationtoggled-is-no-op-for-unknown-id.json +++ b/types/test-cases/reducers/061-session-customizationtoggled-is-no-op-for-unknown-id.json @@ -11,8 +11,7 @@ "type": "plugin", "id": "plugin-a", "uri": "https://plugins.example/a", - "name": "Plugin A", - "enabled": true + "name": "Plugin A" } ], "activeClients": [], @@ -40,8 +39,7 @@ "type": "plugin", "id": "plugin-a", "uri": "https://plugins.example/a", - "name": "Plugin A", - "enabled": true + "name": "Plugin A" } ], "activeClients": [], diff --git a/types/test-cases/reducers/137-session-customizationupdated-replaces-existing-container.json b/types/test-cases/reducers/137-session-customizationupdated-replaces-existing-container.json index cbe1886e4..11e70945c 100644 --- a/types/test-cases/reducers/137-session-customizationupdated-replaces-existing-container.json +++ b/types/test-cases/reducers/137-session-customizationupdated-replaces-existing-container.json @@ -12,7 +12,6 @@ "id": "plugin-a", "uri": "https://plugins.example/a", "name": "Plugin A", - "enabled": true, "load": { "kind": "loading" } @@ -22,7 +21,6 @@ "id": "plugin-b", "uri": "https://plugins.example/b", "name": "Plugin B", - "enabled": true, "load": { "kind": "loaded" } @@ -39,7 +37,6 @@ "id": "plugin-a", "uri": "https://plugins.example/a", "name": "Plugin A", - "enabled": true, "load": { "kind": "error", "message": "Failed to load" @@ -58,7 +55,6 @@ "id": "plugin-a", "uri": "https://plugins.example/a", "name": "Plugin A", - "enabled": true, "load": { "kind": "error", "message": "Failed to load" @@ -69,7 +65,6 @@ "id": "plugin-b", "uri": "https://plugins.example/b", "name": "Plugin B", - "enabled": true, "load": { "kind": "loaded" } diff --git a/types/test-cases/reducers/138-session-customizationupdated-appends-unknown-id.json b/types/test-cases/reducers/138-session-customizationupdated-appends-unknown-id.json index 3007df29c..f558733fb 100644 --- a/types/test-cases/reducers/138-session-customizationupdated-appends-unknown-id.json +++ b/types/test-cases/reducers/138-session-customizationupdated-appends-unknown-id.json @@ -11,8 +11,7 @@ "type": "plugin", "id": "plugin-a", "uri": "https://plugins.example/a", - "name": "Plugin A", - "enabled": true + "name": "Plugin A" } ], "activeClients": [], @@ -26,7 +25,6 @@ "id": "plugin-new", "uri": "https://plugins.example/new", "name": "New Plugin", - "enabled": true, "load": { "kind": "loading" } @@ -43,15 +41,13 @@ "type": "plugin", "id": "plugin-a", "uri": "https://plugins.example/a", - "name": "Plugin A", - "enabled": true + "name": "Plugin A" }, { "type": "plugin", "id": "plugin-new", "uri": "https://plugins.example/new", "name": "New Plugin", - "enabled": true, "load": { "kind": "loading" } diff --git a/types/test-cases/reducers/152-session-customizationremoved-removes-container-and-children.json b/types/test-cases/reducers/152-session-customizationremoved-removes-container-and-children.json index 50807563b..8110de197 100644 --- a/types/test-cases/reducers/152-session-customizationremoved-removes-container-and-children.json +++ b/types/test-cases/reducers/152-session-customizationremoved-removes-container-and-children.json @@ -12,7 +12,6 @@ "id": "plugin-a", "uri": "https://plugins.example/a", "name": "Plugin A", - "enabled": true, "children": [ { "type": "skill", @@ -26,8 +25,7 @@ "type": "plugin", "id": "plugin-b", "uri": "https://plugins.example/b", - "name": "Plugin B", - "enabled": true + "name": "Plugin B" } ], "activeClients": [], @@ -49,8 +47,7 @@ "type": "plugin", "id": "plugin-b", "uri": "https://plugins.example/b", - "name": "Plugin B", - "enabled": true + "name": "Plugin B" } ], "activeClients": [], diff --git a/types/test-cases/reducers/153-session-customizationremoved-removes-child.json b/types/test-cases/reducers/153-session-customizationremoved-removes-child.json index b0c269d31..63ceca282 100644 --- a/types/test-cases/reducers/153-session-customizationremoved-removes-child.json +++ b/types/test-cases/reducers/153-session-customizationremoved-removes-child.json @@ -12,7 +12,6 @@ "id": "plugin-a", "uri": "https://plugins.example/a", "name": "Plugin A", - "enabled": true, "children": [ { "type": "skill", @@ -49,7 +48,6 @@ "id": "plugin-a", "uri": "https://plugins.example/a", "name": "Plugin A", - "enabled": true, "children": [ { "type": "skill", diff --git a/types/test-cases/reducers/154-session-customizationremoved-noop-unknown-id.json b/types/test-cases/reducers/154-session-customizationremoved-noop-unknown-id.json index 737f2ff37..7a8e7e485 100644 --- a/types/test-cases/reducers/154-session-customizationremoved-noop-unknown-id.json +++ b/types/test-cases/reducers/154-session-customizationremoved-noop-unknown-id.json @@ -11,8 +11,7 @@ "type": "plugin", "id": "plugin-a", "uri": "https://plugins.example/a", - "name": "Plugin A", - "enabled": true + "name": "Plugin A" } ], "activeClients": [], @@ -34,8 +33,7 @@ "type": "plugin", "id": "plugin-a", "uri": "https://plugins.example/a", - "name": "Plugin A", - "enabled": true + "name": "Plugin A" } ], "activeClients": [], diff --git a/types/test-cases/reducers/159-session-mcpserverstatechanged-upserts-top-level-server.json b/types/test-cases/reducers/159-session-mcpserverstatechanged-upserts-top-level-server.json index 05dec62a7..ac61e7c6e 100644 --- a/types/test-cases/reducers/159-session-mcpserverstatechanged-upserts-top-level-server.json +++ b/types/test-cases/reducers/159-session-mcpserverstatechanged-upserts-top-level-server.json @@ -14,7 +14,6 @@ "id": "mcp-1", "uri": "file:///workspace/.mcp/servers.json", "name": "Filesystem", - "enabled": true, "state": { "kind": "starting" } @@ -44,7 +43,6 @@ "id": "mcp-1", "uri": "file:///workspace/.mcp/servers.json", "name": "Filesystem", - "enabled": true, "state": { "kind": "ready" }, diff --git a/types/test-cases/reducers/160-session-mcpserverstatechanged-upserts-container-child.json b/types/test-cases/reducers/160-session-mcpserverstatechanged-upserts-container-child.json index 09dcba162..9d5e18a61 100644 --- a/types/test-cases/reducers/160-session-mcpserverstatechanged-upserts-container-child.json +++ b/types/test-cases/reducers/160-session-mcpserverstatechanged-upserts-container-child.json @@ -14,7 +14,6 @@ "id": "plugin-a", "uri": "https://plugins.example/a", "name": "Plugin A", - "enabled": true, "children": [ { "type": "skill", @@ -27,7 +26,6 @@ "id": "mcp-child", "uri": "https://plugins.example/a#mcp/search", "name": "Search", - "enabled": true, "state": { "kind": "starting" } @@ -59,7 +57,6 @@ "id": "plugin-a", "uri": "https://plugins.example/a", "name": "Plugin A", - "enabled": true, "children": [ { "type": "skill", @@ -72,7 +69,6 @@ "id": "mcp-child", "uri": "https://plugins.example/a#mcp/search", "name": "Search", - "enabled": true, "state": { "kind": "ready" }, diff --git a/types/test-cases/reducers/161-session-mcpserverstatechanged-noop-unknown-id.json b/types/test-cases/reducers/161-session-mcpserverstatechanged-noop-unknown-id.json index 4fcaf4fd3..998c6f9e2 100644 --- a/types/test-cases/reducers/161-session-mcpserverstatechanged-noop-unknown-id.json +++ b/types/test-cases/reducers/161-session-mcpserverstatechanged-noop-unknown-id.json @@ -14,7 +14,6 @@ "id": "mcp-1", "uri": "file:///workspace/.mcp/servers.json", "name": "Filesystem", - "enabled": true, "state": { "kind": "ready" }, @@ -44,7 +43,6 @@ "id": "mcp-1", "uri": "file:///workspace/.mcp/servers.json", "name": "Filesystem", - "enabled": true, "state": { "kind": "ready" }, diff --git a/types/test-cases/reducers/162-session-mcpserverstatechanged-noop-non-mcp-id.json b/types/test-cases/reducers/162-session-mcpserverstatechanged-noop-non-mcp-id.json index 66fe919b3..225d7024d 100644 --- a/types/test-cases/reducers/162-session-mcpserverstatechanged-noop-non-mcp-id.json +++ b/types/test-cases/reducers/162-session-mcpserverstatechanged-noop-non-mcp-id.json @@ -14,7 +14,6 @@ "id": "plugin-a", "uri": "https://plugins.example/a", "name": "Plugin A", - "enabled": true, "children": [ { "type": "skill", @@ -57,7 +56,6 @@ "id": "plugin-a", "uri": "https://plugins.example/a", "name": "Plugin A", - "enabled": true, "children": [ { "type": "skill", 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 index c095907e3..ca9856b13 100644 --- 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 @@ -12,7 +12,6 @@ "id": "mcp-top", "uri": "https://mcp.example/server", "name": "Top-level Server", - "enabled": true, "state": { "kind": "ready" } @@ -22,7 +21,6 @@ "id": "plugin-a", "uri": "https://plugins.example/a", "name": "Plugin A", - "enabled": true, "children": [ { "type": "skill", @@ -66,7 +64,6 @@ "id": "mcp-top", "uri": "https://mcp.example/server", "name": "Top-level Server", - "enabled": true, "state": { "kind": "ready" } @@ -76,7 +73,6 @@ "id": "plugin-a", "uri": "https://plugins.example/a", "name": "Plugin A", - "enabled": true, "children": [ { "type": "skill", @@ -84,12 +80,6 @@ "uri": "https://plugins.example/a#skills/lint", "name": "lint", "enabled": false, - "enablement": [ - { - "kind": "session", - "enabled": false - } - ], "disableUserInvocation": true }, { 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 index 5dd151d49..0edff6148 100644 --- 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 @@ -12,7 +12,6 @@ "id": "plugin-a", "uri": "https://plugins.example/a", "name": "Plugin A", - "enabled": true, "children": [ { "type": "skill", @@ -49,7 +48,6 @@ "id": "plugin-a", "uri": "https://plugins.example/a", "name": "Plugin A", - "enabled": true, "children": [ { "type": "skill", diff --git a/types/test-cases/reducers/235-session-mcpserverstartrequested-starts-top-level-server.json b/types/test-cases/reducers/235-session-mcpserverstartrequested-starts-top-level-server.json index 4832c0635..190725ae3 100644 --- a/types/test-cases/reducers/235-session-mcpserverstartrequested-starts-top-level-server.json +++ b/types/test-cases/reducers/235-session-mcpserverstartrequested-starts-top-level-server.json @@ -14,7 +14,6 @@ "id": "mcp-1", "uri": "file:///workspace/.mcp/servers.json", "name": "Filesystem", - "enabled": true, "state": { "kind": "ready" }, @@ -41,7 +40,6 @@ "id": "mcp-1", "uri": "file:///workspace/.mcp/servers.json", "name": "Filesystem", - "enabled": true, "state": { "kind": "starting" }, diff --git a/types/test-cases/reducers/236-session-mcpserverstoprequested-stops-auth-required-server.json b/types/test-cases/reducers/236-session-mcpserverstoprequested-stops-auth-required-server.json index c6a50f29c..8863c6336 100644 --- a/types/test-cases/reducers/236-session-mcpserverstoprequested-stops-auth-required-server.json +++ b/types/test-cases/reducers/236-session-mcpserverstoprequested-stops-auth-required-server.json @@ -14,7 +14,6 @@ "id": "mcp-1", "uri": "file:///workspace/.mcp/servers.json", "name": "Filesystem", - "enabled": true, "state": { "kind": "authRequired", "reason": "required", @@ -47,7 +46,6 @@ "id": "mcp-1", "uri": "file:///workspace/.mcp/servers.json", "name": "Filesystem", - "enabled": true, "state": { "kind": "stopped" }, diff --git a/types/test-cases/reducers/237-session-mcpserverstoprequested-stops-container-child.json b/types/test-cases/reducers/237-session-mcpserverstoprequested-stops-container-child.json index 6604aa753..d67bc95a9 100644 --- a/types/test-cases/reducers/237-session-mcpserverstoprequested-stops-container-child.json +++ b/types/test-cases/reducers/237-session-mcpserverstoprequested-stops-container-child.json @@ -14,7 +14,6 @@ "id": "plugin-a", "uri": "https://plugins.example/a", "name": "Plugin A", - "enabled": true, "children": [ { "type": "skill", @@ -27,7 +26,6 @@ "id": "mcp-child", "uri": "https://plugins.example/a#mcp/search", "name": "Search", - "enabled": true, "state": { "kind": "ready" }, @@ -56,7 +54,6 @@ "id": "plugin-a", "uri": "https://plugins.example/a", "name": "Plugin A", - "enabled": true, "children": [ { "type": "skill", @@ -69,7 +66,6 @@ "id": "mcp-child", "uri": "https://plugins.example/a#mcp/search", "name": "Search", - "enabled": true, "state": { "kind": "stopped" }, diff --git a/types/test-cases/reducers/239-session-mcpserverstartrequested-no-op-unknown-id.json b/types/test-cases/reducers/239-session-mcpserverstartrequested-no-op-unknown-id.json index f0c05949b..840d16fc5 100644 --- a/types/test-cases/reducers/239-session-mcpserverstartrequested-no-op-unknown-id.json +++ b/types/test-cases/reducers/239-session-mcpserverstartrequested-no-op-unknown-id.json @@ -13,15 +13,13 @@ "type": "plugin", "id": "plugin-a", "uri": "https://plugins.example/a", - "name": "Plugin A", - "enabled": true + "name": "Plugin A" }, { "type": "plugin", "id": "plugin-b", "uri": "https://plugins.example/b", "name": "Plugin B", - "enabled": true, "children": [ { "type": "skill", @@ -36,7 +34,6 @@ "id": "mcp-1", "uri": "file:///workspace/.mcp/servers.json", "name": "Filesystem", - "enabled": true, "state": { "kind": "stopped" } @@ -61,15 +58,13 @@ "type": "plugin", "id": "plugin-a", "uri": "https://plugins.example/a", - "name": "Plugin A", - "enabled": true + "name": "Plugin A" }, { "type": "plugin", "id": "plugin-b", "uri": "https://plugins.example/b", "name": "Plugin B", - "enabled": true, "children": [ { "type": "skill", @@ -84,7 +79,6 @@ "id": "mcp-1", "uri": "file:///workspace/.mcp/servers.json", "name": "Filesystem", - "enabled": true, "state": { "kind": "stopped" } diff --git a/types/test-cases/reducers/263-session-customizationtoggled-clears-enablement.json b/types/test-cases/reducers/263-session-customizationtoggled-clears-enablement.json index 6e63c9fdd..118619f78 100644 --- a/types/test-cases/reducers/263-session-customizationtoggled-clears-enablement.json +++ b/types/test-cases/reducers/263-session-customizationtoggled-clears-enablement.json @@ -12,7 +12,6 @@ "id": "server-a", "uri": "file:///workspace/.vscode/mcp.json", "name": "Server A", - "enabled": false, "enablement": [ { "kind": "workspace", @@ -50,7 +49,6 @@ "id": "server-a", "uri": "file:///workspace/.vscode/mcp.json", "name": "Server A", - "enabled": true, "state": { "kind": "stopped" } diff --git a/types/test-cases/round-trips/027-agent-customization-model-and-tools.json b/types/test-cases/round-trips/027-agent-customization-model-and-tools.json index 6bef7c15a..168e69c25 100644 --- a/types/test-cases/round-trips/027-agent-customization-model-and-tools.json +++ b/types/test-cases/round-trips/027-agent-customization-model-and-tools.json @@ -8,7 +8,6 @@ "id": "plugin-1", "uri": "https://example.com/plugins/coding", "name": "Coding Plugin", - "enabled": true, "children": [ { "type": "agent", @@ -27,7 +26,6 @@ "id": "plugin-1", "uri": "https://example.com/plugins/coding", "name": "Coding Plugin", - "enabled": true, "children": [ { "type": "agent", From bdea6b44ac5b45a4fd6365071dcde672299c1564 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 12 Aug 2026 23:16:51 -0700 Subject: [PATCH 3/4] session: omit MCP plugin ownership Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- clients/go/ahptypes/state.generated.go | 7 ------- .../agenthostprotocol/generated/State.generated.kt | 9 --------- clients/rust/crates/ahp-types/src/state.rs | 8 -------- .../AgentHostProtocol/Generated/State.generated.swift | 10 ---------- ...260812-client-bundled-customization-enablement.json | 2 +- docs/guide/customizations.md | 7 ++----- docs/guide/mcp.md | 1 - schema/actions.schema.json | 4 ---- schema/commands.schema.json | 4 ---- schema/errors.schema.json | 4 ---- schema/notifications.schema.json | 4 ---- schema/state.schema.json | 4 ---- types/channels-session/state.ts | 9 --------- 13 files changed, 3 insertions(+), 70 deletions(-) diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index ce14d63c6..d53d41063 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -2884,13 +2884,6 @@ type McpServerCustomization struct { // out-of-band. Meta map[string]json.RawMessage `json:"_meta,omitempty"` Type CustomizationType `json:"type"` - // Source URI of the plugin that contributes this server. A plugin-provided - // server keeps this durable identity while temporarily published top-level; - // its durable enablement key is derived from this URI. - // - // Absent means this is an unowned server, whose durable key is - // `mcpServers#`. - OwningPluginUri *URI `json:"owningPluginUri,omitempty"` // Explicit enablement decisions for this customization, one entry per scope // that has one. This is a wire contract: producers MUST publish entries // sorted by descending specificity (Session, Workspace, then Global). 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 dfb9d49ed..81c458561 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 @@ -3901,15 +3901,6 @@ data class McpServerCustomization( @SerialName("_meta") val meta: Map? = null, val type: CustomizationType, - /** - * Source URI of the plugin that contributes this server. A plugin-provided - * server keeps this durable identity while temporarily published top-level; - * its durable enablement key is derived from this URI. - * - * Absent means this is an unowned server, whose durable key is - * `mcpServers#`. - */ - val owningPluginUri: String? = null, /** * Explicit enablement decisions for this customization, one entry per scope * that has one. This is a wire contract: producers MUST publish entries diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 64a7a2c06..44f0ea48c 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -3481,14 +3481,6 @@ pub struct McpServerCustomization { /// out-of-band. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, - /// Source URI of the plugin that contributes this server. A plugin-provided - /// server keeps this durable identity while temporarily published top-level; - /// its durable enablement key is derived from this URI. - /// - /// Absent means this is an unowned server, whose durable key is - /// `mcpServers#`. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub owning_plugin_uri: Option, /// Explicit enablement decisions for this customization, one entry per scope /// that has one. This is a wire contract: producers MUST publish entries /// sorted by descending specificity (Session, Workspace, then Global). diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index bdbeacfa2..2f61d18a0 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -4276,13 +4276,6 @@ public struct McpServerCustomization: Codable, Sendable { /// out-of-band. public var meta: [String: AnyCodable]? public var type: CustomizationType - /// Source URI of the plugin that contributes this server. A plugin-provided - /// server keeps this durable identity while temporarily published top-level; - /// its durable enablement key is derived from this URI. - /// - /// Absent means this is an unowned server, whose durable key is - /// `mcpServers#`. - public var owningPluginUri: String? /// Explicit enablement decisions for this customization, one entry per scope /// that has one. This is a wire contract: producers MUST publish entries /// sorted by descending specificity (Session, Workspace, then Global). @@ -4332,7 +4325,6 @@ public struct McpServerCustomization: Codable, Sendable { case range case meta = "_meta" case type - case owningPluginUri case enablement case isClientBundled case state @@ -4348,7 +4340,6 @@ public struct McpServerCustomization: Codable, Sendable { range: TextRange? = nil, meta: [String: AnyCodable]? = nil, type: CustomizationType, - owningPluginUri: String? = nil, enablement: [CustomizationEnablement]? = nil, isClientBundled: Bool? = nil, state: McpServerState, @@ -4362,7 +4353,6 @@ public struct McpServerCustomization: Codable, Sendable { self.range = range self.meta = meta self.type = type - self.owningPluginUri = owningPluginUri self.enablement = enablement self.isClientBundled = isClientBundled self.state = state diff --git a/docs/.changes/20260812-client-bundled-customization-enablement.json b/docs/.changes/20260812-client-bundled-customization-enablement.json index 35655ab9e..60115facc 100644 --- a/docs/.changes/20260812-client-bundled-customization-enablement.json +++ b/docs/.changes/20260812-client-bundled-customization-enablement.json @@ -1,4 +1,4 @@ { "type": "added", - "message": "`McpServerCustomization` now exposes plugin ownership and client-bundled enablement metadata, and `ClientPluginCustomization` can publish child enablement decisions." + "message": "`McpServerCustomization` now exposes client-bundled enablement metadata, and `ClientPluginCustomization` can publish child enablement decisions." } diff --git a/docs/guide/customizations.md b/docs/guide/customizations.md index 17df81dbd..872ddea16 100644 --- a/docs/guide/customizations.md +++ b/docs/guide/customizations.md @@ -118,7 +118,7 @@ SkillCustomization { type: 'skill'; description?, disableModelInvoc PromptCustomization { type: 'prompt'; description? } RuleCustomization { type: 'rule'; description?, alwaysApply?, globs? } // covers "instruction" formats too HookCustomization { type: 'hook'; event?, matcher? } -McpServerCustomization { type: 'mcpServer'; owningPluginUri?, enablement?, isClientBundled?, state, channel?, mcpApp? } // see /guide/mcp +McpServerCustomization { type: 'mcpServer'; enablement?, isClientBundled?, 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. @@ -155,10 +155,7 @@ For MCP servers, enablement flows both ways. A client publishing a server includes its global decision (even when enabled), and the host publishes the fully resolved decisions across all scopes. Client-published plugins can supply global decisions for their discovered children through -`ClientPluginCustomization.childEnablement`, keyed by child name. The host -applies these under each child's durable key. A plugin-provided MCP server -retains `owningPluginUri` while temporarily surfaced at the top level; an -unowned server uses `mcpServers#` as its durable key. +`ClientPluginCustomization.childEnablement`, keyed by child name. `DirectoryCustomization` and the five leaf child kinds retain their plain `enabled` fields. A plugin's effective value is derived from its `enablement`; diff --git a/docs/guide/mcp.md b/docs/guide/mcp.md index 1e4cbd187..a8e0be7f0 100644 --- a/docs/guide/mcp.md +++ b/docs/guide/mcp.md @@ -33,7 +33,6 @@ McpServerCustomization { name: string icons?: Icon[] range?: TextRange // span inside `uri` for inline declarations - owningPluginUri?: URI // durable identity for a plugin-provided server enablement?: CustomizationEnablement[] // user-toggleable (see Customizations guide) isClientBundled?: boolean // client owns the Global enablement decision state: McpServerState // discriminated union — see below diff --git a/schema/actions.schema.json b/schema/actions.schema.json index fb0e43b4f..d0731e6f5 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -4265,10 +4265,6 @@ "type": { "const": "mcpServer" }, - "owningPluginUri": { - "$ref": "#/$defs/URI", - "description": "Source URI of the plugin that contributes this server. A plugin-provided\nserver keeps this durable identity while temporarily published top-level;\nits durable enablement key is derived from this URI.\n\nAbsent means this is an unowned server, whose durable key is\n`mcpServers#`." - }, "enablement": { "type": "array", "items": { diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 31060e177..e1911e570 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -3571,10 +3571,6 @@ "type": { "const": "mcpServer" }, - "owningPluginUri": { - "$ref": "#/$defs/URI", - "description": "Source URI of the plugin that contributes this server. A plugin-provided\nserver keeps this durable identity while temporarily published top-level;\nits durable enablement key is derived from this URI.\n\nAbsent means this is an unowned server, whose durable key is\n`mcpServers#`." - }, "enablement": { "type": "array", "items": { diff --git a/schema/errors.schema.json b/schema/errors.schema.json index bf5c1e3d8..faf6b0288 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -2167,10 +2167,6 @@ "type": { "const": "mcpServer" }, - "owningPluginUri": { - "$ref": "#/$defs/URI", - "description": "Source URI of the plugin that contributes this server. A plugin-provided\nserver keeps this durable identity while temporarily published top-level;\nits durable enablement key is derived from this URI.\n\nAbsent means this is an unowned server, whose durable key is\n`mcpServers#`." - }, "enablement": { "type": "array", "items": { diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 07eb29ec0..19ab77673 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -2330,10 +2330,6 @@ "type": { "const": "mcpServer" }, - "owningPluginUri": { - "$ref": "#/$defs/URI", - "description": "Source URI of the plugin that contributes this server. A plugin-provided\nserver keeps this durable identity while temporarily published top-level;\nits durable enablement key is derived from this URI.\n\nAbsent means this is an unowned server, whose durable key is\n`mcpServers#`." - }, "enablement": { "type": "array", "items": { diff --git a/schema/state.schema.json b/schema/state.schema.json index 21d0bae6b..be1850022 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -2078,10 +2078,6 @@ "type": { "const": "mcpServer" }, - "owningPluginUri": { - "$ref": "#/$defs/URI", - "description": "Source URI of the plugin that contributes this server. A plugin-provided\nserver keeps this durable identity while temporarily published top-level;\nits durable enablement key is derived from this URI.\n\nAbsent means this is an unowned server, whose durable key is\n`mcpServers#`." - }, "enablement": { "type": "array", "items": { diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index c3ad23108..e10c1d221 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -1053,15 +1053,6 @@ export interface HookCustomization extends ChildCustomizationBase { */ export interface McpServerCustomization extends CustomizationBase { type: CustomizationType.McpServer; - /** - * Source URI of the plugin that contributes this server. A plugin-provided - * server keeps this durable identity while temporarily published top-level; - * its durable enablement key is derived from this URI. - * - * Absent means this is an unowned server, whose durable key is - * `mcpServers#`. - */ - owningPluginUri?: URI; /** * Explicit enablement decisions for this customization, one entry per scope * that has one. This is a wire contract: producers MUST publish entries From 6b329f6d90b8d52a478920cb413e03d0344608a8 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 12 Aug 2026 23:19:49 -0700 Subject: [PATCH 4/4] rust: format generated reducers The wider `CustomizationEnablement` import and the extra `apply_toggle` parameter push both past rustfmt's line width, so the generated output needed a reflow. Caught by CI's "Check Rust formatting" step, which runs `cargo fmt --check` separately from `cargo test`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- clients/rust/crates/ahp/src/reducers.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index c25118796..05f2d96cd 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -60,15 +60,15 @@ use ahp_types::actions::{ use ahp_types::state::{ ActiveTurn, AnnotationsState, ChangesetOperationStatus, ChangesetState, ChangesetStatus, ChatInputRequest, ChatState, ChildCustomization, ConfirmationOption, Customization, - CustomizationEnablement, ErrorInfo, - InputRequestResponsePart, McpServerStartingState, McpServerState, McpServerStoppedState, - PendingMessage, PendingMessageKind, ResourceWatchState, ResponsePart, RootState, - SessionInputRequest, SessionLifecycle, SessionState, SessionStatus, TerminalCommandPart, - TerminalContentPart, TerminalState, TerminalUnclassifiedPart, ToolCallAuthRequiredState, - ToolCallCancellationReason, ToolCallCancelledState, ToolCallCompletedState, - ToolCallConfirmationReason, ToolCallContributor, ToolCallPendingConfirmationState, - ToolCallPendingResultConfirmationState, ToolCallResponsePart, ToolCallRunningState, - ToolCallState, ToolCallStatus, ToolCallStreamingState, ToolInput, Turn, TurnState, + CustomizationEnablement, ErrorInfo, InputRequestResponsePart, McpServerStartingState, + McpServerState, McpServerStoppedState, PendingMessage, PendingMessageKind, ResourceWatchState, + ResponsePart, RootState, SessionInputRequest, SessionLifecycle, SessionState, SessionStatus, + TerminalCommandPart, TerminalContentPart, TerminalState, TerminalUnclassifiedPart, + ToolCallAuthRequiredState, ToolCallCancellationReason, ToolCallCancelledState, + ToolCallCompletedState, ToolCallConfirmationReason, ToolCallContributor, + ToolCallPendingConfirmationState, ToolCallPendingResultConfirmationState, ToolCallResponsePart, + ToolCallRunningState, ToolCallState, ToolCallStatus, ToolCallStreamingState, ToolInput, Turn, + TurnState, }; /// What happened when an action was applied. @@ -549,7 +549,11 @@ fn apply_child_enablement(c: &mut ChildCustomization, enablement: &[Customizatio } } -fn apply_toggle(list: &mut [Customization], id: &str, enablement: &[CustomizationEnablement]) -> bool { +fn apply_toggle( + list: &mut [Customization], + id: &str, + enablement: &[CustomizationEnablement], +) -> bool { if let Some(container) = list.iter_mut().find(|c| customization_id(c) == Some(id)) { apply_container_enablement(container, enablement); return true;