diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs index f0617a73c..9426d52c4 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs @@ -2396,7 +2396,9 @@ public sealed record AnnotationsUpdatedAction /// /// This side-effect request leaves optimistic catalogue state unchanged. The /// host validates trigger ids and configuration, normalizes event-trigger -/// titles and descriptions, persists the definition, then publishes the +/// titles and descriptions, captures any +/// {@link AutomationSessionTemplate.customizations | session customizations} +/// from the dispatching client, persists the definition, then publishes the /// authoritative result with {@link AutomationSetAction | `automation/set`}. /// Rejections leave the catalogue unchanged. public sealed record AutomationCreateRequestedAction diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs index 0ef02d8be..72a9e4361 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs @@ -300,6 +300,11 @@ public sealed record AutomationCapabilities /// implementation-defined. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public long? RunHistoryLimit { get; init; } + + /// Present when {@link AutomationSessionTemplate.customizations} may contain + /// client plugins for the host to capture. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public AutomationCustomizationsCapability? Customizations { get; init; } } /// Presence capability for {@link AutomationCreateRequestedAction | @@ -333,6 +338,15 @@ public sealed record AutomationRunCancellationCapability { } +/// Presence capability for +/// {@link AutomationSessionTemplate.customizations | automation customizations}. +/// +/// The empty object means "supported"; fields are reserved for future +/// capture options and limits. +public sealed record AutomationCustomizationsCapability +{ +} + /// Re-establishes a dropped connection. The server replays missed actions or /// provides fresh snapshots. public sealed record ReconnectParams diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs index 0640c712e..4b0b296cc 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs @@ -36,6 +36,7 @@ namespace Microsoft.AgentHostProtocol; [JsonSerializable(typeof(AutomationCompletedRunLifecycle))] [JsonSerializable(typeof(AutomationCreateCapability))] [JsonSerializable(typeof(AutomationCreateRequestedAction))] +[JsonSerializable(typeof(AutomationCustomizationsCapability))] [JsonSerializable(typeof(AutomationDefinition))] [JsonSerializable(typeof(AutomationDefinitionPatch))] [JsonSerializable(typeof(AutomationEntry))] diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index 9590319ea..8b14d7302 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -5228,6 +5228,32 @@ public sealed record AutomationSessionTemplate /// {@link ResolveSessionConfigResult.values}. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Dictionary? Config { get; init; } + + /// Client plugins to make available in every run session, in the same + /// published shape as + /// {@link SessionActiveClient.customizations | `activeClients[].customizations`}. + /// Entries are keyed by `id`. + /// + /// Runs usually start when no client is connected, so the host does not + /// resolve these URIs at run time. Instead, when it accepts a + /// {@link AutomationCreateRequestedAction | `automation/createRequested`} or + /// {@link AutomationUpdateRequestedAction | `automation/updateRequested`} + /// that adds an entry or changes an entry's `uri` or `nonce`, the host + /// captures a host-owned copy of the plugin. For client-served URIs such as + /// `virtual://…`, it reads the contents from the dispatching client with + /// server→client `resource*` requests. If a capture fails, the host rejects + /// the whole action. Entries whose `id`, `uri`, and `nonce` are unchanged keep + /// their existing copy, so any client can re-submit a template it received + /// without being able to serve the plugin itself. The resulting copies are + /// reported in {@link AutomationEntry.customizations}. + /// + /// The host MAY share one stored copy between entries with equal `uri` and + /// `nonce`, including across automations; this is not observable to clients. + /// + /// Clients MUST NOT set this field unless the host advertises + /// {@link AutomationCapabilities.customizations}. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Customizations { get; init; } } /// Durable, client-editable definition of an automation. @@ -5277,7 +5303,9 @@ public sealed record AutomationDefinitionPatch public Message? Message { get; init; } /// Replacement {@link AutomationDefinition.session}. The host revalidates - /// affected event triggers when their discovery context changes. + /// affected event triggers when their discovery context changes, and + /// captures {@link AutomationSessionTemplate.customizations} entries that + /// are new or whose `uri` or `nonce` changed. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public AutomationSessionTemplate? Session { get; init; } @@ -5325,6 +5353,22 @@ public sealed class AutomationEntry /// Operations currently permitted for this automation. public required List Operations { get; set; } + /// Host-owned copies of the plugins in + /// {@link AutomationSessionTemplate.customizations}, one per template entry + /// with the same `id`. Absent when the template has no customizations. + /// + /// Each copy's `uri` identifies the captured contents, which clients can + /// browse with `resourceRead`. `children` and `load` report what the host + /// found in that copy, independent of whether the originating client is + /// connected. `clientId` is absent because the copy no longer depends on a + /// client. + /// + /// Every run session receives these plugins in + /// {@link SessionState.customizations}, with the enablement from the + /// matching template entry. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Customizations { get; set; } + /// Creation timestamp in ISO 8601 format. public required string CreatedAt { get; set; } diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index ea5e6a569..163db0ca9 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -1573,7 +1573,9 @@ type ResourceWatchChangedAction struct { // // This side-effect request leaves optimistic catalogue state unchanged. The // host validates trigger ids and configuration, normalizes event-trigger -// titles and descriptions, persists the definition, then publishes the +// titles and descriptions, captures any +// {@link AutomationSessionTemplate.customizations | session customizations} +// from the dispatching client, persists the definition, then publishes the // authoritative result with {@link AutomationSetAction | `automation/set`}. // Rejections leave the catalogue unchanged. type AutomationCreateRequestedAction struct { diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index 9ef81b6f8..4b695fc1e 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -223,6 +223,9 @@ type AutomationCapabilities struct { // runs are not counted toward the limit. Absence means the retention limit is // implementation-defined. RunHistoryLimit *int64 `json:"runHistoryLimit,omitempty"` + // Present when {@link AutomationSessionTemplate.customizations} may contain + // client plugins for the host to capture. + Customizations *AutomationCustomizationsCapability `json:"customizations,omitempty"` } // Presence capability for {@link AutomationCreateRequestedAction | @@ -252,6 +255,14 @@ type AutomationScheduleCapabilities struct { type AutomationRunCancellationCapability struct { } +// Presence capability for +// {@link AutomationSessionTemplate.customizations | automation customizations}. +// +// The empty object means "supported"; fields are reserved for future +// capture options and limits. +type AutomationCustomizationsCapability struct { +} + // Identifies a protocol implementation — the software (and build) on one end // of the connection, as distinct from the {@link AgentInfo | agent persona} it // hosts. Carried as {@link InitializeParams.clientInfo | `clientInfo`} on the diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 4c002cade..a4f280886 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -3939,6 +3939,30 @@ type AutomationSessionTemplate struct { // {@link CreateSessionParams.config}, normally obtained from // {@link ResolveSessionConfigResult.values}. Config map[string]json.RawMessage `json:"config,omitempty"` + // Client plugins to make available in every run session, in the same + // published shape as + // {@link SessionActiveClient.customizations | `activeClients[].customizations`}. + // Entries are keyed by `id`. + // + // Runs usually start when no client is connected, so the host does not + // resolve these URIs at run time. Instead, when it accepts a + // {@link AutomationCreateRequestedAction | `automation/createRequested`} or + // {@link AutomationUpdateRequestedAction | `automation/updateRequested`} + // that adds an entry or changes an entry's `uri` or `nonce`, the host + // captures a host-owned copy of the plugin. For client-served URIs such as + // `virtual://…`, it reads the contents from the dispatching client with + // server→client `resource*` requests. If a capture fails, the host rejects + // the whole action. Entries whose `id`, `uri`, and `nonce` are unchanged keep + // their existing copy, so any client can re-submit a template it received + // without being able to serve the plugin itself. The resulting copies are + // reported in {@link AutomationEntry.customizations}. + // + // The host MAY share one stored copy between entries with equal `uri` and + // `nonce`, including across automations; this is not observable to clients. + // + // Clients MUST NOT set this field unless the host advertises + // {@link AutomationCapabilities.customizations}. + Customizations []ClientPluginCustomization `json:"customizations,omitempty"` } // Durable, client-editable definition of an automation. @@ -3975,7 +3999,9 @@ type AutomationDefinitionPatch struct { // Replacement {@link AutomationDefinition.message}. Message *Message `json:"message,omitempty"` // Replacement {@link AutomationDefinition.session}. The host revalidates - // affected event triggers when their discovery context changes. + // affected event triggers when their discovery context changes, and + // captures {@link AutomationSessionTemplate.customizations} entries that + // are new or whose `uri` or `nonce` changed. Session *AutomationSessionTemplate `json:"session,omitempty"` // Replacement {@link AutomationDefinition.enabled}. Enabled *bool `json:"enabled,omitempty"` @@ -4006,6 +4032,20 @@ type AutomationEntry struct { RunsNextCursor *string `json:"runsNextCursor,omitempty"` // Operations currently permitted for this automation. Operations []AutomationOperation `json:"operations"` + // Host-owned copies of the plugins in + // {@link AutomationSessionTemplate.customizations}, one per template entry + // with the same `id`. Absent when the template has no customizations. + // + // Each copy's `uri` identifies the captured contents, which clients can + // browse with `resourceRead`. `children` and `load` report what the host + // found in that copy, independent of whether the originating client is + // connected. `clientId` is absent because the copy no longer depends on a + // client. + // + // Every run session receives these plugins in + // {@link SessionState.customizations}, with the enablement from the + // matching template entry. + Customizations []PluginCustomization `json:"customizations,omitempty"` // Creation timestamp in ISO 8601 format. CreatedAt string `json:"createdAt"` // Last definition modification timestamp in ISO 8601 format. diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt index 01fb0e15e..814c54e70 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt @@ -431,7 +431,12 @@ data class AutomationCapabilities( * runs are not counted toward the limit. Absence means the retention limit is * implementation-defined. */ - val runHistoryLimit: Long? = null + val runHistoryLimit: Long? = null, + /** + * Present when {@link AutomationSessionTemplate.customizations} may contain + * client plugins for the host to capture. + */ + val customizations: AutomationCustomizationsCapability? = null ) @Serializable @@ -450,6 +455,9 @@ data class AutomationScheduleCapabilities( @Serializable class AutomationRunCancellationCapability +@Serializable +class AutomationCustomizationsCapability + @Serializable data class Implementation( /** 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 6b4d69ced..7fb225c0b 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 @@ -5385,7 +5385,33 @@ data class AutomationSessionTemplate( * {@link CreateSessionParams.config}, normally obtained from * {@link ResolveSessionConfigResult.values}. */ - val config: Map? = null + val config: Map? = null, + /** + * Client plugins to make available in every run session, in the same + * published shape as + * {@link SessionActiveClient.customizations | `activeClients[].customizations`}. + * Entries are keyed by `id`. + * + * Runs usually start when no client is connected, so the host does not + * resolve these URIs at run time. Instead, when it accepts a + * {@link AutomationCreateRequestedAction | `automation/createRequested`} or + * {@link AutomationUpdateRequestedAction | `automation/updateRequested`} + * that adds an entry or changes an entry's `uri` or `nonce`, the host + * captures a host-owned copy of the plugin. For client-served URIs such as + * `virtual://…`, it reads the contents from the dispatching client with + * server→client `resource*` requests. If a capture fails, the host rejects + * the whole action. Entries whose `id`, `uri`, and `nonce` are unchanged keep + * their existing copy, so any client can re-submit a template it received + * without being able to serve the plugin itself. The resulting copies are + * reported in {@link AutomationEntry.customizations}. + * + * The host MAY share one stored copy between entries with equal `uri` and + * `nonce`, including across automations; this is not observable to clients. + * + * Clients MUST NOT set this field unless the host advertises + * {@link AutomationCapabilities.customizations}. + */ + val customizations: List? = null ) @Serializable @@ -5432,7 +5458,9 @@ data class AutomationDefinitionPatch( val message: Message? = null, /** * Replacement {@link AutomationDefinition.session}. The host revalidates - * affected event triggers when their discovery context changes. + * affected event triggers when their discovery context changes, and + * captures {@link AutomationSessionTemplate.customizations} entries that + * are new or whose `uri` or `nonce` changed. */ val session: AutomationSessionTemplate? = null, /** @@ -5479,6 +5507,22 @@ data class AutomationEntry( * Operations currently permitted for this automation. */ val operations: List, + /** + * Host-owned copies of the plugins in + * {@link AutomationSessionTemplate.customizations}, one per template entry + * with the same `id`. Absent when the template has no customizations. + * + * Each copy's `uri` identifies the captured contents, which clients can + * browse with `resourceRead`. `children` and `load` report what the host + * found in that copy, independent of whether the originating client is + * connected. `clientId` is absent because the copy no longer depends on a + * client. + * + * Every run session receives these plugins in + * {@link SessionState.customizations}, with the enablement from the + * matching template entry. + */ + val customizations: List? = null, /** * Creation timestamp in ISO 8601 format. */ diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index 0821be697..b4f7703c4 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -2066,7 +2066,9 @@ pub struct ResourceWatchChangedAction { /// /// This side-effect request leaves optimistic catalogue state unchanged. The /// host validates trigger ids and configuration, normalizes event-trigger -/// titles and descriptions, persists the definition, then publishes the +/// titles and descriptions, captures any +/// {@link AutomationSessionTemplate.customizations | session customizations} +/// from the dispatching client, persists the definition, then publishes the /// authoritative result with {@link AutomationSetAction | `automation/set`}. /// Rejections leave the catalogue unchanged. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index e3dfe255e..e2af670a7 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -344,6 +344,10 @@ pub struct AutomationCapabilities { /// implementation-defined. #[serde(default, skip_serializing_if = "Option::is_none")] pub run_history_limit: Option, + /// Present when {@link AutomationSessionTemplate.customizations} may contain + /// client plugins for the host to capture. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub customizations: Option, } /// Presence capability for {@link AutomationCreateRequestedAction | @@ -378,6 +382,15 @@ pub struct AutomationScheduleCapabilities { #[serde(rename_all = "camelCase")] pub struct AutomationRunCancellationCapability {} +/// Presence capability for +/// {@link AutomationSessionTemplate.customizations | automation customizations}. +/// +/// The empty object means "supported"; fields are reserved for future +/// capture options and limits. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutomationCustomizationsCapability {} + /// Identifies a protocol implementation — the software (and build) on one end /// of the connection, as distinct from the {@link AgentInfo | agent persona} it /// hosts. Carried as {@link InitializeParams.clientInfo | `clientInfo`} on the diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 3af369953..0c891c782 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -3979,6 +3979,7 @@ pub struct ClientPluginCustomization { /// nothing. #[serde(default, skip_serializing_if = "Option::is_none")] pub children: Option>, + pub r#type: CustomizationType, /// Explicit enablement decisions. See {@link McpServerCustomization.enablement}. #[serde(default, skip_serializing_if = "Option::is_none")] pub enablement: Option>, @@ -5465,6 +5466,31 @@ pub struct AutomationSessionTemplate { /// {@link ResolveSessionConfigResult.values}. #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option, + /// Client plugins to make available in every run session, in the same + /// published shape as + /// {@link SessionActiveClient.customizations | `activeClients[].customizations`}. + /// Entries are keyed by `id`. + /// + /// Runs usually start when no client is connected, so the host does not + /// resolve these URIs at run time. Instead, when it accepts a + /// {@link AutomationCreateRequestedAction | `automation/createRequested`} or + /// {@link AutomationUpdateRequestedAction | `automation/updateRequested`} + /// that adds an entry or changes an entry's `uri` or `nonce`, the host + /// captures a host-owned copy of the plugin. For client-served URIs such as + /// `virtual://…`, it reads the contents from the dispatching client with + /// server→client `resource*` requests. If a capture fails, the host rejects + /// the whole action. Entries whose `id`, `uri`, and `nonce` are unchanged keep + /// their existing copy, so any client can re-submit a template it received + /// without being able to serve the plugin itself. The resulting copies are + /// reported in {@link AutomationEntry.customizations}. + /// + /// The host MAY share one stored copy between entries with equal `uri` and + /// `nonce`, including across automations; this is not observable to clients. + /// + /// Clients MUST NOT set this field unless the host advertises + /// {@link AutomationCapabilities.customizations}. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub customizations: Option>, } /// Durable, client-editable definition of an automation. @@ -5508,7 +5534,9 @@ pub struct AutomationDefinitionPatch { #[serde(default, skip_serializing_if = "Option::is_none")] pub message: Option, /// Replacement {@link AutomationDefinition.session}. The host revalidates - /// affected event triggers when their discovery context changes. + /// affected event triggers when their discovery context changes, and + /// captures {@link AutomationSessionTemplate.customizations} entries that + /// are new or whose `uri` or `nonce` changed. #[serde(default, skip_serializing_if = "Option::is_none")] pub session: Option, /// Replacement {@link AutomationDefinition.enabled}. @@ -5547,6 +5575,26 @@ pub struct AutomationEntry { pub runs_next_cursor: Option, /// Operations currently permitted for this automation. pub operations: Vec, + /// Host-owned copies of the plugins in + /// {@link AutomationSessionTemplate.customizations}, one per template entry + /// with the same `id`. Absent when the template has no customizations. + /// + /// Each copy's `uri` identifies the captured contents, which clients can + /// browse with `resourceRead`. `children` and `load` report what the host + /// found in that copy, independent of whether the originating client is + /// connected. `clientId` is absent because the copy no longer depends on a + /// client. + /// + /// Every run session receives these plugins in + /// {@link SessionState.customizations}, with the enablement from the + /// matching template entry. + #[serde( + default, + skip_serializing_if = "Option::is_none", + serialize_with = "serialize_plugin_customizations", + deserialize_with = "deserialize_plugin_customizations" + )] + pub customizations: Option>, /// Creation timestamp in ISO 8601 format. pub created_at: String, /// Last definition modification timestamp in ISO 8601 format. @@ -5556,6 +5604,56 @@ pub struct AutomationEntry { pub meta: Option, } +fn serialize_plugin_customizations( + value: &Option>, + serializer: S, +) -> Result +where + S: serde::Serializer, +{ + let Some(items) = value else { + return serializer.serialize_none(); + }; + let mut out = Vec::with_capacity(items.len()); + for item in items { + let mut raw = serde_json::to_value(item).map_err(serde::ser::Error::custom)?; + let serde_json::Value::Object(object) = &mut raw else { + return Err(serde::ser::Error::custom( + "plugin customization must serialize to an object", + )); + }; + object.insert( + "type".to_owned(), + serde_json::Value::String("plugin".to_owned()), + ); + out.push(raw); + } + serde::Serialize::serialize(&out, serializer) +} + +fn deserialize_plugin_customizations<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: serde::Deserializer<'de>, +{ + let Some(items) = Option::>::deserialize(deserializer)? else { + return Ok(None); + }; + items + .into_iter() + .map(|raw| { + if raw.get("type").and_then(serde_json::Value::as_str) != Some("plugin") { + return Err(serde::de::Error::custom( + "expected plugin customization type", + )); + } + serde_json::from_value(raw).map_err(serde::de::Error::custom) + }) + .collect::, _>>() + .map(Some) +} + /// Authoritative automation catalogue exposed on the `ahp-automations://` /// channel. /// diff --git a/clients/rust/crates/ahp/tests/hosts.rs b/clients/rust/crates/ahp/tests/hosts.rs index a3d458ca8..88399c7b8 100644 --- a/clients/rust/crates/ahp/tests/hosts.rs +++ b/clients/rust/crates/ahp/tests/hosts.rs @@ -391,6 +391,7 @@ async fn automation_capabilities_are_exposed_and_survive_reconnect() { schedules: None, run_cancellation: None, run_history_limit: Some(25), + customizations: None, }; let drop_after_init = Arc::new(AtomicBool::new(false)); let return_replay = Arc::new(Mutex::new(true)); diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index 9d9ca3e25..8b5f8d4f8 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -413,17 +413,22 @@ public struct AutomationCapabilities: Codable, Sendable { /// runs are not counted toward the limit. Absence means the retention limit is /// implementation-defined. public var runHistoryLimit: Int? + /// Present when {@link AutomationSessionTemplate.customizations} may contain + /// client plugins for the host to capture. + public var customizations: AutomationCustomizationsCapability? public init( create: AutomationCreateCapability? = nil, schedules: AutomationScheduleCapabilities? = nil, runCancellation: AutomationRunCancellationCapability? = nil, - runHistoryLimit: Int? = nil + runHistoryLimit: Int? = nil, + customizations: AutomationCustomizationsCapability? = nil ) { self.create = create self.schedules = schedules self.runCancellation = runCancellation self.runHistoryLimit = runHistoryLimit + self.customizations = customizations } } @@ -456,6 +461,14 @@ public struct AutomationRunCancellationCapability: Codable, Sendable { } } +public struct AutomationCustomizationsCapability: Codable, Sendable { + + public init( + + ) { + } +} + public struct Implementation: Codable, Sendable { /// Implementation name, e.g. a product or package identifier. public var name: String diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 21f84be01..334c1831b 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -6310,19 +6310,45 @@ public struct AutomationSessionTemplate: Codable, Sendable { /// {@link CreateSessionParams.config}, normally obtained from /// {@link ResolveSessionConfigResult.values}. public var config: [String: AnyCodable]? + /// Client plugins to make available in every run session, in the same + /// published shape as + /// {@link SessionActiveClient.customizations | `activeClients[].customizations`}. + /// Entries are keyed by `id`. + /// + /// Runs usually start when no client is connected, so the host does not + /// resolve these URIs at run time. Instead, when it accepts a + /// {@link AutomationCreateRequestedAction | `automation/createRequested`} or + /// {@link AutomationUpdateRequestedAction | `automation/updateRequested`} + /// that adds an entry or changes an entry's `uri` or `nonce`, the host + /// captures a host-owned copy of the plugin. For client-served URIs such as + /// `virtual://…`, it reads the contents from the dispatching client with + /// server→client `resource*` requests. If a capture fails, the host rejects + /// the whole action. Entries whose `id`, `uri`, and `nonce` are unchanged keep + /// their existing copy, so any client can re-submit a template it received + /// without being able to serve the plugin itself. The resulting copies are + /// reported in {@link AutomationEntry.customizations}. + /// + /// The host MAY share one stored copy between entries with equal `uri` and + /// `nonce`, including across automations; this is not observable to clients. + /// + /// Clients MUST NOT set this field unless the host advertises + /// {@link AutomationCapabilities.customizations}. + public var customizations: [ClientPluginCustomization]? public init( provider: String? = nil, model: ModelSelection? = nil, agent: AgentSelection? = nil, workingDirectories: [String]? = nil, - config: [String: AnyCodable]? = nil + config: [String: AnyCodable]? = nil, + customizations: [ClientPluginCustomization]? = nil ) { self.provider = provider self.model = model self.agent = agent self.workingDirectories = workingDirectories self.config = config + self.customizations = customizations } } @@ -6375,7 +6401,9 @@ public struct AutomationDefinitionPatch: Codable, Sendable { /// Replacement {@link AutomationDefinition.message}. public var message: Message? /// Replacement {@link AutomationDefinition.session}. The host revalidates - /// affected event triggers when their discovery context changes. + /// affected event triggers when their discovery context changes, and + /// captures {@link AutomationSessionTemplate.customizations} entries that + /// are new or whose `uri` or `nonce` changed. public var session: AutomationSessionTemplate? /// Replacement {@link AutomationDefinition.enabled}. public var enabled: Bool? @@ -6426,6 +6454,20 @@ public struct AutomationEntry: Codable, Sendable { public var runsNextCursor: String? /// Operations currently permitted for this automation. public var operations: [AutomationOperation] + /// Host-owned copies of the plugins in + /// {@link AutomationSessionTemplate.customizations}, one per template entry + /// with the same `id`. Absent when the template has no customizations. + /// + /// Each copy's `uri` identifies the captured contents, which clients can + /// browse with `resourceRead`. `children` and `load` report what the host + /// found in that copy, independent of whether the originating client is + /// connected. `clientId` is absent because the copy no longer depends on a + /// client. + /// + /// Every run session receives these plugins in + /// {@link SessionState.customizations}, with the enablement from the + /// matching template entry. + public var customizations: [PluginCustomization]? /// Creation timestamp in ISO 8601 format. public var createdAt: String /// Last definition modification timestamp in ISO 8601 format. @@ -6440,6 +6482,7 @@ public struct AutomationEntry: Codable, Sendable { case runs case runsNextCursor case operations + case customizations case createdAt case modifiedAt case meta = "_meta" @@ -6452,6 +6495,7 @@ public struct AutomationEntry: Codable, Sendable { runs: [AutomationRunSummary], runsNextCursor: String? = nil, operations: [AutomationOperation], + customizations: [PluginCustomization]? = nil, createdAt: String, modifiedAt: String, meta: [String: AnyCodable]? = nil @@ -6462,6 +6506,7 @@ public struct AutomationEntry: Codable, Sendable { self.runs = runs self.runsNextCursor = runsNextCursor self.operations = operations + self.customizations = customizations self.createdAt = createdAt self.modifiedAt = modifiedAt self.meta = meta diff --git a/docs/.changes/20260924-automation-customizations.json b/docs/.changes/20260924-automation-customizations.json new file mode 100644 index 000000000..3b93d1988 --- /dev/null +++ b/docs/.changes/20260924-automation-customizations.json @@ -0,0 +1,4 @@ +{ + "type": "added", + "message": "`AutomationSessionTemplate.customizations` lets automations carry client plugins (skills, agents, prompts, rules) that the host captures when the definition is saved, with the host-owned copies reported in `AutomationEntry.customizations` and support advertised by `AutomationCapabilities.customizations`." +} diff --git a/docs/.changes/20260924-rust-client-plugin-type.json b/docs/.changes/20260924-rust-client-plugin-type.json new file mode 100644 index 000000000..886869dbb --- /dev/null +++ b/docs/.changes/20260924-rust-client-plugin-type.json @@ -0,0 +1,5 @@ +{ + "type": "fixed", + "message": "`ClientPluginCustomization` now serializes its `\"type\": \"plugin\"` discriminant, so plugins published in `SessionActiveClient.customizations` are no longer sent without `type`.", + "targets": ["rust"] +} diff --git a/docs/guide/automations.md b/docs/guide/automations.md index 33cf71281..e8e93c1bb 100644 --- a/docs/guide/automations.md +++ b/docs/guide/automations.md @@ -35,6 +35,7 @@ AutomationCapabilities { } runCancellation?: {} runHistoryLimit?: number + customizations?: {} } ``` @@ -46,7 +47,7 @@ fields describe optional features and restrictions; clients use each automation's `operations` to determine which definition actions are currently allowed. -`create` and `runCancellation` are presence capabilities: an empty object means +`create`, `runCancellation`, and `customizations` are presence capabilities: an empty object means the feature is supported, and absence means it is not. The object shape leaves room for future feature-specific options without changing capability detection. @@ -130,6 +131,7 @@ AutomationSessionTemplate { agent?: AgentSelection workingDirectories?: URI[] config?: Record + customizations?: ClientPluginCustomization[] } ``` @@ -143,6 +145,53 @@ After a run creates a session, that session's `SessionState.workingDirectories` is authoritative for the directories it actually uses. This keeps per-run workspace preparation out of the durable automation definition and catalogue. +### Customizations + +A client usually contributes plugins (skills, agents, prompts, rules, and so +on) to a session as an [active client](./customizations.md#client-published-plugins). +That doesn't work for automations: runs typically start when no client is +connected, so there is no active client to read the plugins from. The session +template instead lists the plugins each run should get, in the same +`ClientPluginCustomization` shape clients publish with +`session/activeClientSet`. Hosts that support this advertise +`automations.customizations`. + +The host captures a copy of each plugin when the definition is saved, not +when a run starts: + +```mermaid +sequenceDiagram + participant Client + participant Host + + Client->>Host: automation/createRequested (session.customizations) + Host->>Client: resourceList / resourceRead (virtual://…) + Client-->>Host: plugin contents + Note over Host: stores a host-owned copy + Host->>Client: automation/set (entry.customizations) + Note over Host: later, with no client connected + Host->>Host: run session gets the copied plugins +``` + +- On `automation/createRequested` or `automation/updateRequested`, the host + captures every entry that is new or whose `uri` or `nonce` changed, reading + client-served URIs from the dispatching client. If any capture fails, the + host rejects the whole action. +- Entries whose `id`, `uri`, and `nonce` are unchanged keep their existing + copy. A client that can't serve a plugin can still edit the rest of the + definition by re-submitting the template it received. +- `AutomationEntry.customizations` reports the host-owned copies, matched to + template entries by `id`. Each copy has a host `uri` that clients can browse, + plus `children` and `load` describing what the host found. +- Every run session receives the copies in `SessionState.customizations` + without a `clientId`, using the template entry's enablement. +- To pick up local changes, a client compares its current `nonce` with the + template entry and re-submits the entry with the new `nonce`. Hosts never + refresh copies on their own, so unattended runs use exactly what the user + saved. +- Copies are per automation. Hosts may store identical copies (same `uri` + and `nonce`) once and share them between automations. + ### Enabled state `enabled` controls automatic triggers only. A disabled automation can still be @@ -471,6 +520,8 @@ applications. ## Security - Definitions contain no credentials or reusable confirmation decisions. + Captured customizations follow the same rule; they carry plugin content, + not secrets. - The host revalidates provider, model, agent, workspace, and session configuration when each run starts. - State-level operations are authoritative; clients do not infer permission diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 7f86b4a2f..9944c02c3 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -1849,7 +1849,7 @@ }, "ChangesetStatusChangedAction": { "type": "object", - "description": "The {@link ChangesetState.status} for this changeset transitioned (e.g.\n`computing → ready` or `ready → recomputing`). The error payload is set\ntogether with `status` whenever it transitions to\n{@link ChangesetStatus.Error | Error}.", + "description": "The {@link ChangesetState.status} for this changeset transitioned (e.g.\n`computing → ready` or `recomputing → ready`). The error payload is set\ntogether with `status` whenever it transitions to\n{@link ChangesetStatus.Error | Error}.", "properties": { "type": { "const": "changeset/statusChanged" @@ -2164,7 +2164,7 @@ }, "session": { "$ref": "#/$defs/AutomationSessionTemplate", - "description": "Replacement {@link AutomationDefinition.session}. The host revalidates\naffected event triggers when their discovery context changes." + "description": "Replacement {@link AutomationDefinition.session}. The host revalidates\naffected event triggers when their discovery context changes, and\ncaptures {@link AutomationSessionTemplate.customizations} entries that\nare new or whose `uri` or `nonce` changed." }, "enabled": { "type": "boolean", @@ -2186,7 +2186,7 @@ }, "AutomationCreateRequestedAction": { "type": "object", - "description": "Ask the host to create a durable automation at a client-chosen resource.\n\nClients may dispatch this action only when the host advertises its `create`\nautomation capability. {@link AutomationCreateRequestedAction.resource |\n`resource`} MUST use the `ahp-automation:` scheme and MUST NOT already\nidentify an unrelated automation.\n\nThis side-effect request leaves optimistic catalogue state unchanged. The\nhost validates trigger ids and configuration, normalizes event-trigger\ntitles and descriptions, persists the definition, then publishes the\nauthoritative result with {@link AutomationSetAction | `automation/set`}.\nRejections leave the catalogue unchanged.", + "description": "Ask the host to create a durable automation at a client-chosen resource.\n\nClients may dispatch this action only when the host advertises its `create`\nautomation capability. {@link AutomationCreateRequestedAction.resource |\n`resource`} MUST use the `ahp-automation:` scheme and MUST NOT already\nidentify an unrelated automation.\n\nThis side-effect request leaves optimistic catalogue state unchanged. The\nhost validates trigger ids and configuration, normalizes event-trigger\ntitles and descriptions, captures any\n{@link AutomationSessionTemplate.customizations | session customizations}\nfrom the dispatching client, persists the definition, then publishes the\nauthoritative result with {@link AutomationSetAction | `automation/set`}.\nRejections leave the catalogue unchanged.", "properties": { "type": { "const": "automation/createRequested" @@ -7650,6 +7650,13 @@ "type": "object", "additionalProperties": {}, "description": "Session configuration values equivalent to\n{@link CreateSessionParams.config}, normally obtained from\n{@link ResolveSessionConfigResult.values}." + }, + "customizations": { + "type": "array", + "items": { + "$ref": "#/$defs/ClientPluginCustomization" + }, + "description": "Client plugins to make available in every run session, in the same\npublished shape as\n{@link SessionActiveClient.customizations | `activeClients[].customizations`}.\nEntries are keyed by `id`.\n\nRuns usually start when no client is connected, so the host does not\nresolve these URIs at run time. Instead, when it accepts a\n{@link AutomationCreateRequestedAction | `automation/createRequested`} or\n{@link AutomationUpdateRequestedAction | `automation/updateRequested`}\nthat adds an entry or changes an entry's `uri` or `nonce`, the host\ncaptures a host-owned copy of the plugin. For client-served URIs such as\n`virtual://…`, it reads the contents from the dispatching client with\nserver→client `resource*` requests. If a capture fails, the host rejects\nthe whole action. Entries whose `id`, `uri`, and `nonce` are unchanged keep\ntheir existing copy, so any client can re-submit a template it received\nwithout being able to serve the plugin itself. The resulting copies are\nreported in {@link AutomationEntry.customizations}.\n\nThe host MAY share one stored copy between entries with equal `uri` and\n`nonce`, including across automations; this is not observable to clients.\n\nClients MUST NOT set this field unless the host advertises\n{@link AutomationCapabilities.customizations}." } } }, @@ -7728,6 +7735,13 @@ }, "description": "Operations currently permitted for this automation." }, + "customizations": { + "type": "array", + "items": { + "$ref": "#/$defs/PluginCustomization" + }, + "description": "Host-owned copies of the plugins in\n{@link AutomationSessionTemplate.customizations}, one per template entry\nwith the same `id`. Absent when the template has no customizations.\n\nEach copy's `uri` identifies the captured contents, which clients can\nbrowse with `resourceRead`. `children` and `load` report what the host\nfound in that copy, independent of whether the originating client is\nconnected. `clientId` is absent because the copy no longer depends on a\nclient.\n\nEvery run session receives these plugins in\n{@link SessionState.customizations}, with the enablement from the\nmatching template entry." + }, "createdAt": { "type": "string", "description": "Creation timestamp in ISO 8601 format." diff --git a/schema/commands.schema.json b/schema/commands.schema.json index d175d2af0..d659a1fd7 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -208,9 +208,18 @@ "runHistoryLimit": { "type": "number", "description": "Maximum terminal entries retained in {@link AutomationEntry.runs}. Active\nruns are not counted toward the limit. Absence means the retention limit is\nimplementation-defined." + }, + "customizations": { + "$ref": "#/$defs/AutomationCustomizationsCapability", + "description": "Present when {@link AutomationSessionTemplate.customizations} may contain\nclient plugins for the host to capture." } } }, + "AutomationCustomizationsCapability": { + "type": "object", + "description": "Presence capability for\n{@link AutomationSessionTemplate.customizations | automation customizations}.\n\nThe empty object means \"supported\"; fields are reserved for future\ncapture options and limits.", + "properties": {} + }, "AutomationCreateCapability": { "type": "object", "description": "Presence capability for {@link AutomationCreateRequestedAction |\n`automation/createRequested`}.\n\nThe empty object means \"supported\"; fields are reserved for future\ncreate-specific options.", @@ -6863,6 +6872,13 @@ "type": "object", "additionalProperties": {}, "description": "Session configuration values equivalent to\n{@link CreateSessionParams.config}, normally obtained from\n{@link ResolveSessionConfigResult.values}." + }, + "customizations": { + "type": "array", + "items": { + "$ref": "#/$defs/ClientPluginCustomization" + }, + "description": "Client plugins to make available in every run session, in the same\npublished shape as\n{@link SessionActiveClient.customizations | `activeClients[].customizations`}.\nEntries are keyed by `id`.\n\nRuns usually start when no client is connected, so the host does not\nresolve these URIs at run time. Instead, when it accepts a\n{@link AutomationCreateRequestedAction | `automation/createRequested`} or\n{@link AutomationUpdateRequestedAction | `automation/updateRequested`}\nthat adds an entry or changes an entry's `uri` or `nonce`, the host\ncaptures a host-owned copy of the plugin. For client-served URIs such as\n`virtual://…`, it reads the contents from the dispatching client with\nserver→client `resource*` requests. If a capture fails, the host rejects\nthe whole action. Entries whose `id`, `uri`, and `nonce` are unchanged keep\ntheir existing copy, so any client can re-submit a template it received\nwithout being able to serve the plugin itself. The resulting copies are\nreported in {@link AutomationEntry.customizations}.\n\nThe host MAY share one stored copy between entries with equal `uri` and\n`nonce`, including across automations; this is not observable to clients.\n\nClients MUST NOT set this field unless the host advertises\n{@link AutomationCapabilities.customizations}." } } }, @@ -6941,6 +6957,13 @@ }, "description": "Operations currently permitted for this automation." }, + "customizations": { + "type": "array", + "items": { + "$ref": "#/$defs/PluginCustomization" + }, + "description": "Host-owned copies of the plugins in\n{@link AutomationSessionTemplate.customizations}, one per template entry\nwith the same `id`. Absent when the template has no customizations.\n\nEach copy's `uri` identifies the captured contents, which clients can\nbrowse with `resourceRead`. `children` and `load` report what the host\nfound in that copy, independent of whether the originating client is\nconnected. `clientId` is absent because the copy no longer depends on a\nclient.\n\nEvery run session receives these plugins in\n{@link SessionState.customizations}, with the enablement from the\nmatching template entry." + }, "createdAt": { "type": "string", "description": "Creation timestamp in ISO 8601 format." @@ -9085,7 +9108,7 @@ }, "ChangesetStatusChangedAction": { "type": "object", - "description": "The {@link ChangesetState.status} for this changeset transitioned (e.g.\n`computing → ready` or `ready → recomputing`). The error payload is set\ntogether with `status` whenever it transitions to\n{@link ChangesetStatus.Error | Error}.", + "description": "The {@link ChangesetState.status} for this changeset transitioned (e.g.\n`computing → ready` or `recomputing → ready`). The error payload is set\ntogether with `status` whenever it transitions to\n{@link ChangesetStatus.Error | Error}.", "properties": { "type": { "const": "changeset/statusChanged" @@ -9400,7 +9423,7 @@ }, "session": { "$ref": "#/$defs/AutomationSessionTemplate", - "description": "Replacement {@link AutomationDefinition.session}. The host revalidates\naffected event triggers when their discovery context changes." + "description": "Replacement {@link AutomationDefinition.session}. The host revalidates\naffected event triggers when their discovery context changes, and\ncaptures {@link AutomationSessionTemplate.customizations} entries that\nare new or whose `uri` or `nonce` changed." }, "enabled": { "type": "boolean", @@ -9422,7 +9445,7 @@ }, "AutomationCreateRequestedAction": { "type": "object", - "description": "Ask the host to create a durable automation at a client-chosen resource.\n\nClients may dispatch this action only when the host advertises its `create`\nautomation capability. {@link AutomationCreateRequestedAction.resource |\n`resource`} MUST use the `ahp-automation:` scheme and MUST NOT already\nidentify an unrelated automation.\n\nThis side-effect request leaves optimistic catalogue state unchanged. The\nhost validates trigger ids and configuration, normalizes event-trigger\ntitles and descriptions, persists the definition, then publishes the\nauthoritative result with {@link AutomationSetAction | `automation/set`}.\nRejections leave the catalogue unchanged.", + "description": "Ask the host to create a durable automation at a client-chosen resource.\n\nClients may dispatch this action only when the host advertises its `create`\nautomation capability. {@link AutomationCreateRequestedAction.resource |\n`resource`} MUST use the `ahp-automation:` scheme and MUST NOT already\nidentify an unrelated automation.\n\nThis side-effect request leaves optimistic catalogue state unchanged. The\nhost validates trigger ids and configuration, normalizes event-trigger\ntitles and descriptions, captures any\n{@link AutomationSessionTemplate.customizations | session customizations}\nfrom the dispatching client, persists the definition, then publishes the\nauthoritative result with {@link AutomationSetAction | `automation/set`}.\nRejections leave the catalogue unchanged.", "properties": { "type": { "const": "automation/createRequested" diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 494996e05..b27d24496 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -5292,6 +5292,13 @@ "type": "object", "additionalProperties": {}, "description": "Session configuration values equivalent to\n{@link CreateSessionParams.config}, normally obtained from\n{@link ResolveSessionConfigResult.values}." + }, + "customizations": { + "type": "array", + "items": { + "$ref": "#/$defs/ClientPluginCustomization" + }, + "description": "Client plugins to make available in every run session, in the same\npublished shape as\n{@link SessionActiveClient.customizations | `activeClients[].customizations`}.\nEntries are keyed by `id`.\n\nRuns usually start when no client is connected, so the host does not\nresolve these URIs at run time. Instead, when it accepts a\n{@link AutomationCreateRequestedAction | `automation/createRequested`} or\n{@link AutomationUpdateRequestedAction | `automation/updateRequested`}\nthat adds an entry or changes an entry's `uri` or `nonce`, the host\ncaptures a host-owned copy of the plugin. For client-served URIs such as\n`virtual://…`, it reads the contents from the dispatching client with\nserver→client `resource*` requests. If a capture fails, the host rejects\nthe whole action. Entries whose `id`, `uri`, and `nonce` are unchanged keep\ntheir existing copy, so any client can re-submit a template it received\nwithout being able to serve the plugin itself. The resulting copies are\nreported in {@link AutomationEntry.customizations}.\n\nThe host MAY share one stored copy between entries with equal `uri` and\n`nonce`, including across automations; this is not observable to clients.\n\nClients MUST NOT set this field unless the host advertises\n{@link AutomationCapabilities.customizations}." } } }, @@ -5370,6 +5377,13 @@ }, "description": "Operations currently permitted for this automation." }, + "customizations": { + "type": "array", + "items": { + "$ref": "#/$defs/PluginCustomization" + }, + "description": "Host-owned copies of the plugins in\n{@link AutomationSessionTemplate.customizations}, one per template entry\nwith the same `id`. Absent when the template has no customizations.\n\nEach copy's `uri` identifies the captured contents, which clients can\nbrowse with `resourceRead`. `children` and `load` report what the host\nfound in that copy, independent of whether the originating client is\nconnected. `clientId` is absent because the copy no longer depends on a\nclient.\n\nEvery run session receives these plugins in\n{@link SessionState.customizations}, with the enablement from the\nmatching template entry." + }, "createdAt": { "type": "string", "description": "Creation timestamp in ISO 8601 format." @@ -5873,9 +5887,18 @@ "runHistoryLimit": { "type": "number", "description": "Maximum terminal entries retained in {@link AutomationEntry.runs}. Active\nruns are not counted toward the limit. Absence means the retention limit is\nimplementation-defined." + }, + "customizations": { + "$ref": "#/$defs/AutomationCustomizationsCapability", + "description": "Present when {@link AutomationSessionTemplate.customizations} may contain\nclient plugins for the host to capture." } } }, + "AutomationCustomizationsCapability": { + "type": "object", + "description": "Presence capability for\n{@link AutomationSessionTemplate.customizations | automation customizations}.\n\nThe empty object means \"supported\"; fields are reserved for future\ncapture options and limits.", + "properties": {} + }, "AutomationCreateCapability": { "type": "object", "description": "Presence capability for {@link AutomationCreateRequestedAction |\n`automation/createRequested`}.\n\nThe empty object means \"supported\"; fields are reserved for future\ncreate-specific options.", @@ -9974,7 +9997,7 @@ }, "ChangesetStatusChangedAction": { "type": "object", - "description": "The {@link ChangesetState.status} for this changeset transitioned (e.g.\n`computing → ready` or `ready → recomputing`). The error payload is set\ntogether with `status` whenever it transitions to\n{@link ChangesetStatus.Error | Error}.", + "description": "The {@link ChangesetState.status} for this changeset transitioned (e.g.\n`computing → ready` or `recomputing → ready`). The error payload is set\ntogether with `status` whenever it transitions to\n{@link ChangesetStatus.Error | Error}.", "properties": { "type": { "const": "changeset/statusChanged" @@ -10476,7 +10499,7 @@ }, "AutomationCreateRequestedAction": { "type": "object", - "description": "Ask the host to create a durable automation at a client-chosen resource.\n\nClients may dispatch this action only when the host advertises its `create`\nautomation capability. {@link AutomationCreateRequestedAction.resource |\n`resource`} MUST use the `ahp-automation:` scheme and MUST NOT already\nidentify an unrelated automation.\n\nThis side-effect request leaves optimistic catalogue state unchanged. The\nhost validates trigger ids and configuration, normalizes event-trigger\ntitles and descriptions, persists the definition, then publishes the\nauthoritative result with {@link AutomationSetAction | `automation/set`}.\nRejections leave the catalogue unchanged.", + "description": "Ask the host to create a durable automation at a client-chosen resource.\n\nClients may dispatch this action only when the host advertises its `create`\nautomation capability. {@link AutomationCreateRequestedAction.resource |\n`resource`} MUST use the `ahp-automation:` scheme and MUST NOT already\nidentify an unrelated automation.\n\nThis side-effect request leaves optimistic catalogue state unchanged. The\nhost validates trigger ids and configuration, normalizes event-trigger\ntitles and descriptions, captures any\n{@link AutomationSessionTemplate.customizations | session customizations}\nfrom the dispatching client, persists the definition, then publishes the\nauthoritative result with {@link AutomationSetAction | `automation/set`}.\nRejections leave the catalogue unchanged.", "properties": { "type": { "const": "automation/createRequested" @@ -10752,7 +10775,7 @@ }, "session": { "$ref": "#/$defs/AutomationSessionTemplate", - "description": "Replacement {@link AutomationDefinition.session}. The host revalidates\naffected event triggers when their discovery context changes." + "description": "Replacement {@link AutomationDefinition.session}. The host revalidates\naffected event triggers when their discovery context changes, and\ncaptures {@link AutomationSessionTemplate.customizations} entries that\nare new or whose `uri` or `nonce` changed." }, "enabled": { "type": "boolean", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index fcccf5fa6..11cf7189a 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -5470,6 +5470,13 @@ "type": "object", "additionalProperties": {}, "description": "Session configuration values equivalent to\n{@link CreateSessionParams.config}, normally obtained from\n{@link ResolveSessionConfigResult.values}." + }, + "customizations": { + "type": "array", + "items": { + "$ref": "#/$defs/ClientPluginCustomization" + }, + "description": "Client plugins to make available in every run session, in the same\npublished shape as\n{@link SessionActiveClient.customizations | `activeClients[].customizations`}.\nEntries are keyed by `id`.\n\nRuns usually start when no client is connected, so the host does not\nresolve these URIs at run time. Instead, when it accepts a\n{@link AutomationCreateRequestedAction | `automation/createRequested`} or\n{@link AutomationUpdateRequestedAction | `automation/updateRequested`}\nthat adds an entry or changes an entry's `uri` or `nonce`, the host\ncaptures a host-owned copy of the plugin. For client-served URIs such as\n`virtual://…`, it reads the contents from the dispatching client with\nserver→client `resource*` requests. If a capture fails, the host rejects\nthe whole action. Entries whose `id`, `uri`, and `nonce` are unchanged keep\ntheir existing copy, so any client can re-submit a template it received\nwithout being able to serve the plugin itself. The resulting copies are\nreported in {@link AutomationEntry.customizations}.\n\nThe host MAY share one stored copy between entries with equal `uri` and\n`nonce`, including across automations; this is not observable to clients.\n\nClients MUST NOT set this field unless the host advertises\n{@link AutomationCapabilities.customizations}." } } }, @@ -5548,6 +5555,13 @@ }, "description": "Operations currently permitted for this automation." }, + "customizations": { + "type": "array", + "items": { + "$ref": "#/$defs/PluginCustomization" + }, + "description": "Host-owned copies of the plugins in\n{@link AutomationSessionTemplate.customizations}, one per template entry\nwith the same `id`. Absent when the template has no customizations.\n\nEach copy's `uri` identifies the captured contents, which clients can\nbrowse with `resourceRead`. `children` and `load` report what the host\nfound in that copy, independent of whether the originating client is\nconnected. `clientId` is absent because the copy no longer depends on a\nclient.\n\nEvery run session receives these plugins in\n{@link SessionState.customizations}, with the enablement from the\nmatching template entry." + }, "createdAt": { "type": "string", "description": "Creation timestamp in ISO 8601 format." diff --git a/schema/state.schema.json b/schema/state.schema.json index 8d20ca0b8..24e851b2e 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -5203,6 +5203,13 @@ "type": "object", "additionalProperties": {}, "description": "Session configuration values equivalent to\n{@link CreateSessionParams.config}, normally obtained from\n{@link ResolveSessionConfigResult.values}." + }, + "customizations": { + "type": "array", + "items": { + "$ref": "#/$defs/ClientPluginCustomization" + }, + "description": "Client plugins to make available in every run session, in the same\npublished shape as\n{@link SessionActiveClient.customizations | `activeClients[].customizations`}.\nEntries are keyed by `id`.\n\nRuns usually start when no client is connected, so the host does not\nresolve these URIs at run time. Instead, when it accepts a\n{@link AutomationCreateRequestedAction | `automation/createRequested`} or\n{@link AutomationUpdateRequestedAction | `automation/updateRequested`}\nthat adds an entry or changes an entry's `uri` or `nonce`, the host\ncaptures a host-owned copy of the plugin. For client-served URIs such as\n`virtual://…`, it reads the contents from the dispatching client with\nserver→client `resource*` requests. If a capture fails, the host rejects\nthe whole action. Entries whose `id`, `uri`, and `nonce` are unchanged keep\ntheir existing copy, so any client can re-submit a template it received\nwithout being able to serve the plugin itself. The resulting copies are\nreported in {@link AutomationEntry.customizations}.\n\nThe host MAY share one stored copy between entries with equal `uri` and\n`nonce`, including across automations; this is not observable to clients.\n\nClients MUST NOT set this field unless the host advertises\n{@link AutomationCapabilities.customizations}." } } }, @@ -5281,6 +5288,13 @@ }, "description": "Operations currently permitted for this automation." }, + "customizations": { + "type": "array", + "items": { + "$ref": "#/$defs/PluginCustomization" + }, + "description": "Host-owned copies of the plugins in\n{@link AutomationSessionTemplate.customizations}, one per template entry\nwith the same `id`. Absent when the template has no customizations.\n\nEach copy's `uri` identifies the captured contents, which clients can\nbrowse with `resourceRead`. `children` and `load` report what the host\nfound in that copy, independent of whether the originating client is\nconnected. `clientId` is absent because the copy no longer depends on a\nclient.\n\nEvery run session receives these plugins in\n{@link SessionState.customizations}, with the enablement from the\nmatching template entry." + }, "createdAt": { "type": "string", "description": "Creation timestamp in ISO 8601 format." diff --git a/scripts/generate-csharp.ts b/scripts/generate-csharp.ts index 74cb5a193..41845e1d9 100644 --- a/scripts/generate-csharp.ts +++ b/scripts/generate-csharp.ts @@ -2090,6 +2090,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; csName?: str { name: 'AutomationCreateCapability' }, { name: 'AutomationScheduleCapabilities' }, { name: 'AutomationRunCancellationCapability' }, + { name: 'AutomationCustomizationsCapability' }, { name: 'ReconnectParams' }, // Union variants MUST self-carry their `type` discriminator: UnionConverter.Write // serializes the inner value by its runtime type and relies on that property to diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 29ce47eaf..d70f1ae55 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -1725,6 +1725,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: str { name: 'AutomationCreateCapability' }, { name: 'AutomationScheduleCapabilities' }, { name: 'AutomationRunCancellationCapability' }, + { name: 'AutomationCustomizationsCapability' }, { name: 'Implementation' }, { name: 'ReconnectParams' }, { name: 'ReconnectReplayResult', omitDiscriminants: true }, diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index ef9317c62..8bf59feae 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -1728,6 +1728,7 @@ const COMMAND_STRUCTS = [ 'AutomationCreateCapability', 'AutomationScheduleCapabilities', 'AutomationRunCancellationCapability', + 'AutomationCustomizationsCapability', 'Implementation', 'ReconnectParams', 'ReconnectReplayResult', 'ReconnectSnapshotResult', 'SubscribeParams', 'SubscribeView', 'SubscriptionDeliveryOptions', 'SubscribeResult', diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index f22b5eb39..600e63928 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -609,6 +609,10 @@ function generateRustStruct(rustName: string, props: RustProp[], opts: StructOpt attrs.push('serialize_with = "serialize_running_tool_call"'); attrs.push('deserialize_with = "deserialize_running_tool_call"'); } + if (rustName === 'AutomationEntry' && p.rustName === 'customizations') { + attrs.push('serialize_with = "serialize_plugin_customizations"'); + attrs.push('deserialize_with = "deserialize_plugin_customizations"'); + } if (attrs.length > 0) { lines.push(` #[serde(${attrs.join(', ')})]`); } @@ -653,6 +657,56 @@ where }`; } +/** + * `PluginCustomization` omits its `type` discriminant because the + * `Customization` enum supplies it. Standalone plugin lists outside that enum + * (currently `AutomationEntry.customizations`) must restore it on the wire. + */ +function generatePluginCustomizationsSerdeHelpers(): string { + return `fn serialize_plugin_customizations( + value: &Option>, + serializer: S, +) -> Result +where + S: serde::Serializer, +{ + let Some(items) = value else { + return serializer.serialize_none(); + }; + let mut out = Vec::with_capacity(items.len()); + for item in items { + let mut raw = serde_json::to_value(item).map_err(serde::ser::Error::custom)?; + let serde_json::Value::Object(object) = &mut raw else { + return Err(serde::ser::Error::custom("plugin customization must serialize to an object")); + }; + object.insert("type".to_owned(), serde_json::Value::String("plugin".to_owned())); + out.push(raw); + } + serde::Serialize::serialize(&out, serializer) +} + +fn deserialize_plugin_customizations<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: serde::Deserializer<'de>, +{ + let Some(items) = Option::>::deserialize(deserializer)? else { + return Ok(None); + }; + items + .into_iter() + .map(|raw| { + if raw.get("type").and_then(serde_json::Value::as_str) != Some("plugin") { + return Err(serde::de::Error::custom("expected plugin customization type")); + } + serde_json::from_value(raw).map_err(serde::de::Error::custom) + }) + .collect::, _>>() + .map(Some) +}`; +} + // ─── Partial Struct Generation ─────────────────────────────────────────────── function generatePartialStruct(project: Project, tsInterfaceName: string): string { @@ -880,7 +934,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str { name: 'CustomizationDegradedState', omitDiscriminants: true }, { name: 'CustomizationErrorState', omitDiscriminants: true }, { name: 'PluginCustomization', omitDiscriminants: true }, - { name: 'ClientPluginCustomization', omitDiscriminants: true }, + { name: 'ClientPluginCustomization' }, { name: 'DirectoryCustomization', omitDiscriminants: true }, { name: 'AgentCustomization', omitDiscriminants: true }, { name: 'SkillCustomization', omitDiscriminants: true }, @@ -1340,6 +1394,10 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateRunningToolCallSerdeHelpers()); } + if (entry.name === 'AutomationEntry') { + lines.push(''); + lines.push(generatePluginCustomizationsSerdeHelpers()); + } if (entry.name === 'SubscribeParams') { lines.push(''); lines.push(generateSubscribeParamsImplRust()); @@ -1708,6 +1766,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: s { name: 'AutomationCreateCapability' }, { name: 'AutomationScheduleCapabilities' }, { name: 'AutomationRunCancellationCapability' }, + { name: 'AutomationCustomizationsCapability' }, { name: 'Implementation' }, { name: 'ReconnectParams' }, { name: 'ReconnectReplayResult', omitDiscriminants: true }, diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 0bcbae105..cbbae24c5 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -1634,6 +1634,7 @@ const COMMAND_STRUCTS = [ 'AutomationCreateCapability', 'AutomationScheduleCapabilities', 'AutomationRunCancellationCapability', + 'AutomationCustomizationsCapability', 'Implementation', 'ReconnectParams', 'ReconnectReplayResult', 'ReconnectSnapshotResult', 'SubscribeParams', 'SubscribeView', 'SubscriptionDeliveryOptions', 'SubscribeResult', diff --git a/types/channels-automation/actions.ts b/types/channels-automation/actions.ts index 7c48eeb33..d61aecc58 100644 --- a/types/channels-automation/actions.ts +++ b/types/channels-automation/actions.ts @@ -32,7 +32,9 @@ export interface AutomationDefinitionPatch { message?: Message; /** * Replacement {@link AutomationDefinition.session}. The host revalidates - * affected event triggers when their discovery context changes. + * affected event triggers when their discovery context changes, and + * captures {@link AutomationSessionTemplate.customizations} entries that + * are new or whose `uri` or `nonce` changed. */ session?: AutomationSessionTemplate; /** Replacement {@link AutomationDefinition.enabled}. */ @@ -56,7 +58,9 @@ export interface AutomationDefinitionPatch { * * This side-effect request leaves optimistic catalogue state unchanged. The * host validates trigger ids and configuration, normalizes event-trigger - * titles and descriptions, persists the definition, then publishes the + * titles and descriptions, captures any + * {@link AutomationSessionTemplate.customizations | session customizations} + * from the dispatching client, persists the definition, then publishes the * authoritative result with {@link AutomationSetAction | `automation/set`}. * Rejections leave the catalogue unchanged. * diff --git a/types/channels-automation/state.ts b/types/channels-automation/state.ts index d77912f15..666ac8bd5 100644 --- a/types/channels-automation/state.ts +++ b/types/channels-automation/state.ts @@ -15,8 +15,16 @@ import type { import type { ResolveSessionConfigResult } from '../channels-root/commands.js'; import type { AgentInfo, ModelSelection, SessionModelInfo } from '../channels-root/state.js'; import type { CreateSessionParams } from '../channels-session/commands.js'; -import type { AgentSelection } from '../channels-session/state.js'; import type { + AgentSelection, + ClientPluginCustomization, + PluginCustomization, + SessionActiveClient, + SessionState, +} from '../channels-session/state.js'; +import type { AutomationCapabilities } from '../common/commands.js'; +import type { + AutomationCreateRequestedAction, AutomationRemovedAction, AutomationSetAction, AutomationUpdateRequestedAction, @@ -258,6 +266,32 @@ export interface AutomationSessionTemplate { * {@link ResolveSessionConfigResult.values}. */ config?: Record; + /** + * Client plugins to make available in every run session, in the same + * published shape as + * {@link SessionActiveClient.customizations | `activeClients[].customizations`}. + * Entries are keyed by `id`. + * + * Runs usually start when no client is connected, so the host does not + * resolve these URIs at run time. Instead, when it accepts a + * {@link AutomationCreateRequestedAction | `automation/createRequested`} or + * {@link AutomationUpdateRequestedAction | `automation/updateRequested`} + * that adds an entry or changes an entry's `uri` or `nonce`, the host + * captures a host-owned copy of the plugin. For client-served URIs such as + * `virtual://…`, it reads the contents from the dispatching client with + * server→client `resource*` requests. If a capture fails, the host rejects + * the whole action. Entries whose `id`, `uri`, and `nonce` are unchanged keep + * their existing copy, so any client can re-submit a template it received + * without being able to serve the plugin itself. The resulting copies are + * reported in {@link AutomationEntry.customizations}. + * + * The host MAY share one stored copy between entries with equal `uri` and + * `nonce`, including across automations; this is not observable to clients. + * + * Clients MUST NOT set this field unless the host advertises + * {@link AutomationCapabilities.customizations}. + */ + customizations?: ClientPluginCustomization[]; } /** @@ -320,6 +354,22 @@ export interface AutomationEntry { runsNextCursor?: string; /** Operations currently permitted for this automation. */ operations: AutomationOperation[]; + /** + * Host-owned copies of the plugins in + * {@link AutomationSessionTemplate.customizations}, one per template entry + * with the same `id`. Absent when the template has no customizations. + * + * Each copy's `uri` identifies the captured contents, which clients can + * browse with `resourceRead`. `children` and `load` report what the host + * found in that copy, independent of whether the originating client is + * connected. `clientId` is absent because the copy no longer depends on a + * client. + * + * Every run session receives these plugins in + * {@link SessionState.customizations}, with the enablement from the + * matching template entry. + */ + customizations?: PluginCustomization[]; /** Creation timestamp in ISO 8601 format. */ createdAt: string; /** Last definition modification timestamp in ISO 8601 format. */ diff --git a/types/common/commands.ts b/types/common/commands.ts index d2bff1e86..b7adb7c60 100644 --- a/types/common/commands.ts +++ b/types/common/commands.ts @@ -14,6 +14,7 @@ import type { AutomationCreateRequestedAction } from '../channels-automation/act import type { AutomationSchedule, AutomationScheduleTrigger, + AutomationSessionTemplate, AutomationEntry, AutomationState, } from '../channels-automation/state.js'; @@ -324,8 +325,24 @@ export interface AutomationCapabilities { * implementation-defined. */ runHistoryLimit?: number; + /** + * Present when {@link AutomationSessionTemplate.customizations} may contain + * client plugins for the host to capture. + */ + customizations?: AutomationCustomizationsCapability; } +/** + * Presence capability for + * {@link AutomationSessionTemplate.customizations | automation customizations}. + * + * The empty object means "supported"; fields are reserved for future + * capture options and limits. + * + * @category Commands + */ +export interface AutomationCustomizationsCapability {} + /** * Presence capability for {@link AutomationCreateRequestedAction | * `automation/createRequested`}. diff --git a/types/test-cases/round-trips/043-automation-capabilities.json b/types/test-cases/round-trips/043-automation-capabilities.json index c3284921d..6d4833129 100644 --- a/types/test-cases/round-trips/043-automation-capabilities.json +++ b/types/test-cases/round-trips/043-automation-capabilities.json @@ -1,7 +1,7 @@ { "name": "automation-capabilities", "group": "A", - "description": "Automation capability markers preserve present empty objects and nested schedule configuration.", + "description": "Automation capability markers preserve present empty objects, including customizations, and nested schedule configuration.", "type": "InitializeResult", "input": { "protocolVersion": "0.8.0", @@ -13,7 +13,8 @@ "minIntervalMinutes": 5 }, "runCancellation": {}, - "runHistoryLimit": 50 + "runHistoryLimit": 50, + "customizations": {} } }, "acceptableOutputs": [{ @@ -26,7 +27,8 @@ "minIntervalMinutes": 5 }, "runCancellation": {}, - "runHistoryLimit": 50 + "runHistoryLimit": 50, + "customizations": {} } }] } diff --git a/types/test-cases/round-trips/050-automation-customizations-snapshot.json b/types/test-cases/round-trips/050-automation-customizations-snapshot.json new file mode 100644 index 000000000..21c2de3a4 --- /dev/null +++ b/types/test-cases/round-trips/050-automation-customizations-snapshot.json @@ -0,0 +1,182 @@ +{ + "name": "automation-customizations-snapshot", + "group": "A", + "description": "Automation session-template client plugins and their host-owned captured copies round-trip.", + "type": "Snapshot", + "input": { + "resource": "ahp-automations://", + "state": { + "entries": [ + { + "resource": "ahp-automation:/a2", + "definition": { + "title": "Weekly report", + "message": { + "text": "Write the weekly report", + "origin": { + "kind": "automation" + } + }, + "session": { + "provider": "copilot", + "customizations": [ + { + "type": "plugin", + "id": "client-plugin-1", + "uri": "virtual://vscode/workspace-skills", + "name": "Workspace Skills", + "enablement": [ + { + "kind": "global", + "enabled": true + } + ], + "childEnablement": { + "report-writer": [ + { + "kind": "global", + "enabled": false + } + ] + }, + "nonce": "sha256:abc123" + } + ] + }, + "enabled": true, + "triggers": [] + }, + "runs": [], + "operations": [ + "update", + "remove", + "run" + ], + "customizations": [ + { + "type": "plugin", + "id": "client-plugin-1", + "uri": "ahp-automation:/a2/customizations/client-plugin-1", + "name": "Workspace Skills", + "enablement": [ + { + "kind": "global", + "enabled": true + } + ], + "load": { + "kind": "loaded" + }, + "children": [ + { + "type": "skill", + "id": "skill-triage", + "uri": "ahp-automation:/a2/customizations/client-plugin-1/skills/triage/SKILL.md", + "name": "triage" + }, + { + "type": "agent", + "id": "agent-reviewer", + "uri": "ahp-automation:/a2/customizations/client-plugin-1/agents/reviewer.md", + "name": "reviewer" + } + ] + } + ], + "createdAt": "2026-08-01T00:00:00Z", + "modifiedAt": "2026-08-05T12:00:00Z" + } + ] + }, + "fromSeq": 7 + }, + "acceptableOutputs": [ + { + "resource": "ahp-automations://", + "state": { + "entries": [ + { + "resource": "ahp-automation:/a2", + "definition": { + "title": "Weekly report", + "message": { + "text": "Write the weekly report", + "origin": { + "kind": "automation" + } + }, + "session": { + "provider": "copilot", + "customizations": [ + { + "type": "plugin", + "id": "client-plugin-1", + "uri": "virtual://vscode/workspace-skills", + "name": "Workspace Skills", + "enablement": [ + { + "kind": "global", + "enabled": true + } + ], + "childEnablement": { + "report-writer": [ + { + "kind": "global", + "enabled": false + } + ] + }, + "nonce": "sha256:abc123" + } + ] + }, + "enabled": true, + "triggers": [] + }, + "runs": [], + "operations": [ + "update", + "remove", + "run" + ], + "customizations": [ + { + "type": "plugin", + "id": "client-plugin-1", + "uri": "ahp-automation:/a2/customizations/client-plugin-1", + "name": "Workspace Skills", + "enablement": [ + { + "kind": "global", + "enabled": true + } + ], + "load": { + "kind": "loaded" + }, + "children": [ + { + "type": "skill", + "id": "skill-triage", + "uri": "ahp-automation:/a2/customizations/client-plugin-1/skills/triage/SKILL.md", + "name": "triage" + }, + { + "type": "agent", + "id": "agent-reviewer", + "uri": "ahp-automation:/a2/customizations/client-plugin-1/agents/reviewer.md", + "name": "reviewer" + } + ] + } + ], + "createdAt": "2026-08-01T00:00:00Z", + "modifiedAt": "2026-08-05T12:00:00Z" + } + ] + }, + "fromSeq": 7 + } + ] +}