session: scoped customization enablement - #397
Conversation
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>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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>
There was a problem hiding this comment.
Pull request overview
This PR evolves the session customization model to support scoped enablement decisions (Session > Workspace > Global) for plugins and MCP servers, replacing the previous single enabled boolean on those customization kinds while keeping directories and leaf children on the legacy enabled semantics. It also ports the updated reducer behavior and fixtures across all generated clients and refreshes the JSON schemas and docs accordingly.
Changes:
- Introduces
CustomizationEnablement(scoped decisions) and updatesPluginCustomization/McpServerCustomizationto carryenablement?instead ofenabled. - Updates
session/customizationToggledto replace the completeenablementdecision set (and updates reducers + fixtures accordingly). - Extends client-published plugin metadata (
childEnablement) and MCP server metadata (isClientBundled), and propagates codegen/schema/doc updates across the repo.
Show a summary per file
| File | Description |
|---|---|
| types/channels-session/state.ts | Adds scoped enablement types; updates customization shapes (plugins/MCP servers now use enablement?). |
| types/channels-session/actions.ts | Updates session/customizationToggled payload from enabled: boolean to enablement: CustomizationEnablement[]. |
| types/channels-session/reducer.ts | Applies enablement changes in the TS reducer, preserving legacy enabled behavior for directories/leaf children. |
| schema/state.schema.json | Updates JSON schema to match new enablement model and adds CustomizationEnablement definition. |
| schema/actions.schema.json | Updates SessionCustomizationToggledAction schema to use enablement array and new description. |
| schema/commands.schema.json | Propagates updated customization schemas into commands schema. |
| schema/notifications.schema.json | Propagates updated customization schemas into notifications schema. |
| schema/errors.schema.json | Propagates updated customization schemas into errors schema. |
| scripts/generate-swift.ts | Extends Swift generator for CustomizationEnablement and related enums; adds exhaustiveness coverage. |
| scripts/generate-rust.ts | Extends Rust generator for CustomizationEnablement and related enums; wires into actions/state generation. |
| scripts/generate-kotlin.ts | Extends Kotlin generator for CustomizationEnablement and related enums; adds serializer + exhaustiveness coverage. |
| scripts/generate-go.ts | Extends Go generator for CustomizationEnablement and related enums; adds exhaustiveness coverage. |
| docs/guide/customizations.md | Documents scoped enablement model and updated toggle action semantics. |
| docs/guide/mcp.md | Updates MCP server docs to describe scoped enablement and new client-bundled metadata. |
| docs/guide/actions.md | Updates action guide text for session/customizationToggled semantics. |
| docs/.changes/20260804-scoped-customization-enablement.json | Adds changelog fragment for the enablement model change. |
| docs/.changes/20260812-client-bundled-customization-enablement.json | Adds changelog fragment for client-bundled MCP metadata and child enablement publication. |
| types/test-cases/round-trips/027-agent-customization-model-and-tools.json | Updates round-trip fixture to remove legacy enabled on plugin containers. |
| types/test-cases/reducers/263-session-customizationtoggled-clears-enablement.json | Adds reducer fixture validating clearing enablement restores defaults. |
| types/test-cases/reducers/239-session-mcpserverstartrequested-no-op-unknown-id.json | Updates reducer fixture to remove legacy enabled flags from plugin/MCP server entries. |
| types/test-cases/reducers/237-session-mcpserverstoprequested-stops-container-child.json | Updates reducer fixture to remove legacy enabled flags from plugin/MCP server entries. |
| types/test-cases/reducers/236-session-mcpserverstoprequested-stops-auth-required-server.json | Updates reducer fixture to remove legacy enabled flag from MCP server entries. |
| types/test-cases/reducers/235-session-mcpserverstartrequested-starts-top-level-server.json | Updates reducer fixture to remove legacy enabled flag from MCP server entries. |
| types/test-cases/reducers/226-session-customizationtoggled-is-no-op-for-unknown-child-id.json | Updates reducer fixture to use enablement in toggle action and remove legacy container enabled. |
| types/test-cases/reducers/225-session-customizationtoggled-toggles-child-by-id.json | Updates reducer fixture to use enablement in toggle action and remove legacy flags. |
| types/test-cases/reducers/162-session-mcpserverstatechanged-noop-non-mcp-id.json | Updates reducer fixture to remove legacy container enabled. |
| types/test-cases/reducers/161-session-mcpserverstatechanged-noop-unknown-id.json | Updates reducer fixture to remove legacy MCP server enabled. |
| types/test-cases/reducers/160-session-mcpserverstatechanged-upserts-container-child.json | Updates reducer fixture to remove legacy enabled flags. |
| types/test-cases/reducers/159-session-mcpserverstatechanged-upserts-top-level-server.json | Updates reducer fixture to remove legacy MCP server enabled. |
| types/test-cases/reducers/154-session-customizationremoved-noop-unknown-id.json | Updates reducer fixture to remove legacy plugin enabled. |
| types/test-cases/reducers/153-session-customizationremoved-removes-child.json | Updates reducer fixture to remove legacy container enabled. |
| types/test-cases/reducers/152-session-customizationremoved-removes-container-and-children.json | Updates reducer fixture to remove legacy enabled flags. |
| types/test-cases/reducers/138-session-customizationupdated-appends-unknown-id.json | Updates reducer fixture to remove legacy enabled flags from plugins. |
| types/test-cases/reducers/137-session-customizationupdated-replaces-existing-container.json | Updates reducer fixture to remove legacy enabled flags from plugins. |
| types/test-cases/reducers/062-session-customizationtoggled-is-no-op-when-customizations-undefined.json | Updates fixture to use enablement array in toggle action. |
| types/test-cases/reducers/061-session-customizationtoggled-is-no-op-for-unknown-id.json | Updates fixture to use enablement array in toggle action and remove legacy plugin enabled. |
| types/test-cases/reducers/060-session-customizationtoggled-toggles-by-id.json | Updates fixture to use scoped enablement array and remove legacy plugin enabled. |
| types/test-cases/reducers/059-session-customizationschanged-replaces-existing-customizations.json | Updates fixture to remove legacy plugin enabled. |
| types/test-cases/reducers/058-session-customizationschanged-replaces-entire-list.json | Updates fixture to remove legacy plugin enabled. |
| clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift | Updates Swift session reducer to call updated toggle helper with enablement list. |
| clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift | Implements enablement application logic for Swift client reducer behavior. |
| clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift | Updates Swift generated state types for enablement model and new union. |
| clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift | Updates Swift generated action type for session/customizationToggled. |
| clients/rust/crates/ahp/src/reducers.rs | Implements enablement application logic for Rust client reducer behavior. |
| clients/rust/crates/ahp-types/src/state.rs | Updates Rust generated state types for enablement model and new union. |
| clients/rust/crates/ahp-types/src/actions.rs | Updates Rust generated action type for session/customizationToggled. |
| clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt | Implements enablement application logic for Kotlin client reducer behavior. |
| clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt | Updates Kotlin generated state types for enablement model and new union. |
| clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt | Updates Kotlin generated action type for session/customizationToggled. |
| clients/go/ahptypes/state.generated.go | Updates Go generated state types for enablement model and new union. |
| clients/go/ahptypes/actions.generated.go | Updates Go generated action type for session/customizationToggled. |
| clients/go/ahp/reducers.go | Implements enablement application logic for Go client reducer behavior. |
Review details
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Files reviewed: 48/52 changed files
- Comments generated: 4
- Review effort level: Lite
| 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)] |
| 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 |
| * 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 |
| /** | ||
| * 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<string, CustomizationEnablement[]>; |
There was a problem hiding this comment.
AI Review: The council independently confirmed this contract mismatch. The array shape also permits zero or multiple Global entries, even though the field is described as carrying the client's single Global decision for each child. A single Global-only decision value would make both invalid scope and invalid cardinality unrepresentable.
Dmitriy Vasyura (dmitrivMS)
left a comment
There was a problem hiding this comment.
AI Review: Council review identified two additional protocol contract questions.
| case CustomizationType.Plugin: | ||
| case CustomizationType.McpServer: { | ||
| if (enablement.length > 0) { | ||
| return { ...customization, enablement: [...enablement] }; |
There was a problem hiding this comment.
AI Review: session/customizationToggled is client-dispatchable and schema-valid arrays can arrive in any order, but this reducer persists caller order while consumers derive the effective value from element 0. The wire contract says producers MUST sort, yet the server-validation table has no rule for rejecting unsorted or duplicate-scope input, so a malformed client action can be optimistically applied and echoed with the wrong effective state. Please either canonicalize/deduplicate at this boundary in every reducer, or specify server rejection semantics and add shared fixtures for unsorted and duplicate kinds.
| \t\t} | ||
| \t\te.Value = &value | ||
| \tdefault: | ||
| \t\treturn &json.UnmarshalTypeError{Value: "CustomizationEnablement"} |
There was a problem hiding this comment.
AI Review: Is CustomizationEnablement intentionally a closed union? Go, Kotlin, and Swift all fail the containing payload on an unknown kind, while AHP clients are released independently and several state unions preserve unknown variants. If another scope may be added within a compatible protocol version, this needs an unknown-preserving branch plus a round-trip fixture; otherwise, please document that adding a scope requires an incompatible protocol version.
Summary
CustomizationEnablementdecisions for plugins and MCP servers, with precedence ordered Session, Workspace, then Global.enabledsemantics.Wire contract
Producers publish
enablemententries in descending specificity. Consumers may useenablement?.[0]?.enabled ?? true; absent or empty enablement means enabled. The host emits at most one Workspace decision for the session primary working directory.Consumer: microsoft/vscode#330566