From 5eadbe33f6048f748fe42a0a125b96e91414bfdc Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Tue, 15 Sep 2026 18:17:33 -0700 Subject: [PATCH 1/6] Add schema-discovered repository-backed session creation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generated/Commands.generated.cs | 31 ++++- .../JsonSerializerContext.generated.cs | 1 + .../Generated/Notifications.generated.cs | 5 +- .../Generated/State.generated.cs | 32 ++++- clients/go/ahptypes/commands.generated.go | 21 +++ .../go/ahptypes/notifications.generated.go | 3 + clients/go/ahptypes/state.generated.go | 27 +++- .../generated/Commands.generated.kt | 28 +++- .../generated/State.generated.kt | 4 +- clients/rust/crates/ahp-types/src/commands.rs | 21 +++ .../crates/ahp-types/src/notifications.rs | 3 + clients/rust/crates/ahp-types/src/state.rs | 31 ++++- .../Generated/Commands.generated.swift | 31 ++++- .../Generated/State.generated.swift | 4 +- clients/typescript/test/client.test.ts | 127 ++++++++++++++++++ .../20260915-repository-session-config.json | 4 + docs/specification/root-channel.md | 2 + docs/specification/session-channel.md | 92 +++++++++++++ schema/actions.schema.json | 23 +++- schema/commands.schema.json | 33 ++++- schema/errors.schema.json | 33 ++++- schema/notifications.schema.json | 25 +++- schema/state.schema.json | 23 +++- scripts/generate-csharp.ts | 1 + scripts/generate-go.ts | 1 + scripts/generate-json-schema.test.ts | 73 ++++++++++ scripts/generate-kotlin.ts | 2 +- scripts/generate-rust.ts | 1 + scripts/generate-swift.ts | 2 +- types/channels-root/commands.ts | 6 +- types/channels-root/notifications.ts | 3 + types/channels-session/commands.ts | 16 +++ types/channels-session/state.ts | 37 ++++- ...045-session-config-without-repository.json | 50 +++++++ .../046-repository-session-url-only.json | 52 +++++++ .../047-repository-session-revision.json | 52 +++++++ 36 files changed, 867 insertions(+), 33 deletions(-) create mode 100644 docs/.changes/20260915-repository-session-config.json create mode 100644 types/test-cases/round-trips/045-session-config-without-repository.json create mode 100644 types/test-cases/round-trips/046-repository-session-url-only.json create mode 100644 types/test-cases/round-trips/047-repository-session-revision.json diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs index 0ef02d8be..58686432f 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs @@ -462,7 +462,14 @@ public sealed record SubscribeResult /// /// After creation, the client should subscribe to the session URI to receive state /// updates. The server also broadcasts a `root/sessionAdded` notification to all -/// clients. +/// clients. +/// +/// For repository intent advertised by {@link RepositorySessionConfig}, the +/// host MUST authorize the request before repository side effects and prepare +/// the repository before executing turns. It MUST publish the requested intent +/// in {@link SessionState.config} and any resolved `workingDirectories` before +/// `session/ready` or `session/creationFailed`. Clients recover the outcome from +/// session state, not progress notifications. public sealed record CreateSessionParams { /// Session URI (client-chosen, e.g. `ahp-session:/<uuid>`) @@ -490,12 +497,19 @@ public sealed record CreateSessionParams /// {@link AgentCapabilities.multipleWorkingDirectories}; a server without that /// capability treats only the first entry as the session's working directory /// and ignores the rest. Dispatch working-directory actions to change the set - /// after the session has started. + /// after the session has started. + /// + /// A non-empty list and repository intent in `config` are mutually exclusive. + /// A repository URI is not a working-directory URI. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? WorkingDirectories { get; init; } /// Agent-specific configuration values collected via `resolveSessionConfig`. - /// Keys and values correspond to the schema returned by the server. + /// Keys and values correspond to the schema returned by the server. + /// Repository intent uses only the properties identified by the advertised + /// {@link SessionConfigSchema.repository} descriptor. A revision without a + /// repository URI is invalid. Omitting repository intent preserves existing + /// directory/default behavior. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Dictionary? Config { get; init; } @@ -524,7 +538,10 @@ public sealed record CreateSessionParams /// Disposes a session and cleans up server-side resources. /// -/// The server broadcasts a `root/sessionRemoved` notification to all clients. +/// The server broadcasts a `root/sessionRemoved` notification to all clients. +/// Disposal MUST NOT erase a shared checkout or uncommitted user changes. +/// Repository cleanup remains host-owned; ending a client's wait or subscription +/// does not grant permission to delete repository data. public sealed record DisposeSessionParams { /// Channel URI this command targets. @@ -1346,7 +1363,11 @@ public sealed record DisposeTerminalParams /// The client calls this command whenever the user changes a significant input /// (e.g. picks a working directory, toggles a property). Each response returns /// the full current property set (not a delta). The returned `values` contain -/// server-resolved defaults to pass to `createSession`. +/// server-resolved defaults to pass to `createSession`. +/// +/// Repository-backed creation is advertised by `schema.repository`. Resolving +/// that schema or its values MUST NOT clone or prepare a repository; preparation +/// belongs to `createSession`. public sealed record ResolveSessionConfigParams { public required string Channel { get; init; } diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs index 9b1640215..b07bbd7f4 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs @@ -264,6 +264,7 @@ namespace Microsoft.AgentHostProtocol; [JsonSerializable(typeof(ReconnectResult))] [JsonSerializable(typeof(ReconnectResultType))] [JsonSerializable(typeof(ReconnectSnapshotResult))] +[JsonSerializable(typeof(RepositorySessionConfig))] [JsonSerializable(typeof(ResolveSessionConfigParams))] [JsonSerializable(typeof(ResolveSessionConfigResult))] [JsonSerializable(typeof(ResourceChange))] diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs index 9b5bd1a1e..06510d6f8 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs @@ -117,7 +117,10 @@ public sealed record SessionSummaryChangedParams /// the client then never shows an indicator. /// - Like all notifications this is ephemeral and is **not** replayed on /// reconnect. A client that never receives the terminal frame SHOULD expire -/// the indicator after an idle timeout. +/// the indicator after an idle timeout. +/// - Completion of reported work does not establish session readiness. +/// Repository-backed creation uses session state and the existing +/// `session/ready` or `session/creationFailed` actions for its durable outcome. public sealed record ProgressParams { /// Channel URI this notification belongs to (the root channel). diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index 5ead9d4cf..ef8b93cf5 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -1586,7 +1586,9 @@ public sealed class SessionState [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? DefaultChat { get; set; } - /// Session configuration schema and current values + /// Session configuration schema and current values. For repository-backed + /// creation, this includes the advertised repository descriptor and requested + /// intent, so joining and reconnecting clients can recover it from state. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public SessionConfigState? Config { get; set; } @@ -2007,6 +2009,28 @@ public sealed record SessionConfigPropertySchema public bool? SessionMutable { get; init; } } +/// Opt-in descriptor for preparing one repository during session creation. +/// +/// Property ids are host-chosen and MUST name distinct entries in +/// {@link SessionConfigSchema.properties}. Each referenced property MUST have +/// `type: 'string'` and MUST NOT have `readOnly: true` or `sessionMutable: true`. +/// Clients MUST use these ids rather than hardcoding repository field names. +/// +/// Values travel through `resolveSessionConfig.config` and `createSession.config`, +/// not a separate command or `_meta`. Schema discovery MUST NOT clone or prepare +/// a repository. The host accepts repository intent only when this descriptor +/// is advertised. +public sealed record RepositorySessionConfig +{ + /// Property id for a credential-free repository URI. + public required string UrlProperty { get; init; } + + /// Property id for an optional branch, tag, or commit revision. + /// A revision value without a repository URI is invalid. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? RevisionProperty { get; init; } +} + /// A JSON Schema object describing available session configuration metadata. public sealed record SessionConfigSchema { @@ -2019,6 +2043,12 @@ public sealed record SessionConfigSchema /// JSON Schema: list of required property ids [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? Required { get; init; } + + /// Opt-in capability for repository-backed creation using existing config + /// properties. The descriptor does not itself require a repository value. + /// Without repository intent, existing directory/default behavior is unchanged. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public RepositorySessionConfig? Repository { get; init; } } /// Live session configuration metadata. diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index 9ef81b6f8..9ba3fb4c6 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -375,6 +375,13 @@ type SubscribeResult struct { // After creation, the client should subscribe to the session URI to receive state // updates. The server also broadcasts a `root/sessionAdded` notification to all // clients. +// +// For repository intent advertised by {@link RepositorySessionConfig}, the +// host MUST authorize the request before repository side effects and prepare +// the repository before executing turns. It MUST publish the requested intent +// in {@link SessionState.config} and any resolved `workingDirectories` before +// `session/ready` or `session/creationFailed`. Clients recover the outcome from +// session state, not progress notifications. type CreateSessionParams struct { // Channel URI this command targets. Channel URI `json:"channel"` @@ -396,9 +403,16 @@ type CreateSessionParams struct { // capability treats only the first entry as the session's working directory // and ignores the rest. Dispatch working-directory actions to change the set // after the session has started. + // + // A non-empty list and repository intent in `config` are mutually exclusive. + // A repository URI is not a working-directory URI. WorkingDirectories []URI `json:"workingDirectories,omitempty"` // Agent-specific configuration values collected via `resolveSessionConfig`. // Keys and values correspond to the schema returned by the server. + // Repository intent uses only the properties identified by the advertised + // {@link SessionConfigSchema.repository} descriptor. A revision without a + // repository URI is invalid. Omitting repository intent preserves existing + // directory/default behavior. Config map[string]json.RawMessage `json:"config,omitempty"` // Eagerly claim an active client role for the new session. // @@ -423,6 +437,9 @@ type CreateSessionParams struct { // Disposes a session and cleans up server-side resources. // // The server broadcasts a `root/sessionRemoved` notification to all clients. +// Disposal MUST NOT erase a shared checkout or uncommitted user changes. +// Repository cleanup remains host-owned; ending a client's wait or subscription +// does not grant permission to delete repository data. type DisposeSessionParams struct { // Channel URI this command targets. Channel URI `json:"channel"` @@ -1070,6 +1087,10 @@ type DisposeTerminalParams struct { // (e.g. picks a working directory, toggles a property). Each response returns // the full current property set (not a delta). The returned `values` contain // server-resolved defaults to pass to `createSession`. +// +// Repository-backed creation is advertised by `schema.repository`. Resolving +// that schema or its values MUST NOT clone or prepare a repository; preparation +// belongs to `createSession`. type ResolveSessionConfigParams struct { // Channel URI this command targets. Channel URI `json:"channel"` diff --git a/clients/go/ahptypes/notifications.generated.go b/clients/go/ahptypes/notifications.generated.go index 62db00de1..9dab1d823 100644 --- a/clients/go/ahptypes/notifications.generated.go +++ b/clients/go/ahptypes/notifications.generated.go @@ -115,6 +115,9 @@ type SessionSummaryChangedParams struct { // - Like all notifications this is ephemeral and is **not** replayed on // reconnect. A client that never receives the terminal frame SHOULD expire // the indicator after an idle timeout. +// - Completion of reported work does not establish session readiness. +// Repository-backed creation uses session state and the existing +// `session/ready` or `session/creationFailed` actions for its durable outcome. type ProgressParams struct { // Channel URI this notification belongs to (the root channel). Channel URI `json:"channel"` diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 269b50f54..b51e1c5ec 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -905,7 +905,9 @@ type SessionState struct { // marker — chats remain equal peers at the protocol level. Hosts MAY change // this over the session's lifetime. DefaultChat *URI `json:"defaultChat,omitempty"` - // Session configuration schema and current values + // Session configuration schema and current values. For repository-backed + // creation, this includes the advertised repository descriptor and requested + // intent, so joining and reconnecting clients can recover it from state. Config *SessionConfigState `json:"config,omitempty"` // Top-level customizations active in this session. // @@ -1364,6 +1366,25 @@ type SessionConfigPropertySchema struct { SessionMutable *bool `json:"sessionMutable,omitempty"` } +// Opt-in descriptor for preparing one repository during session creation. +// +// Property ids are host-chosen and MUST name distinct entries in +// {@link SessionConfigSchema.properties}. Each referenced property MUST have +// `type: 'string'` and MUST NOT have `readOnly: true` or `sessionMutable: true`. +// Clients MUST use these ids rather than hardcoding repository field names. +// +// Values travel through `resolveSessionConfig.config` and `createSession.config`, +// not a separate command or `_meta`. Schema discovery MUST NOT clone or prepare +// a repository. The host accepts repository intent only when this descriptor +// is advertised. +type RepositorySessionConfig struct { + // Property id for a credential-free repository URI. + UrlProperty string `json:"urlProperty"` + // Property id for an optional branch, tag, or commit revision. + // A revision value without a repository URI is invalid. + RevisionProperty *string `json:"revisionProperty,omitempty"` +} + // A JSON Schema object describing available session configuration metadata. type SessionConfigSchema struct { // JSON Schema: always `'object'` @@ -1372,6 +1393,10 @@ type SessionConfigSchema struct { Properties map[string]SessionConfigPropertySchema `json:"properties"` // JSON Schema: list of required property ids Required []string `json:"required,omitempty"` + // Opt-in capability for repository-backed creation using existing config + // properties. The descriptor does not itself require a repository value. + // Without repository intent, existing directory/default behavior is unchanged. + Repository *RepositorySessionConfig `json:"repository,omitempty"` } // Live session configuration metadata. 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..3f64c091c 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 @@ -617,11 +617,18 @@ data class CreateSessionParams( * capability treats only the first entry as the session's working directory * and ignores the rest. Dispatch working-directory actions to change the set * after the session has started. + * + * A non-empty list and repository intent in `config` are mutually exclusive. + * A repository URI is not a working-directory URI. */ val workingDirectories: List? = null, /** * Agent-specific configuration values collected via `resolveSessionConfig`. * Keys and values correspond to the schema returned by the server. + * Repository intent uses only the properties identified by the advertised + * {@link SessionConfigSchema.repository} descriptor. A revision without a + * repository URI is invalid. Omitting repository intent preserves existing + * directory/default behavior. */ val config: Map? = null, /** @@ -1397,6 +1404,19 @@ data class SessionConfigPropertySchema( val sessionMutable: Boolean? = null ) +@Serializable +data class RepositorySessionConfig( + /** + * Property id for a credential-free repository URI. + */ + val urlProperty: String, + /** + * Property id for an optional branch, tag, or commit revision. + * A revision value without a repository URI is invalid. + */ + val revisionProperty: String? = null +) + @Serializable data class SessionConfigSchema( /** @@ -1410,7 +1430,13 @@ data class SessionConfigSchema( /** * JSON Schema: list of required property ids */ - val required: List? = null + val required: List? = null, + /** + * Opt-in capability for repository-backed creation using existing config + * properties. The descriptor does not itself require a repository value. + * Without repository intent, existing directory/default behavior is unchanged. + */ + val repository: RepositorySessionConfig? = null ) @Serializable diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt index 9fee270bb..b2393ccea 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 @@ -1802,7 +1802,9 @@ data class SessionState( */ val defaultChat: String? = null, /** - * Session configuration schema and current values + * Session configuration schema and current values. For repository-backed + * creation, this includes the advertised repository descriptor and requested + * intent, so joining and reconnecting clients can recover it from state. */ val config: SessionConfigState? = null, /** diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index e3dfe255e..e32d95c63 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -558,6 +558,13 @@ pub struct SubscribeResult { /// After creation, the client should subscribe to the session URI to receive state /// updates. The server also broadcasts a `root/sessionAdded` notification to all /// clients. +/// +/// For repository intent advertised by {@link RepositorySessionConfig}, the +/// host MUST authorize the request before repository side effects and prepare +/// the repository before executing turns. It MUST publish the requested intent +/// in {@link SessionState.config} and any resolved `workingDirectories` before +/// `session/ready` or `session/creationFailed`. Clients recover the outcome from +/// session state, not progress notifications. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateSessionParams { @@ -583,10 +590,17 @@ pub struct CreateSessionParams { /// capability treats only the first entry as the session's working directory /// and ignores the rest. Dispatch working-directory actions to change the set /// after the session has started. + /// + /// A non-empty list and repository intent in `config` are mutually exclusive. + /// A repository URI is not a working-directory URI. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, /// Agent-specific configuration values collected via `resolveSessionConfig`. /// Keys and values correspond to the schema returned by the server. + /// Repository intent uses only the properties identified by the advertised + /// {@link SessionConfigSchema.repository} descriptor. A revision without a + /// repository URI is invalid. Omitting repository intent preserves existing + /// directory/default behavior. #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option, /// Eagerly claim an active client role for the new session. @@ -614,6 +628,9 @@ pub struct CreateSessionParams { /// Disposes a session and cleans up server-side resources. /// /// The server broadcasts a `root/sessionRemoved` notification to all clients. +/// Disposal MUST NOT erase a shared checkout or uncommitted user changes. +/// Repository cleanup remains host-owned; ending a client's wait or subscription +/// does not grant permission to delete repository data. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DisposeSessionParams { @@ -1374,6 +1391,10 @@ pub struct DisposeTerminalParams { /// (e.g. picks a working directory, toggles a property). Each response returns /// the full current property set (not a delta). The returned `values` contain /// server-resolved defaults to pass to `createSession`. +/// +/// Repository-backed creation is advertised by `schema.repository`. Resolving +/// that schema or its values MUST NOT clone or prepare a repository; preparation +/// belongs to `createSession`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ResolveSessionConfigParams { diff --git a/clients/rust/crates/ahp-types/src/notifications.rs b/clients/rust/crates/ahp-types/src/notifications.rs index 3fa4cce5d..bee3ba0be 100644 --- a/clients/rust/crates/ahp-types/src/notifications.rs +++ b/clients/rust/crates/ahp-types/src/notifications.rs @@ -153,6 +153,9 @@ pub struct SessionSummaryChangedParams { /// - Like all notifications this is ephemeral and is **not** replayed on /// reconnect. A client that never receives the terminal frame SHOULD expire /// the indicator after an idle timeout. +/// - Completion of reported work does not establish session readiness. +/// Repository-backed creation uses session state and the existing +/// `session/ready` or `session/creationFailed` actions for its durable outcome. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ProgressParams { diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 6d7ff245a..2069d901f 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -2053,7 +2053,9 @@ pub struct SessionState { /// this over the session's lifetime. #[serde(default, skip_serializing_if = "Option::is_none")] pub default_chat: Option, - /// Session configuration schema and current values + /// Session configuration schema and current values. For repository-backed + /// creation, this includes the advertised repository descriptor and requested + /// intent, so joining and reconnecting clients can recover it from state. #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option, /// Top-level customizations active in this session. @@ -2463,6 +2465,28 @@ pub struct SessionConfigPropertySchema { pub session_mutable: Option, } +/// Opt-in descriptor for preparing one repository during session creation. +/// +/// Property ids are host-chosen and MUST name distinct entries in +/// {@link SessionConfigSchema.properties}. Each referenced property MUST have +/// `type: 'string'` and MUST NOT have `readOnly: true` or `sessionMutable: true`. +/// Clients MUST use these ids rather than hardcoding repository field names. +/// +/// Values travel through `resolveSessionConfig.config` and `createSession.config`, +/// not a separate command or `_meta`. Schema discovery MUST NOT clone or prepare +/// a repository. The host accepts repository intent only when this descriptor +/// is advertised. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RepositorySessionConfig { + /// Property id for a credential-free repository URI. + pub url_property: String, + /// Property id for an optional branch, tag, or commit revision. + /// A revision value without a repository URI is invalid. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub revision_property: Option, +} + /// A JSON Schema object describing available session configuration metadata. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -2474,6 +2498,11 @@ pub struct SessionConfigSchema { /// JSON Schema: list of required property ids #[serde(default, skip_serializing_if = "Option::is_none")] pub required: Option>, + /// Opt-in capability for repository-backed creation using existing config + /// properties. The descriptor does not itself require a repository value. + /// Without repository intent, existing directory/default behavior is unchanged. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repository: Option, } /// Live session configuration metadata. diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index 9d9ca3e25..a9328ec0b 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -651,9 +651,16 @@ public struct CreateSessionParams: Codable, Sendable { /// capability treats only the first entry as the session's working directory /// and ignores the rest. Dispatch working-directory actions to change the set /// after the session has started. + /// + /// A non-empty list and repository intent in `config` are mutually exclusive. + /// A repository URI is not a working-directory URI. public var workingDirectories: [String]? /// Agent-specific configuration values collected via `resolveSessionConfig`. /// Keys and values correspond to the schema returned by the server. + /// Repository intent uses only the properties identified by the advertised + /// {@link SessionConfigSchema.repository} descriptor. A revision without a + /// repository URI is invalid. Omitting repository intent preserves existing + /// directory/default behavior. public var config: [String: AnyCodable]? /// Eagerly claim an active client role for the new session. /// @@ -1720,6 +1727,22 @@ public struct SessionConfigPropertySchema: Codable, Sendable { } } +public struct RepositorySessionConfig: Codable, Sendable { + /// Property id for a credential-free repository URI. + public var urlProperty: String + /// Property id for an optional branch, tag, or commit revision. + /// A revision value without a repository URI is invalid. + public var revisionProperty: String? + + public init( + urlProperty: String, + revisionProperty: String? = nil + ) { + self.urlProperty = urlProperty + self.revisionProperty = revisionProperty + } +} + public struct SessionConfigSchema: Codable, Sendable { /// JSON Schema: always `'object'` public var type: String @@ -1727,15 +1750,21 @@ public struct SessionConfigSchema: Codable, Sendable { public var properties: [String: SessionConfigPropertySchema] /// JSON Schema: list of required property ids public var required: [String]? + /// Opt-in capability for repository-backed creation using existing config + /// properties. The descriptor does not itself require a repository value. + /// Without repository intent, existing directory/default behavior is unchanged. + public var repository: RepositorySessionConfig? public init( type: String, properties: [String: SessionConfigPropertySchema], - required: [String]? = nil + required: [String]? = nil, + repository: RepositorySessionConfig? = nil ) { self.type = type self.properties = properties self.required = required + self.repository = repository } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 313783d66..b4b206f51 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -1839,7 +1839,9 @@ public struct SessionState: Codable, Sendable { /// marker — chats remain equal peers at the protocol level. Hosts MAY change /// this over the session's lifetime. public var defaultChat: String? - /// Session configuration schema and current values + /// Session configuration schema and current values. For repository-backed + /// creation, this includes the advertised repository descriptor and requested + /// intent, so joining and reconnecting clients can recover it from state. public var config: SessionConfigState? /// Top-level customizations active in this session. /// diff --git a/clients/typescript/test/client.test.ts b/clients/typescript/test/client.test.ts index 6177c092f..6026aaa45 100644 --- a/clients/typescript/test/client.test.ts +++ b/clients/typescript/test/client.test.ts @@ -42,6 +42,13 @@ import type { import { JsonRpcErrorCodes } from '../src/types/common/errors.js'; import { AutomationOperation, type AutomationEntry } from '../src/types/channels-automation/state.js'; import { MessageKind } from '../src/types/channels-chat/state.js'; +import { + SessionLifecycle, + SessionStatus, + type RepositorySessionConfig, + type SessionConfigSchema, + type SessionState, +} from '../src/types/index.js'; const ROOT = 'ahp-root://' as const; const AUTOMATIONS = 'ahp-automations://' as const; @@ -108,6 +115,126 @@ test('initialize round-trip', async () => { await client.shutdown(); }); +for (const revisionProperty of [undefined, 'host_revision']) { + test(`generic session config round-trips repository intent ${revisionProperty ? 'with' : 'without'} a revision`, async t => { + const [c, s] = InMemoryTransport.pair(); + const client = new AhpClient(c); + t.after(() => client.shutdown()); + client.connect(); + + const repository: RepositorySessionConfig = { + urlProperty: 'host_source', + ...(revisionProperty ? { revisionProperty } : {}), + }; + const schema: SessionConfigSchema = { + type: 'object', + properties: { + host_source: { type: 'string', title: 'Repository' }, + mode: { type: 'string', title: 'Mode', default: 'review' }, + ...(revisionProperty ? { [revisionProperty]: { type: 'string' as const, title: 'Revision' } } : {}), + }, + repository, + }; + + const discovery = client.request('resolveSessionConfig', { channel: ROOT }); + const discoveryRequest = await readRequest(s); + assert.equal(discoveryRequest.method, 'resolveSessionConfig'); + assert.deepEqual(discoveryRequest.params, { channel: ROOT }); + reply(s, discoveryRequest.id, { schema, values: { mode: 'review' } }); + + const discovered = await discovery; + assert.deepEqual(discovered.schema, schema); + const descriptor = discovered.schema.repository; + assert.ok(descriptor); + const config = { + ...discovered.values, + [descriptor.urlProperty]: 'https://example.org/team/project.git', + ...(descriptor.revisionProperty ? { [descriptor.revisionProperty]: 'refs/tags/v1.2.3' } : {}), + }; + const resolution = client.request('resolveSessionConfig', { channel: ROOT, config }); + const resolveRequest = await readRequest(s); + assert.equal(resolveRequest.method, 'resolveSessionConfig'); + assert.deepEqual(resolveRequest.params, { channel: ROOT, config }); + reply(s, resolveRequest.id, { schema, values: config }); + const resolved = await resolution; + assert.deepEqual(resolved.values, config); + + const params = { channel: 'ahp-session:/repository-test', config: resolved.values }; + const creation = client.request('createSession', params); + const createRequest = await readRequest(s); + assert.equal(createRequest.method, 'createSession'); + assert.deepEqual(createRequest.params, params); + reply(s, createRequest.id, null); + assert.equal(await creation, null); + }); +} + +for (const failed of [false, true]) { + test(`session state recovers repository intent and directories after creation ${failed ? 'fails' : 'succeeds'}`, () => { + const resource = 'ahp-session:/repository-test'; + const initial: SessionState = { + provider: 'example', + title: 'Repository session', + status: SessionStatus.Idle, + lifecycle: SessionLifecycle.Creating, + activeClients: [], + chats: [], + workingDirectories: [], + config: { + schema: { + type: 'object', + properties: { + source: { type: 'string', title: 'Repository' }, + revision: { type: 'string', title: 'Revision' }, + }, + repository: { urlProperty: 'source', revisionProperty: 'revision' }, + }, + values: { source: 'https://example.org/team/project.git', revision: 'main' }, + }, + }; + const mirror = new AhpStateMirror(); + mirror.applySnapshot({ resource, state: initial, fromSeq: 0 }); + mirror.apply({ + channel: resource, + serverSeq: 1, + origin: undefined, + action: { type: ActionType.SessionWorkingDirectorySet, directory: 'file:///work/project' }, + }); + const preparing = mirror.getSession(resource); + assert.ok(preparing); + assert.equal(preparing.lifecycle, SessionLifecycle.Creating); + assert.deepEqual(preparing.config, initial.config); + + const joining = new AhpStateMirror(); + joining.applySnapshot({ resource, state: preparing, fromSeq: 1 }); + assert.deepEqual(joining.getSession(resource), preparing); + + const completion: ActionEnvelope = { + channel: resource, + serverSeq: 2, + origin: undefined, + action: failed + ? { type: ActionType.SessionCreationFailed, error: { errorType: 'preparationFailed', message: 'Preparation failed' } } + : { type: ActionType.SessionReady }, + }; + mirror.apply(completion); + joining.apply(completion); + const completed = mirror.getSession(resource); + assert.ok(completed); + assert.equal(completed.lifecycle, failed ? SessionLifecycle.Failed : SessionLifecycle.Ready); + assert.deepEqual(completed.config, initial.config); + assert.deepEqual(completed.workingDirectories, ['file:///work/project']); + assert.deepEqual(joining.getSession(resource), completed); + if (failed) { + assert.deepEqual(completed.creationError, { errorType: 'preparationFailed', message: 'Preparation failed' }); + } + + const reconnected = new AhpStateMirror(); + reconnected.applySnapshot({ resource, state: completed, fromSeq: 2 }); + assert.deepEqual(reconnected.getSession(resource), completed); + }); +} + test('subscribe attaches before sending the request and fans out an action', async () => { const [c, s] = InMemoryTransport.pair(); const client = new AhpClient(c); diff --git a/docs/.changes/20260915-repository-session-config.json b/docs/.changes/20260915-repository-session-config.json new file mode 100644 index 000000000..da83186ff --- /dev/null +++ b/docs/.changes/20260915-repository-session-config.json @@ -0,0 +1,4 @@ +{ + "type": "added", + "message": "Optional `SessionConfigSchema.repository` descriptor for host-owned, repository-backed session creation through existing configuration and lifecycle messages." +} diff --git a/docs/specification/root-channel.md b/docs/specification/root-channel.md index 2ccdad606..c0c717285 100644 --- a/docs/specification/root-channel.md +++ b/docs/specification/root-channel.md @@ -185,6 +185,8 @@ The server MAY emit `root/progress` to report incremental progress on a long-run `progress` is monotonically non-decreasing for a given `progressToken`. `total` is present only when the magnitude is known up front (e.g. a `Content-Length`); when absent, clients SHOULD show an indeterminate indicator. The operation is complete when `progress === total` — the server MUST emit a final frame satisfying this, setting `total` to the final `progress` when the total was never known, after which no further frames reference the token. An optional `message` carries a human-readable description of the work in progress; a client that tracks the token renders its own (localized) label and MAY ignore it, while a generic client MAY display `message` verbatim. The server MAY emit no progress at all (for example when the work was already done), in which case the client simply never shows an indicator. Like the catalogue events, `root/progress` is ephemeral and is **not** replayed on reconnect. +Completing reported work does not establish session readiness. For [repository-backed creation](./session-channel#repository-backed-creation), hosts MAY use this same progress notification, but clients recover the requested intent, resolved directories, and `creating` / `ready` / `failed` outcome from session state. A minimal client can ignore progress entirely. + ## Authentication Events The server MAY emit [`auth/required`](/specification/authentication#auth-expiry-notification) on the root channel when an agent's protected resource needs (re-)authentication. See [Authentication](/specification/authentication) for the full flow. diff --git a/docs/specification/session-channel.md b/docs/specification/session-channel.md index 4066ad140..19402cac8 100644 --- a/docs/specification/session-channel.md +++ b/docs/specification/session-channel.md @@ -35,6 +35,98 @@ Subscribers receive a [`SessionState`](/reference/session#sessionstate) snapshot [`createSession`](/reference/session#createsession) is a JSON-RPC request. The client picks the URI; the server allocates session state and begins backend initialisation. If the URI is already in use the server returns `SessionAlreadyExists` (`-32003`). +#### Repository-backed creation + +A host can offer to prepare **one repository for a new session** through the existing session configuration flow. The client collects repository intent; the host owns authorization, credentials, preparation, and cleanup. This capability does not define reusable projects, a repository catalogue, or a general-purpose clone command. + +##### Capability and field constraints + +The host opts in by returning the optional [`SessionConfigSchema.repository`](/reference/session#sessionconfigschema) descriptor from [`resolveSessionConfig`](/reference/root#resolvesessionconfig): + +```ts +export interface RepositorySessionConfig { + urlProperty: string; + revisionProperty?: string; +} +``` + +The descriptor identifies existing entries in `schema.properties`; it does not carry the repository values. + +| Field | Meaning | +|---|---| +| `urlProperty` | Host-chosen property id for a credential-free repository URI. | +| `revisionProperty` | Optional host-chosen property id for a branch, tag, or commit revision. | + +Every referenced property MUST exist in `schema.properties`, have `type: "string"`, and be writable at creation (`readOnly` MUST NOT be `true`). Neither property may have `sessionMutable: true`: these values describe creation intent, not a request to switch repositories or revisions in an existing session. When `revisionProperty` is present, it MUST differ from `urlProperty`. These relationships are host validation rules; validating the descriptor's JSON shape alone does not check its references to other properties. + +Clients MUST use the advertised property ids, not hardcoded names. The descriptor itself is the opt-in capability; clients MUST NOT infer repository support from a provider name, protocol version, `_meta`, or a property whose name happens to resemble a repository field. A host MUST NOT accept repository intent unless it advertises this descriptor. + +The descriptor does not itself make either value required. The existing `required` list still describes form requirements; AHP adds no globally required repository property. A host without the descriptor, or a request without repository intent, retains its existing directory/default behavior. + +##### Values and validation + +Values travel in `resolveSessionConfig.config`, then in `createSession.config`. Discovery and iterative configuration resolution MUST NOT clone or prepare a repository. The host MAY advertise supported URI schemes and revision choices through the existing property descriptions, enums, and completions. + +For example, a host may choose `source_uri` and `source_ref`: + +```json +{ + "schema": { + "type": "object", + "properties": { + "source_uri": { "type": "string", "title": "Repository" }, + "source_ref": { "type": "string", "title": "Revision" } + }, + "repository": { + "urlProperty": "source_uri", + "revisionProperty": "source_ref" + } + }, + "values": {} +} +``` + +The client can submit `{"source_uri":"https://example.org/team/project.git","source_ref":"main"}` as `resolveSessionConfig.config`, without a `workingDirectory`, and pass the returned values to creation: + +```json +{ + "jsonrpc": "2.0", + "id": 2, + "method": "createSession", + "params": { + "channel": "ahp-session:/new-session", + "config": { + "source_uri": "https://example.org/team/project.git", + "source_ref": "main" + } + } +} +``` + +The repository URI identifies the source, not a host filesystem directory. Repository intent and a non-empty `createSession.workingDirectories` list are mutually exclusive. A revision value without a repository URI is invalid. Omit an unused repository or revision value rather than supplying an empty string. The host MUST reject invalid intent, including conflicting directories or a revision without a repository, with `InvalidParams` (`-32602`), rather than silently selecting a default directory. A repository-aware client MUST surface an invalid or unsupported descriptor instead of silently dropping the user's repository intent. + +Repository URIs and configuration values MUST NOT contain credentials such as passwords or access tokens. Authentication uses the existing [authentication contract](./authentication); the host MUST authorize the requesting client before repository side effects and use only credentials permitted for that request. Credentials MUST NOT appear in session state, progress messages, or logs. + +##### Preparation and recoverable state + +Repository preparation is part of the existing `creating` lifecycle. The host MUST finish preparation before executing turns or publishing `session/ready`. No additional lifecycle state is introduced. + +The host MUST publish the accepted, requested repository URI and optional revision under the advertised ids in `SessionState.config.values`, together with the descriptor in `SessionState.config.schema`. Make this intent available in the initial `creating` snapshot so another client joining during preparation can understand the session. Preserve requested intent even if the host resolves a branch or tag to a commit; the resolved working location is a separate fact. + +Before dispatching `session/ready` or `session/creationFailed`, the host MUST publish the actual resolved `workingDirectories` in session state, using the existing snapshot and working-directory actions. While no directory has been resolved, `workingDirectories` MAY be absent or empty; do not claim a checkout was prepared when preparation failed. On failure, the existing `session/creationFailed` action records `lifecycle: "failed"` and `creationError`. Both outcomes retain the requested intent and any resolved directories so clients can recover them from a snapshot or replay. + +The host MAY report preparation through the existing `createSession.progressToken` and [`root/progress`](./root-channel#progress). Progress is optional, ephemeral, and not replayed. Neither a completed progress indicator nor a successful command response is a replacement for session readiness or failure state. + +##### Reattachment, retry, and cleanup + +`createSession` is not an idempotent preparation command. A duplicate URI still returns `SessionAlreadyExists` (`-32003`), including while preparation is running or after creation has failed; it MUST NOT start another preparation for that session. After a lost response, the client should reattach to the same session URI through subscription or [reconnection](./lifecycle#reconnection) and inspect its state. It MUST NOT treat a duplicate creation as successful recovery. After a failure is addressed, a user can explicitly retry with a new session URI rather than overwrite the failed session. + +Cancelling a local wait, disconnecting, or unsubscribing does not grant permission to delete repository data. When the user intends to dispose the session, use the existing `disposeSession` command; this capability adds no cancellation RPC. The host MUST NOT erase a shared checkout or uncommitted user changes during cancellation or disposal. Cleanup of exclusively owned temporary preparation resources remains a host responsibility. + +##### Minimal-client behavior + +A minimal client can ignore the descriptor, render the ordinary advertised configuration fields, pass resolved values through `config`, and render the existing session lifecycle and `workingDirectories`. It needs neither Git support nor a repository-specific form, clone RPC, or progress implementation. It can also omit this optional creation capability entirely and continue using directory/default creation. A joining or reconnecting client renders the authoritative state without repeating repository preparation. + ### Active session Once a session reaches `lifecycle: 'ready'`, clients may create chats on it with [`createChat`](/reference/chat#createchat). Each chat is independently subscribable at its own `ahp-chat:/` URI; see the [Chat Channel specification](./chat-channel) for the per-chat lifecycle, turn flow, tool calls, and input request handling. diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 70f7a2b5a..fd2470671 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -3213,7 +3213,7 @@ }, "config": { "$ref": "#/$defs/SessionConfigState", - "description": "Session configuration schema and current values" + "description": "Session configuration schema and current values. For repository-backed\ncreation, this includes the advertised repository descriptor and requested\nintent, so joining and reconnecting clients can recover it from state." }, "customizations": { "type": "array", @@ -3635,6 +3635,23 @@ "title" ] }, + "RepositorySessionConfig": { + "type": "object", + "description": "Opt-in descriptor for preparing one repository during session creation.\n\nProperty ids are host-chosen and MUST name distinct entries in\n{@link SessionConfigSchema.properties}. Each referenced property MUST have\n`type: 'string'` and MUST NOT have `readOnly: true` or `sessionMutable: true`.\nClients MUST use these ids rather than hardcoding repository field names.\n\nValues travel through `resolveSessionConfig.config` and `createSession.config`,\nnot a separate command or `_meta`. Schema discovery MUST NOT clone or prepare\na repository. The host accepts repository intent only when this descriptor\nis advertised.", + "properties": { + "urlProperty": { + "type": "string", + "description": "Property id for a credential-free repository URI." + }, + "revisionProperty": { + "type": "string", + "description": "Property id for an optional branch, tag, or commit revision.\nA revision value without a repository URI is invalid." + } + }, + "required": [ + "urlProperty" + ] + }, "SessionConfigSchema": { "type": "object", "description": "A JSON Schema object describing available session configuration metadata.", @@ -3659,6 +3676,10 @@ "type": "string" }, "description": "JSON Schema: list of required property ids" + }, + "repository": { + "$ref": "#/$defs/RepositorySessionConfig", + "description": "Opt-in capability for repository-backed creation using existing config\nproperties. The descriptor does not itself require a repository value.\nWithout repository intent, existing directory/default behavior is unchanged." } }, "required": [ diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 229023682..4a6e66d77 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -949,7 +949,7 @@ }, "ResolveSessionConfigParams": { "type": "object", - "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.", + "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.\n\nRepository-backed creation is advertised by `schema.repository`. Resolving\nthat schema or its values MUST NOT clone or prepare a repository; preparation\nbelongs to `createSession`.", "properties": { "channel": { "type": "string", @@ -1081,7 +1081,7 @@ }, "CreateSessionParams": { "type": "object", - "description": "Creates a new session with the specified agent provider.\n\nIf the session URI already exists, the server MUST return an error with code\n`-32003` (`SessionAlreadyExists`).\n\nAfter creation, the client should subscribe to the session URI to receive state\nupdates. The server also broadcasts a `root/sessionAdded` notification to all\nclients.", + "description": "Creates a new session with the specified agent provider.\n\nIf the session URI already exists, the server MUST return an error with code\n`-32003` (`SessionAlreadyExists`).\n\nAfter creation, the client should subscribe to the session URI to receive state\nupdates. The server also broadcasts a `root/sessionAdded` notification to all\nclients.\n\nFor repository intent advertised by {@link RepositorySessionConfig}, the\nhost MUST authorize the request before repository side effects and prepare\nthe repository before executing turns. It MUST publish the requested intent\nin {@link SessionState.config} and any resolved `workingDirectories` before\n`session/ready` or `session/creationFailed`. Clients recover the outcome from\nsession state, not progress notifications.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -1101,12 +1101,12 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises a protected-primary capability. An\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary | immutable\nprimary} is fixed, while a\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement | replaceable\nprimary} is changed only with `session/workingDirectoryReplaced`.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch working-directory actions to change the set\nafter the session has started." + "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises a protected-primary capability. An\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary | immutable\nprimary} is fixed, while a\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement | replaceable\nprimary} is changed only with `session/workingDirectoryReplaced`.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch working-directory actions to change the set\nafter the session has started.\n\nA non-empty list and repository intent in `config` are mutually exclusive.\nA repository URI is not a working-directory URI." }, "config": { "type": "object", "additionalProperties": {}, - "description": "Agent-specific configuration values collected via `resolveSessionConfig`.\nKeys and values correspond to the schema returned by the server." + "description": "Agent-specific configuration values collected via `resolveSessionConfig`.\nKeys and values correspond to the schema returned by the server.\nRepository intent uses only the properties identified by the advertised\n{@link SessionConfigSchema.repository} descriptor. A revision without a\nrepository URI is invalid. Omitting repository intent preserves existing\ndirectory/default behavior." }, "activeClient": { "$ref": "#/$defs/SessionActiveClient", @@ -1123,7 +1123,7 @@ }, "DisposeSessionParams": { "type": "object", - "description": "Disposes a session and cleans up server-side resources.\n\nThe server broadcasts a `root/sessionRemoved` notification to all clients.", + "description": "Disposes a session and cleans up server-side resources.\n\nThe server broadcasts a `root/sessionRemoved` notification to all clients.\nDisposal MUST NOT erase a shared checkout or uncommitted user changes.\nRepository cleanup remains host-owned; ending a client's wait or subscription\ndoes not grant permission to delete repository data.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -2457,7 +2457,7 @@ }, "config": { "$ref": "#/$defs/SessionConfigState", - "description": "Session configuration schema and current values" + "description": "Session configuration schema and current values. For repository-backed\ncreation, this includes the advertised repository descriptor and requested\nintent, so joining and reconnecting clients can recover it from state." }, "customizations": { "type": "array", @@ -2879,6 +2879,23 @@ "title" ] }, + "RepositorySessionConfig": { + "type": "object", + "description": "Opt-in descriptor for preparing one repository during session creation.\n\nProperty ids are host-chosen and MUST name distinct entries in\n{@link SessionConfigSchema.properties}. Each referenced property MUST have\n`type: 'string'` and MUST NOT have `readOnly: true` or `sessionMutable: true`.\nClients MUST use these ids rather than hardcoding repository field names.\n\nValues travel through `resolveSessionConfig.config` and `createSession.config`,\nnot a separate command or `_meta`. Schema discovery MUST NOT clone or prepare\na repository. The host accepts repository intent only when this descriptor\nis advertised.", + "properties": { + "urlProperty": { + "type": "string", + "description": "Property id for a credential-free repository URI." + }, + "revisionProperty": { + "type": "string", + "description": "Property id for an optional branch, tag, or commit revision.\nA revision value without a repository URI is invalid." + } + }, + "required": [ + "urlProperty" + ] + }, "SessionConfigSchema": { "type": "object", "description": "A JSON Schema object describing available session configuration metadata.", @@ -2903,6 +2920,10 @@ "type": "string" }, "description": "JSON Schema: list of required property ids" + }, + "repository": { + "$ref": "#/$defs/RepositorySessionConfig", + "description": "Opt-in capability for repository-backed creation using existing config\nproperties. The descriptor does not itself require a repository value.\nWithout repository intent, existing directory/default behavior is unchanged." } }, "required": [ diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 71cb746bd..dcca2a03b 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -886,7 +886,7 @@ }, "config": { "$ref": "#/$defs/SessionConfigState", - "description": "Session configuration schema and current values" + "description": "Session configuration schema and current values. For repository-backed\ncreation, this includes the advertised repository descriptor and requested\nintent, so joining and reconnecting clients can recover it from state." }, "customizations": { "type": "array", @@ -1308,6 +1308,23 @@ "title" ] }, + "RepositorySessionConfig": { + "type": "object", + "description": "Opt-in descriptor for preparing one repository during session creation.\n\nProperty ids are host-chosen and MUST name distinct entries in\n{@link SessionConfigSchema.properties}. Each referenced property MUST have\n`type: 'string'` and MUST NOT have `readOnly: true` or `sessionMutable: true`.\nClients MUST use these ids rather than hardcoding repository field names.\n\nValues travel through `resolveSessionConfig.config` and `createSession.config`,\nnot a separate command or `_meta`. Schema discovery MUST NOT clone or prepare\na repository. The host accepts repository intent only when this descriptor\nis advertised.", + "properties": { + "urlProperty": { + "type": "string", + "description": "Property id for a credential-free repository URI." + }, + "revisionProperty": { + "type": "string", + "description": "Property id for an optional branch, tag, or commit revision.\nA revision value without a repository URI is invalid." + } + }, + "required": [ + "urlProperty" + ] + }, "SessionConfigSchema": { "type": "object", "description": "A JSON Schema object describing available session configuration metadata.", @@ -1332,6 +1349,10 @@ "type": "string" }, "description": "JSON Schema: list of required property ids" + }, + "repository": { + "$ref": "#/$defs/RepositorySessionConfig", + "description": "Opt-in capability for repository-backed creation using existing config\nproperties. The descriptor does not itself require a repository value.\nWithout repository intent, existing directory/default behavior is unchanged." } }, "required": [ @@ -6601,7 +6622,7 @@ }, "ResolveSessionConfigParams": { "type": "object", - "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.", + "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.\n\nRepository-backed creation is advertised by `schema.repository`. Resolving\nthat schema or its values MUST NOT clone or prepare a repository; preparation\nbelongs to `createSession`.", "properties": { "channel": { "type": "string", @@ -6733,7 +6754,7 @@ }, "CreateSessionParams": { "type": "object", - "description": "Creates a new session with the specified agent provider.\n\nIf the session URI already exists, the server MUST return an error with code\n`-32003` (`SessionAlreadyExists`).\n\nAfter creation, the client should subscribe to the session URI to receive state\nupdates. The server also broadcasts a `root/sessionAdded` notification to all\nclients.", + "description": "Creates a new session with the specified agent provider.\n\nIf the session URI already exists, the server MUST return an error with code\n`-32003` (`SessionAlreadyExists`).\n\nAfter creation, the client should subscribe to the session URI to receive state\nupdates. The server also broadcasts a `root/sessionAdded` notification to all\nclients.\n\nFor repository intent advertised by {@link RepositorySessionConfig}, the\nhost MUST authorize the request before repository side effects and prepare\nthe repository before executing turns. It MUST publish the requested intent\nin {@link SessionState.config} and any resolved `workingDirectories` before\n`session/ready` or `session/creationFailed`. Clients recover the outcome from\nsession state, not progress notifications.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -6753,12 +6774,12 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises a protected-primary capability. An\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary | immutable\nprimary} is fixed, while a\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement | replaceable\nprimary} is changed only with `session/workingDirectoryReplaced`.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch working-directory actions to change the set\nafter the session has started." + "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises a protected-primary capability. An\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary | immutable\nprimary} is fixed, while a\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement | replaceable\nprimary} is changed only with `session/workingDirectoryReplaced`.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch working-directory actions to change the set\nafter the session has started.\n\nA non-empty list and repository intent in `config` are mutually exclusive.\nA repository URI is not a working-directory URI." }, "config": { "type": "object", "additionalProperties": {}, - "description": "Agent-specific configuration values collected via `resolveSessionConfig`.\nKeys and values correspond to the schema returned by the server." + "description": "Agent-specific configuration values collected via `resolveSessionConfig`.\nKeys and values correspond to the schema returned by the server.\nRepository intent uses only the properties identified by the advertised\n{@link SessionConfigSchema.repository} descriptor. A revision without a\nrepository URI is invalid. Omitting repository intent preserves existing\ndirectory/default behavior." }, "activeClient": { "$ref": "#/$defs/SessionActiveClient", @@ -6775,7 +6796,7 @@ }, "DisposeSessionParams": { "type": "object", - "description": "Disposes a session and cleans up server-side resources.\n\nThe server broadcasts a `root/sessionRemoved` notification to all clients.", + "description": "Disposes a session and cleans up server-side resources.\n\nThe server broadcasts a `root/sessionRemoved` notification to all clients.\nDisposal MUST NOT erase a shared checkout or uncommitted user changes.\nRepository cleanup remains host-owned; ending a client's wait or subscription\ndoes not grant permission to delete repository data.", "properties": { "channel": { "$ref": "#/$defs/URI", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 01271335e..7dbcc476d 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -146,7 +146,7 @@ }, "ProgressParams": { "type": "object", - "description": "Generic progress notification for a long-running operation.\n\nA client opts in to progress for a request by including a `progressToken` in\nthat request (today: the `progressToken` field on `createSession`). If the\nserver does long-running work to service the request — e.g. lazily\ndownloading an agent's native SDK the first time a session of that provider\nis materialized — it emits `progress` notifications carrying the same token.\n\nThe notification is operation-agnostic: it says nothing about *what* is\nprogressing. The client correlates `progressToken` back to the request it\noriginated from (and thus the UI surface awaiting it) and renders its own\nlocalized indicator. The same channel serves any future long-running\noperation without a new method.\n\nSemantics:\n\n- `progress` is monotonically non-decreasing for a given `progressToken`.\n- `total` is present only when the server knows the magnitude up front\n (e.g. a `Content-Length`); when absent the client SHOULD show an\n indeterminate indicator.\n- The operation is complete when `progress === total`. The server MUST emit a\n final frame satisfying `progress === total`; when the total was never\n known, it sets `total` to the final `progress` on that frame. No further\n frames reference the token afterwards.\n- The server MAY emit no progress at all (e.g. the work was already done);\n the client then never shows an indicator.\n- Like all notifications this is ephemeral and is **not** replayed on\n reconnect. A client that never receives the terminal frame SHOULD expire\n the indicator after an idle timeout.", + "description": "Generic progress notification for a long-running operation.\n\nA client opts in to progress for a request by including a `progressToken` in\nthat request (today: the `progressToken` field on `createSession`). If the\nserver does long-running work to service the request — e.g. lazily\ndownloading an agent's native SDK the first time a session of that provider\nis materialized — it emits `progress` notifications carrying the same token.\n\nThe notification is operation-agnostic: it says nothing about *what* is\nprogressing. The client correlates `progressToken` back to the request it\noriginated from (and thus the UI surface awaiting it) and renders its own\nlocalized indicator. The same channel serves any future long-running\noperation without a new method.\n\nSemantics:\n\n- `progress` is monotonically non-decreasing for a given `progressToken`.\n- `total` is present only when the server knows the magnitude up front\n (e.g. a `Content-Length`); when absent the client SHOULD show an\n indeterminate indicator.\n- The operation is complete when `progress === total`. The server MUST emit a\n final frame satisfying `progress === total`; when the total was never\n known, it sets `total` to the final `progress` on that frame. No further\n frames reference the token afterwards.\n- The server MAY emit no progress at all (e.g. the work was already done);\n the client then never shows an indicator.\n- Like all notifications this is ephemeral and is **not** replayed on\n reconnect. A client that never receives the terminal frame SHOULD expire\n the indicator after an idle timeout.\n- Completion of reported work does not establish session readiness.\n Repository-backed creation uses session state and the existing\n `session/ready` or `session/creationFailed` actions for its durable outcome.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -1053,7 +1053,7 @@ }, "config": { "$ref": "#/$defs/SessionConfigState", - "description": "Session configuration schema and current values" + "description": "Session configuration schema and current values. For repository-backed\ncreation, this includes the advertised repository descriptor and requested\nintent, so joining and reconnecting clients can recover it from state." }, "customizations": { "type": "array", @@ -1475,6 +1475,23 @@ "title" ] }, + "RepositorySessionConfig": { + "type": "object", + "description": "Opt-in descriptor for preparing one repository during session creation.\n\nProperty ids are host-chosen and MUST name distinct entries in\n{@link SessionConfigSchema.properties}. Each referenced property MUST have\n`type: 'string'` and MUST NOT have `readOnly: true` or `sessionMutable: true`.\nClients MUST use these ids rather than hardcoding repository field names.\n\nValues travel through `resolveSessionConfig.config` and `createSession.config`,\nnot a separate command or `_meta`. Schema discovery MUST NOT clone or prepare\na repository. The host accepts repository intent only when this descriptor\nis advertised.", + "properties": { + "urlProperty": { + "type": "string", + "description": "Property id for a credential-free repository URI." + }, + "revisionProperty": { + "type": "string", + "description": "Property id for an optional branch, tag, or commit revision.\nA revision value without a repository URI is invalid." + } + }, + "required": [ + "urlProperty" + ] + }, "SessionConfigSchema": { "type": "object", "description": "A JSON Schema object describing available session configuration metadata.", @@ -1499,6 +1516,10 @@ "type": "string" }, "description": "JSON Schema: list of required property ids" + }, + "repository": { + "$ref": "#/$defs/RepositorySessionConfig", + "description": "Opt-in capability for repository-backed creation using existing config\nproperties. The descriptor does not itself require a repository value.\nWithout repository intent, existing directory/default behavior is unchanged." } }, "required": [ diff --git a/schema/state.schema.json b/schema/state.schema.json index 097236dfa..aa1f0b7a9 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -797,7 +797,7 @@ }, "config": { "$ref": "#/$defs/SessionConfigState", - "description": "Session configuration schema and current values" + "description": "Session configuration schema and current values. For repository-backed\ncreation, this includes the advertised repository descriptor and requested\nintent, so joining and reconnecting clients can recover it from state." }, "customizations": { "type": "array", @@ -1219,6 +1219,23 @@ "title" ] }, + "RepositorySessionConfig": { + "type": "object", + "description": "Opt-in descriptor for preparing one repository during session creation.\n\nProperty ids are host-chosen and MUST name distinct entries in\n{@link SessionConfigSchema.properties}. Each referenced property MUST have\n`type: 'string'` and MUST NOT have `readOnly: true` or `sessionMutable: true`.\nClients MUST use these ids rather than hardcoding repository field names.\n\nValues travel through `resolveSessionConfig.config` and `createSession.config`,\nnot a separate command or `_meta`. Schema discovery MUST NOT clone or prepare\na repository. The host accepts repository intent only when this descriptor\nis advertised.", + "properties": { + "urlProperty": { + "type": "string", + "description": "Property id for a credential-free repository URI." + }, + "revisionProperty": { + "type": "string", + "description": "Property id for an optional branch, tag, or commit revision.\nA revision value without a repository URI is invalid." + } + }, + "required": [ + "urlProperty" + ] + }, "SessionConfigSchema": { "type": "object", "description": "A JSON Schema object describing available session configuration metadata.", @@ -1243,6 +1260,10 @@ "type": "string" }, "description": "JSON Schema: list of required property ids" + }, + "repository": { + "$ref": "#/$defs/RepositorySessionConfig", + "description": "Opt-in capability for repository-backed creation using existing config\nproperties. The descriptor does not itself require a repository value.\nWithout repository intent, existing directory/default behavior is unchanged." } }, "required": [ diff --git a/scripts/generate-csharp.ts b/scripts/generate-csharp.ts index 207212210..b95dbb5e0 100644 --- a/scripts/generate-csharp.ts +++ b/scripts/generate-csharp.ts @@ -691,6 +691,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; csName?: strin { name: 'ChangesSummary' }, { name: 'ProjectInfo' }, { name: 'SessionConfigPropertySchema' }, + { name: 'RepositorySessionConfig' }, { name: 'SessionConfigSchema' }, { name: 'SessionConfigState', mutable: true }, { name: 'Turn', mutable: true }, diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 71926ac2f..d5c13d455 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -758,6 +758,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin { name: 'PendingMessage' }, { name: 'ProjectInfo' }, { name: 'SessionConfigPropertySchema' }, + { name: 'RepositorySessionConfig' }, { name: 'SessionConfigSchema' }, { name: 'SessionConfigState' }, { name: 'Turn' }, diff --git a/scripts/generate-json-schema.test.ts b/scripts/generate-json-schema.test.ts index 539858091..2232dabba 100644 --- a/scripts/generate-json-schema.test.ts +++ b/scripts/generate-json-schema.test.ts @@ -200,6 +200,79 @@ describe('generated JSON schemas', () => { assert.match(expiresIn.description as string, /MUST be a positive integer/); }); + it('keeps repository session descriptors optional and validates their wire shape', () => { + const defs = schema.$defs as Record>; + const configSchema = defs.SessionConfigSchema; + const properties = configSchema.properties as Record>; + const repository = defs.RepositorySessionConfig; + const repositoryProperties = repository.properties as Record>; + + assert.deepEqual(configSchema.required, ['type', 'properties']); + assert.equal(properties.repository.$ref, '#/$defs/RepositorySessionConfig'); + assert.deepEqual(repository.required, ['urlProperty']); + assert.deepEqual(Object.keys(repositoryProperties).sort(), ['revisionProperty', 'urlProperty']); + assert.equal(repositoryProperties.urlProperty.type, 'string'); + assert.equal(repositoryProperties.revisionProperty.type, 'string'); + + const legacy = { + type: 'object', + properties: { mode: { type: 'string', title: 'Mode' } }, + required: ['mode'], + }; + assert.equal(schemaAccepts(schema, configSchema, legacy), true); + + const repositorySchema = { + type: 'object', + properties: { + host_source: { type: 'string', title: 'Repository', readOnly: false, sessionMutable: false }, + host_revision: { type: 'string', title: 'Revision', readOnly: false, sessionMutable: false }, + }, + }; + for (const descriptor of [ + { urlProperty: 'host_source' }, + { urlProperty: 'host_source', revisionProperty: 'host_revision' }, + ]) { + assert.equal( + schemaAccepts(schema, configSchema, { ...repositorySchema, repository: descriptor }), + true, + ); + } + for (const descriptor of [ + {}, + { revisionProperty: 'host_revision' }, + { urlProperty: 42 }, + { urlProperty: 'host_source', revisionProperty: false }, + null, + [], + ]) { + assert.equal( + schemaAccepts(schema, configSchema, { ...repositorySchema, repository: descriptor }), + false, + ); + } + }); + + it('retains generic config inputs for repository-backed creation', () => { + if (file !== 'commands.schema.json') { + return; + } + const defs = schema.$defs as Record>; + const config = { + host_source: 'https://example.org/team/project.git', + host_revision: 'refs/tags/v1.2.3', + mode: 'review', + }; + for (const [definition, channel] of [ + ['ResolveSessionConfigParams', 'ahp-root://'], + ['CreateSessionParams', 'ahp-session:/repository-test'], + ]) { + const properties = defs[definition].properties as Record>; + assert.equal(properties.config.type, 'object'); + assert.equal(properties.repository, undefined); + assert.equal(schemaAccepts(schema, defs[definition], { channel, config }), true); + } + }); + it('constrains every ChatOrigin branch to a distinct kind', () => { const defs = schema.$defs as Record>; const chatOrigin = defs.ChatOrigin; diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index ea13b41e5..5921f428f 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -1746,7 +1746,7 @@ const COMMAND_STRUCTS = [ 'AuthenticateParams', 'AuthenticateResult', 'CreateTerminalParams', 'DisposeTerminalParams', 'ResolveSessionConfigParams', 'ResolveSessionConfigResult', - 'SessionConfigPropertySchema', 'SessionConfigSchema', + 'SessionConfigPropertySchema', 'RepositorySessionConfig', 'SessionConfigSchema', 'SessionConfigCompletionsParams', 'SessionConfigCompletionsResult', 'SessionConfigValueItem', 'CompletionsParams', 'CompletionItem', 'CompletionsResult', diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 4e999bbcf..853b6b1fa 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -819,6 +819,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str { name: 'ChangesSummary' }, { name: 'ProjectInfo' }, { name: 'SessionConfigPropertySchema' }, + { name: 'RepositorySessionConfig' }, { name: 'SessionConfigSchema' }, { name: 'SessionConfigState' }, { name: 'Turn' }, diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 89fa9bcdf..0211b3ac5 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -1652,7 +1652,7 @@ const COMMAND_STRUCTS = [ 'AuthenticateParams', 'AuthenticateResult', 'CreateTerminalParams', 'DisposeTerminalParams', 'ResolveSessionConfigParams', 'ResolveSessionConfigResult', - 'SessionConfigPropertySchema', 'SessionConfigSchema', + 'SessionConfigPropertySchema', 'RepositorySessionConfig', 'SessionConfigSchema', 'SessionConfigCompletionsParams', 'SessionConfigCompletionsResult', 'SessionConfigValueItem', 'CompletionsParams', 'CompletionItem', 'CompletionsResult', diff --git a/types/channels-root/commands.ts b/types/channels-root/commands.ts index ff3c706b7..5dc5b0851 100644 --- a/types/channels-root/commands.ts +++ b/types/channels-root/commands.ts @@ -13,7 +13,7 @@ import type { SessionSummary, SessionConfigSchema } from '../channels-session/st // Re-export schema types so the legacy `commands.ts` aggregator continues to // expose them from the same import path. export type { ConfigPropertySchema, ConfigSchema } from '../common/state.js'; -export type { SessionConfigPropertySchema, SessionConfigSchema } from '../channels-session/state.js'; +export type { RepositorySessionConfig, SessionConfigPropertySchema, SessionConfigSchema } from '../channels-session/state.js'; // ─── listSessions ──────────────────────────────────────────────────────────── @@ -79,6 +79,10 @@ export interface ListSessionsResult extends PaginatedResult { * the full current property set (not a delta). The returned `values` contain * server-resolved defaults to pass to `createSession`. * + * Repository-backed creation is advertised by `schema.repository`. Resolving + * that schema or its values MUST NOT clone or prepare a repository; preparation + * belongs to `createSession`. + * * @category Commands * @method resolveSessionConfig * @direction Client → Server diff --git a/types/channels-root/notifications.ts b/types/channels-root/notifications.ts index eb74fedc5..752d6dc51 100644 --- a/types/channels-root/notifications.ts +++ b/types/channels-root/notifications.ts @@ -175,6 +175,9 @@ export interface SessionSummaryChangedParams { * - Like all notifications this is ephemeral and is **not** replayed on * reconnect. A client that never receives the terminal frame SHOULD expire * the indicator after an idle timeout. + * - Completion of reported work does not establish session readiness. + * Repository-backed creation uses session state and the existing + * `session/ready` or `session/creationFailed` actions for its durable outcome. * * @category Protocol Notifications * @method root/progress diff --git a/types/channels-session/commands.ts b/types/channels-session/commands.ts index 5c452067b..59d820558 100644 --- a/types/channels-session/commands.ts +++ b/types/channels-session/commands.ts @@ -26,6 +26,13 @@ import type { * updates. The server also broadcasts a `root/sessionAdded` notification to all * clients. * + * For repository intent advertised by {@link RepositorySessionConfig}, the + * host MUST authorize the request before repository side effects and prepare + * the repository before executing turns. It MUST publish the requested intent + * in {@link SessionState.config} and any resolved `workingDirectories` before + * `session/ready` or `session/creationFailed`. Clients recover the outcome from + * session state, not progress notifications. + * * @category Commands * @method createSession * @direction Client → Server @@ -67,11 +74,17 @@ export interface CreateSessionParams extends BaseParams { * and ignores the rest. Dispatch working-directory actions to change the set * after the session has started. * + * A non-empty list and repository intent in `config` are mutually exclusive. + * A repository URI is not a working-directory URI. */ workingDirectories?: URI[]; /** * Agent-specific configuration values collected via `resolveSessionConfig`. * Keys and values correspond to the schema returned by the server. + * Repository intent uses only the properties identified by the advertised + * {@link SessionConfigSchema.repository} descriptor. A revision without a + * repository URI is invalid. Omitting repository intent preserves existing + * directory/default behavior. */ config?: Record; /** @@ -104,6 +117,9 @@ export interface CreateSessionParams extends BaseParams { * Disposes a session and cleans up server-side resources. * * The server broadcasts a `root/sessionRemoved` notification to all clients. + * Disposal MUST NOT erase a shared checkout or uncommitted user changes. + * Repository cleanup remains host-owned; ending a client's wait or subscription + * does not grant permission to delete repository data. * * @category Commands * @method disposeSession diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index 78db10000..710421afa 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -187,7 +187,11 @@ export interface SessionState extends SessionMetadata { * this over the session's lifetime. */ defaultChat?: URI; - /** Session configuration schema and current values */ + /** + * Session configuration schema and current values. For repository-backed + * creation, this includes the advertised repository descriptor and requested + * intent, so joining and reconnecting clients can recover it from state. + */ config?: SessionConfigState; /** * Top-level customizations active in this session. @@ -568,6 +572,31 @@ export interface SessionConfigPropertySchema extends ConfigPropertySchema { sessionMutable?: boolean; } +/** + * Opt-in descriptor for preparing one repository during session creation. + * + * Property ids are host-chosen and MUST name distinct entries in + * {@link SessionConfigSchema.properties}. Each referenced property MUST have + * `type: 'string'` and MUST NOT have `readOnly: true` or `sessionMutable: true`. + * Clients MUST use these ids rather than hardcoding repository field names. + * + * Values travel through `resolveSessionConfig.config` and `createSession.config`, + * not a separate command or `_meta`. Schema discovery MUST NOT clone or prepare + * a repository. The host accepts repository intent only when this descriptor + * is advertised. + * + * @category Session Config Types + */ +export interface RepositorySessionConfig { + /** Property id for a credential-free repository URI. */ + urlProperty: string; + /** + * Property id for an optional branch, tag, or commit revision. + * A revision value without a repository URI is invalid. + */ + revisionProperty?: string; +} + /** * A JSON Schema object describing available session configuration metadata. * @@ -580,6 +609,12 @@ export interface SessionConfigSchema { properties: Record; /** JSON Schema: list of required property ids */ required?: string[]; + /** + * Opt-in capability for repository-backed creation using existing config + * properties. The descriptor does not itself require a repository value. + * Without repository intent, existing directory/default behavior is unchanged. + */ + repository?: RepositorySessionConfig; } /** diff --git a/types/test-cases/round-trips/045-session-config-without-repository.json b/types/test-cases/round-trips/045-session-config-without-repository.json new file mode 100644 index 000000000..b2b01bb6b --- /dev/null +++ b/types/test-cases/round-trips/045-session-config-without-repository.json @@ -0,0 +1,50 @@ +{ + "name": "session-config-without-repository", + "group": "A", + "description": "An existing directory-backed session config remains valid without a repository descriptor.", + "type": "Snapshot", + "input": { + "resource": "ahp-session:/directory-session", + "state": { + "provider": "example", + "title": "Directory session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "workingDirectories": ["file:///work/existing"], + "config": { + "schema": { + "type": "object", + "properties": { + "mode": { "type": "string", "title": "Mode", "sessionMutable": true } + } + }, + "values": { "mode": "review" } + } + }, + "fromSeq": 1 + }, + "acceptableOutputs": [{ + "resource": "ahp-session:/directory-session", + "state": { + "provider": "example", + "title": "Directory session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "workingDirectories": ["file:///work/existing"], + "config": { + "schema": { + "type": "object", + "properties": { + "mode": { "type": "string", "title": "Mode", "sessionMutable": true } + } + }, + "values": { "mode": "review" } + } + }, + "fromSeq": 1 + }] +} diff --git a/types/test-cases/round-trips/046-repository-session-url-only.json b/types/test-cases/round-trips/046-repository-session-url-only.json new file mode 100644 index 000000000..d8d715cd5 --- /dev/null +++ b/types/test-cases/round-trips/046-repository-session-url-only.json @@ -0,0 +1,52 @@ +{ + "name": "repository-session-url-only", + "group": "A", + "description": "A ready session preserves a repository descriptor without a revision field, requested URI, and resolved directory.", + "type": "Snapshot", + "input": { + "resource": "ahp-session:/repository-session", + "state": { + "provider": "example", + "title": "Repository session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "workingDirectories": ["file:///work/project"], + "config": { + "schema": { + "type": "object", + "properties": { + "source": { "type": "string", "title": "Repository" } + }, + "repository": { "urlProperty": "source" } + }, + "values": { "source": "https://example.org/team/project.git" } + } + }, + "fromSeq": 2 + }, + "acceptableOutputs": [{ + "resource": "ahp-session:/repository-session", + "state": { + "provider": "example", + "title": "Repository session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "workingDirectories": ["file:///work/project"], + "config": { + "schema": { + "type": "object", + "properties": { + "source": { "type": "string", "title": "Repository" } + }, + "repository": { "urlProperty": "source" } + }, + "values": { "source": "https://example.org/team/project.git" } + } + }, + "fromSeq": 2 + }] +} diff --git a/types/test-cases/round-trips/047-repository-session-revision.json b/types/test-cases/round-trips/047-repository-session-revision.json new file mode 100644 index 000000000..b8a52ef4e --- /dev/null +++ b/types/test-cases/round-trips/047-repository-session-revision.json @@ -0,0 +1,52 @@ +{ + "name": "repository-session-revision", + "group": "A", + "description": "A creating session preserves host-chosen repository and revision field ids and requested values before a directory is resolved.", + "type": "Snapshot", + "input": { + "resource": "ahp-session:/repository-session", + "state": { + "provider": "example", + "title": "Repository session", + "status": 1, + "lifecycle": "creating", + "activeClients": [], + "chats": [], + "config": { + "schema": { + "type": "object", + "properties": { + "source_uri": { "type": "string", "title": "Repository", "readOnly": false, "sessionMutable": false }, + "source_ref": { "type": "string", "title": "Revision", "readOnly": false, "sessionMutable": false } + }, + "repository": { "urlProperty": "source_uri", "revisionProperty": "source_ref" } + }, + "values": { "source_uri": "https://example.org/team/project.git", "source_ref": "refs/tags/v1.2.3" } + } + }, + "fromSeq": 0 + }, + "acceptableOutputs": [{ + "resource": "ahp-session:/repository-session", + "state": { + "provider": "example", + "title": "Repository session", + "status": 1, + "lifecycle": "creating", + "activeClients": [], + "chats": [], + "config": { + "schema": { + "type": "object", + "properties": { + "source_uri": { "type": "string", "title": "Repository", "readOnly": false, "sessionMutable": false }, + "source_ref": { "type": "string", "title": "Revision", "readOnly": false, "sessionMutable": false } + }, + "repository": { "urlProperty": "source_uri", "revisionProperty": "source_ref" } + }, + "values": { "source_uri": "https://example.org/team/project.git", "source_ref": "refs/tags/v1.2.3" } + } + }, + "fromSeq": 0 + }] +} From b6a62eba9b67cbe3252682e9d6a4255d3e1175c6 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 17 Sep 2026 16:10:30 -0700 Subject: [PATCH 2/6] Standardize repository-backed session configuration keys Use optional repositorySource and repositoryRevision values in the existing session config flow. Remove the unreleased descriptor, key mapping, exports, and generator registrations while retaining host-owned preparation, lifecycle, and recovery semantics. Update generated mirrors, documentation, and shared wire and SDK coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 79731f48-d288-483f-8809-136aa141d1eb --- .../Generated/Commands.generated.cs | 46 ++++++---- .../JsonSerializerContext.generated.cs | 1 - .../Generated/State.generated.cs | 44 ++++------ clients/go/ahptypes/commands.generated.go | 46 ++++++---- clients/go/ahptypes/state.generated.go | 39 ++++----- .../generated/Commands.generated.kt | 44 ++++------ .../generated/State.generated.kt | 5 +- clients/rust/crates/ahp-types/src/commands.rs | 46 ++++++---- clients/rust/crates/ahp-types/src/state.rs | 43 ++++------ .../Generated/Commands.generated.swift | 47 ++++------- .../Generated/State.generated.swift | 5 +- clients/typescript/test/client.test.ts | 64 +++++++++----- .../20260915-repository-session-config.json | 2 +- docs/specification/session-channel.md | 49 +++++------ schema/actions.schema.json | 25 +----- schema/commands.schema.json | 35 ++------ schema/errors.schema.json | 35 ++------ schema/notifications.schema.json | 25 +----- schema/state.schema.json | 25 +----- scripts/generate-csharp.ts | 1 - scripts/generate-go.ts | 1 - scripts/generate-json-schema.test.ts | 83 +++++++++---------- scripts/generate-kotlin.ts | 2 +- scripts/generate-rust.ts | 1 - scripts/generate-swift.ts | 2 +- types/channels-root/commands.ts | 19 +++-- types/channels-session/commands.ts | 31 ++++--- types/channels-session/state.ts | 47 ++++------- ...045-session-config-without-repository.json | 2 +- ...> 046-repository-session-source-only.json} | 22 +++-- .../047-repository-session-revision.json | 20 ++--- .../048-repository-session-failed.json | 52 ++++++++++++ 32 files changed, 420 insertions(+), 489 deletions(-) rename types/test-cases/round-trips/{046-repository-session-url-only.json => 046-repository-session-source-only.json} (52%) create mode 100644 types/test-cases/round-trips/048-repository-session-failed.json diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs index 58686432f..96ac60a71 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs @@ -464,12 +464,14 @@ public sealed record SubscribeResult /// updates. The server also broadcasts a `root/sessionAdded` notification to all /// clients. /// -/// For repository intent advertised by {@link RepositorySessionConfig}, the -/// host MUST authorize the request before repository side effects and prepare -/// the repository before executing turns. It MUST publish the requested intent -/// in {@link SessionState.config} and any resolved `workingDirectories` before -/// `session/ready` or `session/creationFailed`. Clients recover the outcome from -/// session state, not progress notifications. +/// For repository intent advertised by {@link SessionConfigSchema.properties}, +/// the host MUST authorize the request before repository side effects and +/// prepare the repository before executing turns. It MUST publish the requested +/// `repositorySource` and optional `repositoryRevision` in +/// {@link SessionState.config} from the initial `creating` snapshot and retain +/// them through `ready` or `failed`. Any resolved `workingDirectories` MUST be +/// published before `session/ready` or `session/creationFailed`. Clients recover +/// the outcome from session state, not progress notifications. public sealed record CreateSessionParams { /// Session URI (client-chosen, e.g. `ahp-session:/<uuid>`) @@ -500,16 +502,21 @@ public sealed record CreateSessionParams /// after the session has started. /// /// A non-empty list and repository intent in `config` are mutually exclusive. - /// A repository URI is not a working-directory URI. + /// A repository URI identifies the source, not a working-directory URI; one + /// source may produce multiple directories. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? WorkingDirectories { get; init; } - /// Agent-specific configuration values collected via `resolveSessionConfig`. + /// Session configuration values collected via `resolveSessionConfig`. /// Keys and values correspond to the schema returned by the server. - /// Repository intent uses only the properties identified by the advertised - /// {@link SessionConfigSchema.repository} descriptor. A revision without a - /// repository URI is invalid. Omitting repository intent preserves existing - /// directory/default behavior. + /// Repository intent uses the standard `repositorySource` and optional + /// `repositoryRevision` keys only when advertised by + /// {@link SessionConfigSchema.properties}. Values MUST be non-empty strings; + /// the source MUST be a credential-free repository URI. A revision without a + /// source, unsupported input, or conflicting directories MUST produce + /// `InvalidParams` (`-32602`), not silently fall back. Omitting repository + /// intent preserves existing directory/default behavior. Other keys remain + /// host-defined. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Dictionary? Config { get; init; } @@ -1365,9 +1372,12 @@ public sealed record DisposeTerminalParams /// the full current property set (not a delta). The returned `values` contain /// server-resolved defaults to pass to `createSession`. /// -/// Repository-backed creation is advertised by `schema.repository`. Resolving -/// that schema or its values MUST NOT clone or prepare a repository; preparation -/// belongs to `createSession`. +/// Repository-backed creation is advertised by a valid +/// `schema.properties.repositorySource`, with optional +/// `schema.properties.repositoryRevision`; see {@link SessionConfigSchema}. +/// Values use those fixed keys in `config`. Resolving the schema or its values, +/// including discovery without a working directory, MUST NOT clone or prepare +/// a repository; preparation belongs to `createSession`. public sealed record ResolveSessionConfigParams { public required string Channel { get; init; } @@ -1386,7 +1396,11 @@ public sealed record ResolveSessionConfigParams [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? WorkingDirectory { get; init; } - /// Current user-filled configuration values + /// Current user-filled configuration values. Repository intent uses + /// `repositorySource` and optional `repositoryRevision` only when advertised + /// by the session config schema. Invalid or unsupported repository input MUST + /// produce `InvalidParams` (`-32602`), not silently select directory/default + /// behavior. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Dictionary? Config { get; init; } } diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs index b07bbd7f4..9b1640215 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs @@ -264,7 +264,6 @@ namespace Microsoft.AgentHostProtocol; [JsonSerializable(typeof(ReconnectResult))] [JsonSerializable(typeof(ReconnectResultType))] [JsonSerializable(typeof(ReconnectSnapshotResult))] -[JsonSerializable(typeof(RepositorySessionConfig))] [JsonSerializable(typeof(ResolveSessionConfigParams))] [JsonSerializable(typeof(ResolveSessionConfigResult))] [JsonSerializable(typeof(ResourceChange))] diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index ef8b93cf5..7ad15ecb4 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -1587,8 +1587,9 @@ public sealed class SessionState public string? DefaultChat { get; set; } /// Session configuration schema and current values. For repository-backed - /// creation, this includes the advertised repository descriptor and requested - /// intent, so joining and reconnecting clients can recover it from state. + /// creation, this includes the advertised standard properties and requested + /// `repositorySource` and optional `repositoryRevision` values throughout + /// `creating`, `ready`, and `failed`, so clients can recover intent from state. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public SessionConfigState? Config { get; set; } @@ -2009,29 +2010,20 @@ public sealed record SessionConfigPropertySchema public bool? SessionMutable { get; init; } } -/// Opt-in descriptor for preparing one repository during session creation. +/// A JSON Schema object describing available session configuration metadata. /// -/// Property ids are host-chosen and MUST name distinct entries in -/// {@link SessionConfigSchema.properties}. Each referenced property MUST have -/// `type: 'string'` and MUST NOT have `readOnly: true` or `sessionMutable: true`. -/// Clients MUST use these ids rather than hardcoding repository field names. +/// Repository-backed creation uses the standard optional config keys +/// `repositorySource` (a credential-free repository URI) and +/// `repositoryRevision` (a branch, tag, or commit). Support is advertised by +/// `properties.repositorySource`; `properties.repositoryRevision` MUST NOT be +/// advertised without it. Each advertised property MUST have `type: 'string'` +/// and MUST NOT have `readOnly: true` or `sessionMutable: true`. /// -/// Values travel through `resolveSessionConfig.config` and `createSession.config`, -/// not a separate command or `_meta`. Schema discovery MUST NOT clone or prepare -/// a repository. The host accepts repository intent only when this descriptor -/// is advertised. -public sealed record RepositorySessionConfig -{ - /// Property id for a credential-free repository URI. - public required string UrlProperty { get; init; } - - /// Property id for an optional branch, tag, or commit revision. - /// A revision value without a repository URI is invalid. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? RevisionProperty { get; init; } -} - -/// A JSON Schema object describing available session configuration metadata. +/// The host MUST NOT accept repository inputs unless their corresponding +/// properties are advertised. Values travel through `resolveSessionConfig.config` +/// and `createSession.config`; schema discovery MUST NOT prepare a repository. +/// Neither key is globally required. Without repository intent, existing +/// directory/default behavior is unchanged. Other property ids remain host-defined. public sealed record SessionConfigSchema { /// JSON Schema: always `'object'` @@ -2043,12 +2035,6 @@ public sealed record SessionConfigSchema /// JSON Schema: list of required property ids [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? Required { get; init; } - - /// Opt-in capability for repository-backed creation using existing config - /// properties. The descriptor does not itself require a repository value. - /// Without repository intent, existing directory/default behavior is unchanged. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public RepositorySessionConfig? Repository { get; init; } } /// Live session configuration metadata. diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index 9ba3fb4c6..51e1b1ca1 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -376,12 +376,14 @@ type SubscribeResult struct { // updates. The server also broadcasts a `root/sessionAdded` notification to all // clients. // -// For repository intent advertised by {@link RepositorySessionConfig}, the -// host MUST authorize the request before repository side effects and prepare -// the repository before executing turns. It MUST publish the requested intent -// in {@link SessionState.config} and any resolved `workingDirectories` before -// `session/ready` or `session/creationFailed`. Clients recover the outcome from -// session state, not progress notifications. +// For repository intent advertised by {@link SessionConfigSchema.properties}, +// the host MUST authorize the request before repository side effects and +// prepare the repository before executing turns. It MUST publish the requested +// `repositorySource` and optional `repositoryRevision` in +// {@link SessionState.config} from the initial `creating` snapshot and retain +// them through `ready` or `failed`. Any resolved `workingDirectories` MUST be +// published before `session/ready` or `session/creationFailed`. Clients recover +// the outcome from session state, not progress notifications. type CreateSessionParams struct { // Channel URI this command targets. Channel URI `json:"channel"` @@ -405,14 +407,19 @@ type CreateSessionParams struct { // after the session has started. // // A non-empty list and repository intent in `config` are mutually exclusive. - // A repository URI is not a working-directory URI. + // A repository URI identifies the source, not a working-directory URI; one + // source may produce multiple directories. WorkingDirectories []URI `json:"workingDirectories,omitempty"` - // Agent-specific configuration values collected via `resolveSessionConfig`. + // Session configuration values collected via `resolveSessionConfig`. // Keys and values correspond to the schema returned by the server. - // Repository intent uses only the properties identified by the advertised - // {@link SessionConfigSchema.repository} descriptor. A revision without a - // repository URI is invalid. Omitting repository intent preserves existing - // directory/default behavior. + // Repository intent uses the standard `repositorySource` and optional + // `repositoryRevision` keys only when advertised by + // {@link SessionConfigSchema.properties}. Values MUST be non-empty strings; + // the source MUST be a credential-free repository URI. A revision without a + // source, unsupported input, or conflicting directories MUST produce + // `InvalidParams` (`-32602`), not silently fall back. Omitting repository + // intent preserves existing directory/default behavior. Other keys remain + // host-defined. Config map[string]json.RawMessage `json:"config,omitempty"` // Eagerly claim an active client role for the new session. // @@ -1088,9 +1095,12 @@ type DisposeTerminalParams struct { // the full current property set (not a delta). The returned `values` contain // server-resolved defaults to pass to `createSession`. // -// Repository-backed creation is advertised by `schema.repository`. Resolving -// that schema or its values MUST NOT clone or prepare a repository; preparation -// belongs to `createSession`. +// Repository-backed creation is advertised by a valid +// `schema.properties.repositorySource`, with optional +// `schema.properties.repositoryRevision`; see {@link SessionConfigSchema}. +// Values use those fixed keys in `config`. Resolving the schema or its values, +// including discovery without a working directory, MUST NOT clone or prepare +// a repository; preparation belongs to `createSession`. type ResolveSessionConfigParams struct { // Channel URI this command targets. Channel URI `json:"channel"` @@ -1101,7 +1111,11 @@ type ResolveSessionConfigParams struct { Provider *string `json:"provider,omitempty"` // Working directory for the session WorkingDirectory *URI `json:"workingDirectory,omitempty"` - // Current user-filled configuration values + // Current user-filled configuration values. Repository intent uses + // `repositorySource` and optional `repositoryRevision` only when advertised + // by the session config schema. Invalid or unsupported repository input MUST + // produce `InvalidParams` (`-32602`), not silently select directory/default + // behavior. Config map[string]json.RawMessage `json:"config,omitempty"` } diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index b51e1c5ec..ce1f59a48 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -906,8 +906,9 @@ type SessionState struct { // this over the session's lifetime. DefaultChat *URI `json:"defaultChat,omitempty"` // Session configuration schema and current values. For repository-backed - // creation, this includes the advertised repository descriptor and requested - // intent, so joining and reconnecting clients can recover it from state. + // creation, this includes the advertised standard properties and requested + // `repositorySource` and optional `repositoryRevision` values throughout + // `creating`, `ready`, and `failed`, so clients can recover intent from state. Config *SessionConfigState `json:"config,omitempty"` // Top-level customizations active in this session. // @@ -1366,26 +1367,20 @@ type SessionConfigPropertySchema struct { SessionMutable *bool `json:"sessionMutable,omitempty"` } -// Opt-in descriptor for preparing one repository during session creation. +// A JSON Schema object describing available session configuration metadata. // -// Property ids are host-chosen and MUST name distinct entries in -// {@link SessionConfigSchema.properties}. Each referenced property MUST have -// `type: 'string'` and MUST NOT have `readOnly: true` or `sessionMutable: true`. -// Clients MUST use these ids rather than hardcoding repository field names. +// Repository-backed creation uses the standard optional config keys +// `repositorySource` (a credential-free repository URI) and +// `repositoryRevision` (a branch, tag, or commit). Support is advertised by +// `properties.repositorySource`; `properties.repositoryRevision` MUST NOT be +// advertised without it. Each advertised property MUST have `type: 'string'` +// and MUST NOT have `readOnly: true` or `sessionMutable: true`. // -// Values travel through `resolveSessionConfig.config` and `createSession.config`, -// not a separate command or `_meta`. Schema discovery MUST NOT clone or prepare -// a repository. The host accepts repository intent only when this descriptor -// is advertised. -type RepositorySessionConfig struct { - // Property id for a credential-free repository URI. - UrlProperty string `json:"urlProperty"` - // Property id for an optional branch, tag, or commit revision. - // A revision value without a repository URI is invalid. - RevisionProperty *string `json:"revisionProperty,omitempty"` -} - -// A JSON Schema object describing available session configuration metadata. +// The host MUST NOT accept repository inputs unless their corresponding +// properties are advertised. Values travel through `resolveSessionConfig.config` +// and `createSession.config`; schema discovery MUST NOT prepare a repository. +// Neither key is globally required. Without repository intent, existing +// directory/default behavior is unchanged. Other property ids remain host-defined. type SessionConfigSchema struct { // JSON Schema: always `'object'` Type string `json:"type"` @@ -1393,10 +1388,6 @@ type SessionConfigSchema struct { Properties map[string]SessionConfigPropertySchema `json:"properties"` // JSON Schema: list of required property ids Required []string `json:"required,omitempty"` - // Opt-in capability for repository-backed creation using existing config - // properties. The descriptor does not itself require a repository value. - // Without repository intent, existing directory/default behavior is unchanged. - Repository *RepositorySessionConfig `json:"repository,omitempty"` } // Live session configuration metadata. 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 3f64c091c..0e8a67af4 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 @@ -619,16 +619,21 @@ data class CreateSessionParams( * after the session has started. * * A non-empty list and repository intent in `config` are mutually exclusive. - * A repository URI is not a working-directory URI. + * A repository URI identifies the source, not a working-directory URI; one + * source may produce multiple directories. */ val workingDirectories: List? = null, /** - * Agent-specific configuration values collected via `resolveSessionConfig`. + * Session configuration values collected via `resolveSessionConfig`. * Keys and values correspond to the schema returned by the server. - * Repository intent uses only the properties identified by the advertised - * {@link SessionConfigSchema.repository} descriptor. A revision without a - * repository URI is invalid. Omitting repository intent preserves existing - * directory/default behavior. + * Repository intent uses the standard `repositorySource` and optional + * `repositoryRevision` keys only when advertised by + * {@link SessionConfigSchema.properties}. Values MUST be non-empty strings; + * the source MUST be a credential-free repository URI. A revision without a + * source, unsupported input, or conflicting directories MUST produce + * `InvalidParams` (`-32602`), not silently fall back. Omitting repository + * intent preserves existing directory/default behavior. Other keys remain + * host-defined. */ val config: Map? = null, /** @@ -1324,7 +1329,11 @@ data class ResolveSessionConfigParams( */ val workingDirectory: String? = null, /** - * Current user-filled configuration values + * Current user-filled configuration values. Repository intent uses + * `repositorySource` and optional `repositoryRevision` only when advertised + * by the session config schema. Invalid or unsupported repository input MUST + * produce `InvalidParams` (`-32602`), not silently select directory/default + * behavior. */ val config: Map? = null ) @@ -1404,19 +1413,6 @@ data class SessionConfigPropertySchema( val sessionMutable: Boolean? = null ) -@Serializable -data class RepositorySessionConfig( - /** - * Property id for a credential-free repository URI. - */ - val urlProperty: String, - /** - * Property id for an optional branch, tag, or commit revision. - * A revision value without a repository URI is invalid. - */ - val revisionProperty: String? = null -) - @Serializable data class SessionConfigSchema( /** @@ -1430,13 +1426,7 @@ data class SessionConfigSchema( /** * JSON Schema: list of required property ids */ - val required: List? = null, - /** - * Opt-in capability for repository-backed creation using existing config - * properties. The descriptor does not itself require a repository value. - * Without repository intent, existing directory/default behavior is unchanged. - */ - val repository: RepositorySessionConfig? = null + val required: List? = null ) @Serializable diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt index b2393ccea..5f9c83ce9 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 @@ -1803,8 +1803,9 @@ data class SessionState( val defaultChat: String? = null, /** * Session configuration schema and current values. For repository-backed - * creation, this includes the advertised repository descriptor and requested - * intent, so joining and reconnecting clients can recover it from state. + * creation, this includes the advertised standard properties and requested + * `repositorySource` and optional `repositoryRevision` values throughout + * `creating`, `ready`, and `failed`, so clients can recover intent from state. */ val config: SessionConfigState? = null, /** diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index e32d95c63..2cad0b487 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -559,12 +559,14 @@ pub struct SubscribeResult { /// updates. The server also broadcasts a `root/sessionAdded` notification to all /// clients. /// -/// For repository intent advertised by {@link RepositorySessionConfig}, the -/// host MUST authorize the request before repository side effects and prepare -/// the repository before executing turns. It MUST publish the requested intent -/// in {@link SessionState.config} and any resolved `workingDirectories` before -/// `session/ready` or `session/creationFailed`. Clients recover the outcome from -/// session state, not progress notifications. +/// For repository intent advertised by {@link SessionConfigSchema.properties}, +/// the host MUST authorize the request before repository side effects and +/// prepare the repository before executing turns. It MUST publish the requested +/// `repositorySource` and optional `repositoryRevision` in +/// {@link SessionState.config} from the initial `creating` snapshot and retain +/// them through `ready` or `failed`. Any resolved `workingDirectories` MUST be +/// published before `session/ready` or `session/creationFailed`. Clients recover +/// the outcome from session state, not progress notifications. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateSessionParams { @@ -592,15 +594,20 @@ pub struct CreateSessionParams { /// after the session has started. /// /// A non-empty list and repository intent in `config` are mutually exclusive. - /// A repository URI is not a working-directory URI. + /// A repository URI identifies the source, not a working-directory URI; one + /// source may produce multiple directories. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, - /// Agent-specific configuration values collected via `resolveSessionConfig`. + /// Session configuration values collected via `resolveSessionConfig`. /// Keys and values correspond to the schema returned by the server. - /// Repository intent uses only the properties identified by the advertised - /// {@link SessionConfigSchema.repository} descriptor. A revision without a - /// repository URI is invalid. Omitting repository intent preserves existing - /// directory/default behavior. + /// Repository intent uses the standard `repositorySource` and optional + /// `repositoryRevision` keys only when advertised by + /// {@link SessionConfigSchema.properties}. Values MUST be non-empty strings; + /// the source MUST be a credential-free repository URI. A revision without a + /// source, unsupported input, or conflicting directories MUST produce + /// `InvalidParams` (`-32602`), not silently fall back. Omitting repository + /// intent preserves existing directory/default behavior. Other keys remain + /// host-defined. #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option, /// Eagerly claim an active client role for the new session. @@ -1392,9 +1399,12 @@ pub struct DisposeTerminalParams { /// the full current property set (not a delta). The returned `values` contain /// server-resolved defaults to pass to `createSession`. /// -/// Repository-backed creation is advertised by `schema.repository`. Resolving -/// that schema or its values MUST NOT clone or prepare a repository; preparation -/// belongs to `createSession`. +/// Repository-backed creation is advertised by a valid +/// `schema.properties.repositorySource`, with optional +/// `schema.properties.repositoryRevision`; see {@link SessionConfigSchema}. +/// Values use those fixed keys in `config`. Resolving the schema or its values, +/// including discovery without a working directory, MUST NOT clone or prepare +/// a repository; preparation belongs to `createSession`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ResolveSessionConfigParams { @@ -1410,7 +1420,11 @@ pub struct ResolveSessionConfigParams { /// Working directory for the session #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directory: Option, - /// Current user-filled configuration values + /// Current user-filled configuration values. Repository intent uses + /// `repositorySource` and optional `repositoryRevision` only when advertised + /// by the session config schema. Invalid or unsupported repository input MUST + /// produce `InvalidParams` (`-32602`), not silently select directory/default + /// behavior. #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option, } diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 2069d901f..635b991ea 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -2054,8 +2054,9 @@ pub struct SessionState { #[serde(default, skip_serializing_if = "Option::is_none")] pub default_chat: Option, /// Session configuration schema and current values. For repository-backed - /// creation, this includes the advertised repository descriptor and requested - /// intent, so joining and reconnecting clients can recover it from state. + /// creation, this includes the advertised standard properties and requested + /// `repositorySource` and optional `repositoryRevision` values throughout + /// `creating`, `ready`, and `failed`, so clients can recover intent from state. #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option, /// Top-level customizations active in this session. @@ -2465,29 +2466,20 @@ pub struct SessionConfigPropertySchema { pub session_mutable: Option, } -/// Opt-in descriptor for preparing one repository during session creation. +/// A JSON Schema object describing available session configuration metadata. /// -/// Property ids are host-chosen and MUST name distinct entries in -/// {@link SessionConfigSchema.properties}. Each referenced property MUST have -/// `type: 'string'` and MUST NOT have `readOnly: true` or `sessionMutable: true`. -/// Clients MUST use these ids rather than hardcoding repository field names. +/// Repository-backed creation uses the standard optional config keys +/// `repositorySource` (a credential-free repository URI) and +/// `repositoryRevision` (a branch, tag, or commit). Support is advertised by +/// `properties.repositorySource`; `properties.repositoryRevision` MUST NOT be +/// advertised without it. Each advertised property MUST have `type: 'string'` +/// and MUST NOT have `readOnly: true` or `sessionMutable: true`. /// -/// Values travel through `resolveSessionConfig.config` and `createSession.config`, -/// not a separate command or `_meta`. Schema discovery MUST NOT clone or prepare -/// a repository. The host accepts repository intent only when this descriptor -/// is advertised. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RepositorySessionConfig { - /// Property id for a credential-free repository URI. - pub url_property: String, - /// Property id for an optional branch, tag, or commit revision. - /// A revision value without a repository URI is invalid. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub revision_property: Option, -} - -/// A JSON Schema object describing available session configuration metadata. +/// The host MUST NOT accept repository inputs unless their corresponding +/// properties are advertised. Values travel through `resolveSessionConfig.config` +/// and `createSession.config`; schema discovery MUST NOT prepare a repository. +/// Neither key is globally required. Without repository intent, existing +/// directory/default behavior is unchanged. Other property ids remain host-defined. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionConfigSchema { @@ -2498,11 +2490,6 @@ pub struct SessionConfigSchema { /// JSON Schema: list of required property ids #[serde(default, skip_serializing_if = "Option::is_none")] pub required: Option>, - /// Opt-in capability for repository-backed creation using existing config - /// properties. The descriptor does not itself require a repository value. - /// Without repository intent, existing directory/default behavior is unchanged. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub repository: Option, } /// Live session configuration metadata. diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index a9328ec0b..967e96c98 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -653,14 +653,19 @@ public struct CreateSessionParams: Codable, Sendable { /// after the session has started. /// /// A non-empty list and repository intent in `config` are mutually exclusive. - /// A repository URI is not a working-directory URI. + /// A repository URI identifies the source, not a working-directory URI; one + /// source may produce multiple directories. public var workingDirectories: [String]? - /// Agent-specific configuration values collected via `resolveSessionConfig`. + /// Session configuration values collected via `resolveSessionConfig`. /// Keys and values correspond to the schema returned by the server. - /// Repository intent uses only the properties identified by the advertised - /// {@link SessionConfigSchema.repository} descriptor. A revision without a - /// repository URI is invalid. Omitting repository intent preserves existing - /// directory/default behavior. + /// Repository intent uses the standard `repositorySource` and optional + /// `repositoryRevision` keys only when advertised by + /// {@link SessionConfigSchema.properties}. Values MUST be non-empty strings; + /// the source MUST be a credential-free repository URI. A revision without a + /// source, unsupported input, or conflicting directories MUST produce + /// `InvalidParams` (`-32602`), not silently fall back. Omitting repository + /// intent preserves existing directory/default behavior. Other keys remain + /// host-defined. public var config: [String: AnyCodable]? /// Eagerly claim an active client role for the new session. /// @@ -1603,7 +1608,11 @@ public struct ResolveSessionConfigParams: Codable, Sendable { public var provider: String? /// Working directory for the session public var workingDirectory: String? - /// Current user-filled configuration values + /// Current user-filled configuration values. Repository intent uses + /// `repositorySource` and optional `repositoryRevision` only when advertised + /// by the session config schema. Invalid or unsupported repository input MUST + /// produce `InvalidParams` (`-32602`), not silently select directory/default + /// behavior. public var config: [String: AnyCodable]? enum CodingKeys: String, CodingKey { @@ -1727,22 +1736,6 @@ public struct SessionConfigPropertySchema: Codable, Sendable { } } -public struct RepositorySessionConfig: Codable, Sendable { - /// Property id for a credential-free repository URI. - public var urlProperty: String - /// Property id for an optional branch, tag, or commit revision. - /// A revision value without a repository URI is invalid. - public var revisionProperty: String? - - public init( - urlProperty: String, - revisionProperty: String? = nil - ) { - self.urlProperty = urlProperty - self.revisionProperty = revisionProperty - } -} - public struct SessionConfigSchema: Codable, Sendable { /// JSON Schema: always `'object'` public var type: String @@ -1750,21 +1743,15 @@ public struct SessionConfigSchema: Codable, Sendable { public var properties: [String: SessionConfigPropertySchema] /// JSON Schema: list of required property ids public var required: [String]? - /// Opt-in capability for repository-backed creation using existing config - /// properties. The descriptor does not itself require a repository value. - /// Without repository intent, existing directory/default behavior is unchanged. - public var repository: RepositorySessionConfig? public init( type: String, properties: [String: SessionConfigPropertySchema], - required: [String]? = nil, - repository: RepositorySessionConfig? = nil + required: [String]? = nil ) { self.type = type self.properties = properties self.required = required - self.repository = repository } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index b4b206f51..11b821124 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -1840,8 +1840,9 @@ public struct SessionState: Codable, Sendable { /// this over the session's lifetime. public var defaultChat: String? /// Session configuration schema and current values. For repository-backed - /// creation, this includes the advertised repository descriptor and requested - /// intent, so joining and reconnecting clients can recover it from state. + /// creation, this includes the advertised standard properties and requested + /// `repositorySource` and optional `repositoryRevision` values throughout + /// `creating`, `ready`, and `failed`, so clients can recover intent from state. public var config: SessionConfigState? /// Top-level customizations active in this session. /// diff --git a/clients/typescript/test/client.test.ts b/clients/typescript/test/client.test.ts index 6026aaa45..5c68acfd2 100644 --- a/clients/typescript/test/client.test.ts +++ b/clients/typescript/test/client.test.ts @@ -45,7 +45,6 @@ import { MessageKind } from '../src/types/channels-chat/state.js'; import { SessionLifecycle, SessionStatus, - type RepositorySessionConfig, type SessionConfigSchema, type SessionState, } from '../src/types/index.js'; @@ -115,26 +114,23 @@ test('initialize round-trip', async () => { await client.shutdown(); }); -for (const revisionProperty of [undefined, 'host_revision']) { - test(`generic session config round-trips repository intent ${revisionProperty ? 'with' : 'without'} a revision`, async t => { +for (const withRevision of [false, true]) { + test(`generic session config round-trips repository intent ${withRevision ? 'with' : 'without'} a revision`, async t => { const [c, s] = InMemoryTransport.pair(); const client = new AhpClient(c); t.after(() => client.shutdown()); client.connect(); - const repository: RepositorySessionConfig = { - urlProperty: 'host_source', - ...(revisionProperty ? { revisionProperty } : {}), - }; const schema: SessionConfigSchema = { type: 'object', properties: { - host_source: { type: 'string', title: 'Repository' }, + repositorySource: { type: 'string', title: 'Repository' }, mode: { type: 'string', title: 'Mode', default: 'review' }, - ...(revisionProperty ? { [revisionProperty]: { type: 'string' as const, title: 'Revision' } } : {}), }, - repository, }; + if (withRevision) { + schema.properties.repositoryRevision = { type: 'string', title: 'Revision' }; + } const discovery = client.request('resolveSessionConfig', { channel: ROOT }); const discoveryRequest = await readRequest(s); @@ -144,12 +140,10 @@ for (const revisionProperty of [undefined, 'host_revision']) { const discovered = await discovery; assert.deepEqual(discovered.schema, schema); - const descriptor = discovered.schema.repository; - assert.ok(descriptor); const config = { ...discovered.values, - [descriptor.urlProperty]: 'https://example.org/team/project.git', - ...(descriptor.revisionProperty ? { [descriptor.revisionProperty]: 'refs/tags/v1.2.3' } : {}), + repositorySource: 'https://example.org/team/project.git', + ...(withRevision ? { repositoryRevision: 'refs/tags/v1.2.3' } : {}), }; const resolution = client.request('resolveSessionConfig', { channel: ROOT, config }); const resolveRequest = await readRequest(s); @@ -169,6 +163,27 @@ for (const revisionProperty of [undefined, 'host_revision']) { }); } +for (const method of ['resolveSessionConfig', 'createSession'] as const) { + test(`${method} surfaces host rejection of repository intent`, async t => { + const [c, s] = InMemoryTransport.pair(); + const client = new AhpClient(c); + t.after(() => client.shutdown()); + client.connect(); + + const channel = method === 'resolveSessionConfig' ? ROOT : 'ahp-session:/repository-test'; + const config = { + repositorySource: 'https://example.org/team/project.git', + repositoryRevision: 'unsupported', + }; + const request = client.request(method, { channel, config }); + const rejected = assert.rejects(request, new RpcError(JsonRpcErrorCodes.InvalidParams, 'Unsupported repository revision')); + const sent = await readRequest(s); + assert.deepEqual({ method: sent.method, params: sent.params }, { method, params: { channel, config } }); + replyError(s, sent.id, JsonRpcErrorCodes.InvalidParams, 'Unsupported repository revision'); + await rejected; + }); +} + for (const failed of [false, true]) { test(`session state recovers repository intent and directories after creation ${failed ? 'fails' : 'succeeds'}`, () => { const resource = 'ahp-session:/repository-test'; @@ -184,12 +199,11 @@ for (const failed of [false, true]) { schema: { type: 'object', properties: { - source: { type: 'string', title: 'Repository' }, - revision: { type: 'string', title: 'Revision' }, + repositorySource: { type: 'string', title: 'Repository' }, + repositoryRevision: { type: 'string', title: 'Revision' }, }, - repository: { urlProperty: 'source', revisionProperty: 'revision' }, }, - values: { source: 'https://example.org/team/project.git', revision: 'main' }, + values: { repositorySource: 'https://example.org/team/project.git', repositoryRevision: 'main' }, }, }; const mirror = new AhpStateMirror(); @@ -200,18 +214,24 @@ for (const failed of [false, true]) { origin: undefined, action: { type: ActionType.SessionWorkingDirectorySet, directory: 'file:///work/project' }, }); + mirror.apply({ + channel: resource, + serverSeq: 2, + origin: undefined, + action: { type: ActionType.SessionWorkingDirectorySet, directory: 'file:///work/project-worktree' }, + }); const preparing = mirror.getSession(resource); assert.ok(preparing); assert.equal(preparing.lifecycle, SessionLifecycle.Creating); assert.deepEqual(preparing.config, initial.config); const joining = new AhpStateMirror(); - joining.applySnapshot({ resource, state: preparing, fromSeq: 1 }); + joining.applySnapshot({ resource, state: preparing, fromSeq: 2 }); assert.deepEqual(joining.getSession(resource), preparing); const completion: ActionEnvelope = { channel: resource, - serverSeq: 2, + serverSeq: 3, origin: undefined, action: failed ? { type: ActionType.SessionCreationFailed, error: { errorType: 'preparationFailed', message: 'Preparation failed' } } @@ -223,14 +243,14 @@ for (const failed of [false, true]) { assert.ok(completed); assert.equal(completed.lifecycle, failed ? SessionLifecycle.Failed : SessionLifecycle.Ready); assert.deepEqual(completed.config, initial.config); - assert.deepEqual(completed.workingDirectories, ['file:///work/project']); + assert.deepEqual(completed.workingDirectories, ['file:///work/project', 'file:///work/project-worktree']); assert.deepEqual(joining.getSession(resource), completed); if (failed) { assert.deepEqual(completed.creationError, { errorType: 'preparationFailed', message: 'Preparation failed' }); } const reconnected = new AhpStateMirror(); - reconnected.applySnapshot({ resource, state: completed, fromSeq: 2 }); + reconnected.applySnapshot({ resource, state: completed, fromSeq: 3 }); assert.deepEqual(reconnected.getSession(resource), completed); }); } diff --git a/docs/.changes/20260915-repository-session-config.json b/docs/.changes/20260915-repository-session-config.json index da83186ff..ab57b6476 100644 --- a/docs/.changes/20260915-repository-session-config.json +++ b/docs/.changes/20260915-repository-session-config.json @@ -1,4 +1,4 @@ { "type": "added", - "message": "Optional `SessionConfigSchema.repository` descriptor for host-owned, repository-backed session creation through existing configuration and lifecycle messages." + "message": "Standard optional `repositorySource` and `repositoryRevision` configuration keys for schema-advertised, host-owned repository-backed session creation through existing configuration and lifecycle messages." } diff --git a/docs/specification/session-channel.md b/docs/specification/session-channel.md index 19402cac8..d0c7f7397 100644 --- a/docs/specification/session-channel.md +++ b/docs/specification/session-channel.md @@ -41,52 +41,39 @@ A host can offer to prepare **one repository for a new session** through the exi ##### Capability and field constraints -The host opts in by returning the optional [`SessionConfigSchema.repository`](/reference/session#sessionconfigschema) descriptor from [`resolveSessionConfig`](/reference/root#resolvesessionconfig): +The host opts in by returning a valid `schema.properties.repositorySource` from [`resolveSessionConfig`](/reference/root#resolvesessionconfig). [`SessionConfigSchema`](/reference/session#sessionconfigschema) remains a generic configuration schema: repository support uses standard property names, not separate repository metadata or a host-selected key mapping. -```ts -export interface RepositorySessionConfig { - urlProperty: string; - revisionProperty?: string; -} -``` - -The descriptor identifies existing entries in `schema.properties`; it does not carry the repository values. - -| Field | Meaning | +| Configuration key | Meaning | |---|---| -| `urlProperty` | Host-chosen property id for a credential-free repository URI. | -| `revisionProperty` | Optional host-chosen property id for a branch, tag, or commit revision. | +| `repositorySource` | Credential-free repository URI string identifying the requested source. | +| `repositoryRevision` | Optional branch, tag, or commit string. | -Every referenced property MUST exist in `schema.properties`, have `type: "string"`, and be writable at creation (`readOnly` MUST NOT be `true`). Neither property may have `sessionMutable: true`: these values describe creation intent, not a request to switch repositories or revisions in an existing session. When `revisionProperty` is present, it MUST differ from `urlProperty`. These relationships are host validation rules; validating the descriptor's JSON shape alone does not check its references to other properties. +Each advertised property MUST have `type: "string"` and be writable at creation (`readOnly` MUST NOT be `true`). Neither property may have `sessionMutable: true`: these values describe creation intent, not a request to switch repositories or revisions in an existing session. `schema.properties.repositoryRevision` is optional and MUST NOT be advertised without a valid `schema.properties.repositorySource`. -Clients MUST use the advertised property ids, not hardcoded names. The descriptor itself is the opt-in capability; clients MUST NOT infer repository support from a provider name, protocol version, `_meta`, or a property whose name happens to resemble a repository field. A host MUST NOT accept repository intent unless it advertises this descriptor. +Clients MUST use these exact keys after checking the advertised properties. They MUST NOT infer repository support from a provider name, protocol version, `_meta`, or another property whose name resembles a repository field. A host MUST NOT accept `repositorySource` unless it advertises a valid source property, and MUST NOT accept `repositoryRevision` unless it advertises a valid revision property. There are no alternate standard keys or aliases. Other configuration keys remain host-defined. -The descriptor does not itself make either value required. The existing `required` list still describes form requirements; AHP adds no globally required repository property. A host without the descriptor, or a request without repository intent, retains its existing directory/default behavior. +Advertising support does not itself make either value required. The existing `required` list still describes form requirements; AHP adds no globally required repository property. A host without repository support, or a request without repository intent, retains its existing directory/default behavior. The generated schema describes the generic configuration shape; the host remains responsible for enforcing these semantic rules. ##### Values and validation Values travel in `resolveSessionConfig.config`, then in `createSession.config`. Discovery and iterative configuration resolution MUST NOT clone or prepare a repository. The host MAY advertise supported URI schemes and revision choices through the existing property descriptions, enums, and completions. -For example, a host may choose `source_uri` and `source_ref`: +For example, directory-free discovery can return: ```json { "schema": { "type": "object", "properties": { - "source_uri": { "type": "string", "title": "Repository" }, - "source_ref": { "type": "string", "title": "Revision" } - }, - "repository": { - "urlProperty": "source_uri", - "revisionProperty": "source_ref" + "repositorySource": { "type": "string", "title": "Repository" }, + "repositoryRevision": { "type": "string", "title": "Revision" } } }, "values": {} } ``` -The client can submit `{"source_uri":"https://example.org/team/project.git","source_ref":"main"}` as `resolveSessionConfig.config`, without a `workingDirectory`, and pass the returned values to creation: +The client can submit `{"repositorySource":"https://example.org/team/project.git","repositoryRevision":"main"}` as `resolveSessionConfig.config`, without a `workingDirectory`, and pass the returned values to creation: ```json { @@ -96,14 +83,16 @@ The client can submit `{"source_uri":"https://example.org/team/project.git","sou "params": { "channel": "ahp-session:/new-session", "config": { - "source_uri": "https://example.org/team/project.git", - "source_ref": "main" + "repositorySource": "https://example.org/team/project.git", + "repositoryRevision": "main" } } } ``` -The repository URI identifies the source, not a host filesystem directory. Repository intent and a non-empty `createSession.workingDirectories` list are mutually exclusive. A revision value without a repository URI is invalid. Omit an unused repository or revision value rather than supplying an empty string. The host MUST reject invalid intent, including conflicting directories or a revision without a repository, with `InvalidParams` (`-32602`), rather than silently selecting a default directory. A repository-aware client MUST surface an invalid or unsupported descriptor instead of silently dropping the user's repository intent. +The repository URI identifies the source, not a checkout or host filesystem directory. One source can produce multiple directories, including separate checkouts or worktrees; clients MUST NOT use the source URI as a directory identity. Repository intent and a non-empty `createSession.workingDirectories` list are mutually exclusive. + +When supplied, each value MUST be a non-empty string. A revision without a source is invalid. Omit an unused source or revision instead of supplying an empty string. For both configuration resolution and creation, the host MUST reject invalid or unsupported intent with `InvalidParams` (`-32602`), including an unadvertised source or revision, a malformed or credential-bearing source URI, an unsupported revision, or conflicting creation directories. It MUST NOT silently drop explicit input, select a default directory, or replace an unsupported revision with its default. A repository-aware client MUST surface invalid capability declarations or unsupported input instead of silently dropping the user's intent. Repository URIs and configuration values MUST NOT contain credentials such as passwords or access tokens. Authentication uses the existing [authentication contract](./authentication); the host MUST authorize the requesting client before repository side effects and use only credentials permitted for that request. Credentials MUST NOT appear in session state, progress messages, or logs. @@ -111,7 +100,7 @@ Repository URIs and configuration values MUST NOT contain credentials such as pa Repository preparation is part of the existing `creating` lifecycle. The host MUST finish preparation before executing turns or publishing `session/ready`. No additional lifecycle state is introduced. -The host MUST publish the accepted, requested repository URI and optional revision under the advertised ids in `SessionState.config.values`, together with the descriptor in `SessionState.config.schema`. Make this intent available in the initial `creating` snapshot so another client joining during preparation can understand the session. Preserve requested intent even if the host resolves a branch or tag to a commit; the resolved working location is a separate fact. +The host MUST publish the accepted, requested source and optional revision as `SessionState.config.values.repositorySource` and `SessionState.config.values.repositoryRevision`, together with their advertised properties in `SessionState.config.schema`. Make this intent available in the initial `creating` snapshot so another client joining during preparation can understand the session. Preserve requested intent even if the host resolves a branch or tag to a commit; the resolved working location is a separate fact. Before dispatching `session/ready` or `session/creationFailed`, the host MUST publish the actual resolved `workingDirectories` in session state, using the existing snapshot and working-directory actions. While no directory has been resolved, `workingDirectories` MAY be absent or empty; do not claim a checkout was prepared when preparation failed. On failure, the existing `session/creationFailed` action records `lifecycle: "failed"` and `creationError`. Both outcomes retain the requested intent and any resolved directories so clients can recover them from a snapshot or replay. @@ -119,13 +108,13 @@ The host MAY report preparation through the existing `createSession.progressToke ##### Reattachment, retry, and cleanup -`createSession` is not an idempotent preparation command. A duplicate URI still returns `SessionAlreadyExists` (`-32003`), including while preparation is running or after creation has failed; it MUST NOT start another preparation for that session. After a lost response, the client should reattach to the same session URI through subscription or [reconnection](./lifecycle#reconnection) and inspect its state. It MUST NOT treat a duplicate creation as successful recovery. After a failure is addressed, a user can explicitly retry with a new session URI rather than overwrite the failed session. +`createSession` is not an idempotent preparation command. A duplicate URI still returns `SessionAlreadyExists` (`-32003`), including while preparation is running or after creation has failed; it MUST NOT start another preparation for that session. After a lost response, the client should reattach to the same session URI through subscription or [reconnection](./lifecycle#reconnection) and inspect its state. Before treating the recovered session as the requested creation, it MUST verify that `config.values.repositorySource` and `config.values.repositoryRevision` match the requested intent and inspect the lifecycle. A mismatch is a conflict, not successful recovery. It MUST NOT treat a duplicate creation error as successful recovery. After a failure is addressed, a user can explicitly retry with a new session URI rather than overwrite the failed session. Cancelling a local wait, disconnecting, or unsubscribing does not grant permission to delete repository data. When the user intends to dispose the session, use the existing `disposeSession` command; this capability adds no cancellation RPC. The host MUST NOT erase a shared checkout or uncommitted user changes during cancellation or disposal. Cleanup of exclusively owned temporary preparation resources remains a host responsibility. ##### Minimal-client behavior -A minimal client can ignore the descriptor, render the ordinary advertised configuration fields, pass resolved values through `config`, and render the existing session lifecycle and `workingDirectories`. It needs neither Git support nor a repository-specific form, clone RPC, or progress implementation. It can also omit this optional creation capability entirely and continue using directory/default creation. A joining or reconnecting client renders the authoritative state without repeating repository preparation. +A minimal client can render the ordinary advertised configuration fields, pass resolved values through `config`, and render the existing session lifecycle and `workingDirectories`. It needs neither Git support nor a repository-specific form, clone RPC, or progress implementation. It can also omit this optional creation capability entirely and continue using directory/default creation. A joining or reconnecting client renders the authoritative state without repeating repository preparation. ### Active session diff --git a/schema/actions.schema.json b/schema/actions.schema.json index fd2470671..f3c73f485 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -3213,7 +3213,7 @@ }, "config": { "$ref": "#/$defs/SessionConfigState", - "description": "Session configuration schema and current values. For repository-backed\ncreation, this includes the advertised repository descriptor and requested\nintent, so joining and reconnecting clients can recover it from state." + "description": "Session configuration schema and current values. For repository-backed\ncreation, this includes the advertised standard properties and requested\n`repositorySource` and optional `repositoryRevision` values throughout\n`creating`, `ready`, and `failed`, so clients can recover intent from state." }, "customizations": { "type": "array", @@ -3635,26 +3635,9 @@ "title" ] }, - "RepositorySessionConfig": { - "type": "object", - "description": "Opt-in descriptor for preparing one repository during session creation.\n\nProperty ids are host-chosen and MUST name distinct entries in\n{@link SessionConfigSchema.properties}. Each referenced property MUST have\n`type: 'string'` and MUST NOT have `readOnly: true` or `sessionMutable: true`.\nClients MUST use these ids rather than hardcoding repository field names.\n\nValues travel through `resolveSessionConfig.config` and `createSession.config`,\nnot a separate command or `_meta`. Schema discovery MUST NOT clone or prepare\na repository. The host accepts repository intent only when this descriptor\nis advertised.", - "properties": { - "urlProperty": { - "type": "string", - "description": "Property id for a credential-free repository URI." - }, - "revisionProperty": { - "type": "string", - "description": "Property id for an optional branch, tag, or commit revision.\nA revision value without a repository URI is invalid." - } - }, - "required": [ - "urlProperty" - ] - }, "SessionConfigSchema": { "type": "object", - "description": "A JSON Schema object describing available session configuration metadata.", + "description": "A JSON Schema object describing available session configuration metadata.\n\nRepository-backed creation uses the standard optional config keys\n`repositorySource` (a credential-free repository URI) and\n`repositoryRevision` (a branch, tag, or commit). Support is advertised by\n`properties.repositorySource`; `properties.repositoryRevision` MUST NOT be\nadvertised without it. Each advertised property MUST have `type: 'string'`\nand MUST NOT have `readOnly: true` or `sessionMutable: true`.\n\nThe host MUST NOT accept repository inputs unless their corresponding\nproperties are advertised. Values travel through `resolveSessionConfig.config`\nand `createSession.config`; schema discovery MUST NOT prepare a repository.\nNeither key is globally required. Without repository intent, existing\ndirectory/default behavior is unchanged. Other property ids remain host-defined.", "properties": { "type": { "type": "string", @@ -3676,10 +3659,6 @@ "type": "string" }, "description": "JSON Schema: list of required property ids" - }, - "repository": { - "$ref": "#/$defs/RepositorySessionConfig", - "description": "Opt-in capability for repository-backed creation using existing config\nproperties. The descriptor does not itself require a repository value.\nWithout repository intent, existing directory/default behavior is unchanged." } }, "required": [ diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 4a6e66d77..629b183ac 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -949,7 +949,7 @@ }, "ResolveSessionConfigParams": { "type": "object", - "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.\n\nRepository-backed creation is advertised by `schema.repository`. Resolving\nthat schema or its values MUST NOT clone or prepare a repository; preparation\nbelongs to `createSession`.", + "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.\n\nRepository-backed creation is advertised by a valid\n`schema.properties.repositorySource`, with optional\n`schema.properties.repositoryRevision`; see {@link SessionConfigSchema}.\nValues use those fixed keys in `config`. Resolving the schema or its values,\nincluding discovery without a working directory, MUST NOT clone or prepare\na repository; preparation belongs to `createSession`.", "properties": { "channel": { "type": "string", @@ -973,7 +973,7 @@ "config": { "type": "object", "additionalProperties": {}, - "description": "Current user-filled configuration values" + "description": "Current user-filled configuration values. Repository intent uses\n`repositorySource` and optional `repositoryRevision` only when advertised\nby the session config schema. Invalid or unsupported repository input MUST\nproduce `InvalidParams` (`-32602`), not silently select directory/default\nbehavior." } }, "required": [ @@ -1081,7 +1081,7 @@ }, "CreateSessionParams": { "type": "object", - "description": "Creates a new session with the specified agent provider.\n\nIf the session URI already exists, the server MUST return an error with code\n`-32003` (`SessionAlreadyExists`).\n\nAfter creation, the client should subscribe to the session URI to receive state\nupdates. The server also broadcasts a `root/sessionAdded` notification to all\nclients.\n\nFor repository intent advertised by {@link RepositorySessionConfig}, the\nhost MUST authorize the request before repository side effects and prepare\nthe repository before executing turns. It MUST publish the requested intent\nin {@link SessionState.config} and any resolved `workingDirectories` before\n`session/ready` or `session/creationFailed`. Clients recover the outcome from\nsession state, not progress notifications.", + "description": "Creates a new session with the specified agent provider.\n\nIf the session URI already exists, the server MUST return an error with code\n`-32003` (`SessionAlreadyExists`).\n\nAfter creation, the client should subscribe to the session URI to receive state\nupdates. The server also broadcasts a `root/sessionAdded` notification to all\nclients.\n\nFor repository intent advertised by {@link SessionConfigSchema.properties},\nthe host MUST authorize the request before repository side effects and\nprepare the repository before executing turns. It MUST publish the requested\n`repositorySource` and optional `repositoryRevision` in\n{@link SessionState.config} from the initial `creating` snapshot and retain\nthem through `ready` or `failed`. Any resolved `workingDirectories` MUST be\npublished before `session/ready` or `session/creationFailed`. Clients recover\nthe outcome from session state, not progress notifications.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -1101,12 +1101,12 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises a protected-primary capability. An\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary | immutable\nprimary} is fixed, while a\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement | replaceable\nprimary} is changed only with `session/workingDirectoryReplaced`.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch working-directory actions to change the set\nafter the session has started.\n\nA non-empty list and repository intent in `config` are mutually exclusive.\nA repository URI is not a working-directory URI." + "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises a protected-primary capability. An\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary | immutable\nprimary} is fixed, while a\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement | replaceable\nprimary} is changed only with `session/workingDirectoryReplaced`.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch working-directory actions to change the set\nafter the session has started.\n\nA non-empty list and repository intent in `config` are mutually exclusive.\nA repository URI identifies the source, not a working-directory URI; one\nsource may produce multiple directories." }, "config": { "type": "object", "additionalProperties": {}, - "description": "Agent-specific configuration values collected via `resolveSessionConfig`.\nKeys and values correspond to the schema returned by the server.\nRepository intent uses only the properties identified by the advertised\n{@link SessionConfigSchema.repository} descriptor. A revision without a\nrepository URI is invalid. Omitting repository intent preserves existing\ndirectory/default behavior." + "description": "Session configuration values collected via `resolveSessionConfig`.\nKeys and values correspond to the schema returned by the server.\nRepository intent uses the standard `repositorySource` and optional\n`repositoryRevision` keys only when advertised by\n{@link SessionConfigSchema.properties}. Values MUST be non-empty strings;\nthe source MUST be a credential-free repository URI. A revision without a\nsource, unsupported input, or conflicting directories MUST produce\n`InvalidParams` (`-32602`), not silently fall back. Omitting repository\nintent preserves existing directory/default behavior. Other keys remain\nhost-defined." }, "activeClient": { "$ref": "#/$defs/SessionActiveClient", @@ -2457,7 +2457,7 @@ }, "config": { "$ref": "#/$defs/SessionConfigState", - "description": "Session configuration schema and current values. For repository-backed\ncreation, this includes the advertised repository descriptor and requested\nintent, so joining and reconnecting clients can recover it from state." + "description": "Session configuration schema and current values. For repository-backed\ncreation, this includes the advertised standard properties and requested\n`repositorySource` and optional `repositoryRevision` values throughout\n`creating`, `ready`, and `failed`, so clients can recover intent from state." }, "customizations": { "type": "array", @@ -2879,26 +2879,9 @@ "title" ] }, - "RepositorySessionConfig": { - "type": "object", - "description": "Opt-in descriptor for preparing one repository during session creation.\n\nProperty ids are host-chosen and MUST name distinct entries in\n{@link SessionConfigSchema.properties}. Each referenced property MUST have\n`type: 'string'` and MUST NOT have `readOnly: true` or `sessionMutable: true`.\nClients MUST use these ids rather than hardcoding repository field names.\n\nValues travel through `resolveSessionConfig.config` and `createSession.config`,\nnot a separate command or `_meta`. Schema discovery MUST NOT clone or prepare\na repository. The host accepts repository intent only when this descriptor\nis advertised.", - "properties": { - "urlProperty": { - "type": "string", - "description": "Property id for a credential-free repository URI." - }, - "revisionProperty": { - "type": "string", - "description": "Property id for an optional branch, tag, or commit revision.\nA revision value without a repository URI is invalid." - } - }, - "required": [ - "urlProperty" - ] - }, "SessionConfigSchema": { "type": "object", - "description": "A JSON Schema object describing available session configuration metadata.", + "description": "A JSON Schema object describing available session configuration metadata.\n\nRepository-backed creation uses the standard optional config keys\n`repositorySource` (a credential-free repository URI) and\n`repositoryRevision` (a branch, tag, or commit). Support is advertised by\n`properties.repositorySource`; `properties.repositoryRevision` MUST NOT be\nadvertised without it. Each advertised property MUST have `type: 'string'`\nand MUST NOT have `readOnly: true` or `sessionMutable: true`.\n\nThe host MUST NOT accept repository inputs unless their corresponding\nproperties are advertised. Values travel through `resolveSessionConfig.config`\nand `createSession.config`; schema discovery MUST NOT prepare a repository.\nNeither key is globally required. Without repository intent, existing\ndirectory/default behavior is unchanged. Other property ids remain host-defined.", "properties": { "type": { "type": "string", @@ -2920,10 +2903,6 @@ "type": "string" }, "description": "JSON Schema: list of required property ids" - }, - "repository": { - "$ref": "#/$defs/RepositorySessionConfig", - "description": "Opt-in capability for repository-backed creation using existing config\nproperties. The descriptor does not itself require a repository value.\nWithout repository intent, existing directory/default behavior is unchanged." } }, "required": [ diff --git a/schema/errors.schema.json b/schema/errors.schema.json index dcca2a03b..8c0860a77 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -886,7 +886,7 @@ }, "config": { "$ref": "#/$defs/SessionConfigState", - "description": "Session configuration schema and current values. For repository-backed\ncreation, this includes the advertised repository descriptor and requested\nintent, so joining and reconnecting clients can recover it from state." + "description": "Session configuration schema and current values. For repository-backed\ncreation, this includes the advertised standard properties and requested\n`repositorySource` and optional `repositoryRevision` values throughout\n`creating`, `ready`, and `failed`, so clients can recover intent from state." }, "customizations": { "type": "array", @@ -1308,26 +1308,9 @@ "title" ] }, - "RepositorySessionConfig": { - "type": "object", - "description": "Opt-in descriptor for preparing one repository during session creation.\n\nProperty ids are host-chosen and MUST name distinct entries in\n{@link SessionConfigSchema.properties}. Each referenced property MUST have\n`type: 'string'` and MUST NOT have `readOnly: true` or `sessionMutable: true`.\nClients MUST use these ids rather than hardcoding repository field names.\n\nValues travel through `resolveSessionConfig.config` and `createSession.config`,\nnot a separate command or `_meta`. Schema discovery MUST NOT clone or prepare\na repository. The host accepts repository intent only when this descriptor\nis advertised.", - "properties": { - "urlProperty": { - "type": "string", - "description": "Property id for a credential-free repository URI." - }, - "revisionProperty": { - "type": "string", - "description": "Property id for an optional branch, tag, or commit revision.\nA revision value without a repository URI is invalid." - } - }, - "required": [ - "urlProperty" - ] - }, "SessionConfigSchema": { "type": "object", - "description": "A JSON Schema object describing available session configuration metadata.", + "description": "A JSON Schema object describing available session configuration metadata.\n\nRepository-backed creation uses the standard optional config keys\n`repositorySource` (a credential-free repository URI) and\n`repositoryRevision` (a branch, tag, or commit). Support is advertised by\n`properties.repositorySource`; `properties.repositoryRevision` MUST NOT be\nadvertised without it. Each advertised property MUST have `type: 'string'`\nand MUST NOT have `readOnly: true` or `sessionMutable: true`.\n\nThe host MUST NOT accept repository inputs unless their corresponding\nproperties are advertised. Values travel through `resolveSessionConfig.config`\nand `createSession.config`; schema discovery MUST NOT prepare a repository.\nNeither key is globally required. Without repository intent, existing\ndirectory/default behavior is unchanged. Other property ids remain host-defined.", "properties": { "type": { "type": "string", @@ -1349,10 +1332,6 @@ "type": "string" }, "description": "JSON Schema: list of required property ids" - }, - "repository": { - "$ref": "#/$defs/RepositorySessionConfig", - "description": "Opt-in capability for repository-backed creation using existing config\nproperties. The descriptor does not itself require a repository value.\nWithout repository intent, existing directory/default behavior is unchanged." } }, "required": [ @@ -6622,7 +6601,7 @@ }, "ResolveSessionConfigParams": { "type": "object", - "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.\n\nRepository-backed creation is advertised by `schema.repository`. Resolving\nthat schema or its values MUST NOT clone or prepare a repository; preparation\nbelongs to `createSession`.", + "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.\n\nRepository-backed creation is advertised by a valid\n`schema.properties.repositorySource`, with optional\n`schema.properties.repositoryRevision`; see {@link SessionConfigSchema}.\nValues use those fixed keys in `config`. Resolving the schema or its values,\nincluding discovery without a working directory, MUST NOT clone or prepare\na repository; preparation belongs to `createSession`.", "properties": { "channel": { "type": "string", @@ -6646,7 +6625,7 @@ "config": { "type": "object", "additionalProperties": {}, - "description": "Current user-filled configuration values" + "description": "Current user-filled configuration values. Repository intent uses\n`repositorySource` and optional `repositoryRevision` only when advertised\nby the session config schema. Invalid or unsupported repository input MUST\nproduce `InvalidParams` (`-32602`), not silently select directory/default\nbehavior." } }, "required": [ @@ -6754,7 +6733,7 @@ }, "CreateSessionParams": { "type": "object", - "description": "Creates a new session with the specified agent provider.\n\nIf the session URI already exists, the server MUST return an error with code\n`-32003` (`SessionAlreadyExists`).\n\nAfter creation, the client should subscribe to the session URI to receive state\nupdates. The server also broadcasts a `root/sessionAdded` notification to all\nclients.\n\nFor repository intent advertised by {@link RepositorySessionConfig}, the\nhost MUST authorize the request before repository side effects and prepare\nthe repository before executing turns. It MUST publish the requested intent\nin {@link SessionState.config} and any resolved `workingDirectories` before\n`session/ready` or `session/creationFailed`. Clients recover the outcome from\nsession state, not progress notifications.", + "description": "Creates a new session with the specified agent provider.\n\nIf the session URI already exists, the server MUST return an error with code\n`-32003` (`SessionAlreadyExists`).\n\nAfter creation, the client should subscribe to the session URI to receive state\nupdates. The server also broadcasts a `root/sessionAdded` notification to all\nclients.\n\nFor repository intent advertised by {@link SessionConfigSchema.properties},\nthe host MUST authorize the request before repository side effects and\nprepare the repository before executing turns. It MUST publish the requested\n`repositorySource` and optional `repositoryRevision` in\n{@link SessionState.config} from the initial `creating` snapshot and retain\nthem through `ready` or `failed`. Any resolved `workingDirectories` MUST be\npublished before `session/ready` or `session/creationFailed`. Clients recover\nthe outcome from session state, not progress notifications.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -6774,12 +6753,12 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises a protected-primary capability. An\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary | immutable\nprimary} is fixed, while a\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement | replaceable\nprimary} is changed only with `session/workingDirectoryReplaced`.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch working-directory actions to change the set\nafter the session has started.\n\nA non-empty list and repository intent in `config` are mutually exclusive.\nA repository URI is not a working-directory URI." + "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises a protected-primary capability. An\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary | immutable\nprimary} is fixed, while a\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement | replaceable\nprimary} is changed only with `session/workingDirectoryReplaced`.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch working-directory actions to change the set\nafter the session has started.\n\nA non-empty list and repository intent in `config` are mutually exclusive.\nA repository URI identifies the source, not a working-directory URI; one\nsource may produce multiple directories." }, "config": { "type": "object", "additionalProperties": {}, - "description": "Agent-specific configuration values collected via `resolveSessionConfig`.\nKeys and values correspond to the schema returned by the server.\nRepository intent uses only the properties identified by the advertised\n{@link SessionConfigSchema.repository} descriptor. A revision without a\nrepository URI is invalid. Omitting repository intent preserves existing\ndirectory/default behavior." + "description": "Session configuration values collected via `resolveSessionConfig`.\nKeys and values correspond to the schema returned by the server.\nRepository intent uses the standard `repositorySource` and optional\n`repositoryRevision` keys only when advertised by\n{@link SessionConfigSchema.properties}. Values MUST be non-empty strings;\nthe source MUST be a credential-free repository URI. A revision without a\nsource, unsupported input, or conflicting directories MUST produce\n`InvalidParams` (`-32602`), not silently fall back. Omitting repository\nintent preserves existing directory/default behavior. Other keys remain\nhost-defined." }, "activeClient": { "$ref": "#/$defs/SessionActiveClient", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 7dbcc476d..a528b0d54 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -1053,7 +1053,7 @@ }, "config": { "$ref": "#/$defs/SessionConfigState", - "description": "Session configuration schema and current values. For repository-backed\ncreation, this includes the advertised repository descriptor and requested\nintent, so joining and reconnecting clients can recover it from state." + "description": "Session configuration schema and current values. For repository-backed\ncreation, this includes the advertised standard properties and requested\n`repositorySource` and optional `repositoryRevision` values throughout\n`creating`, `ready`, and `failed`, so clients can recover intent from state." }, "customizations": { "type": "array", @@ -1475,26 +1475,9 @@ "title" ] }, - "RepositorySessionConfig": { - "type": "object", - "description": "Opt-in descriptor for preparing one repository during session creation.\n\nProperty ids are host-chosen and MUST name distinct entries in\n{@link SessionConfigSchema.properties}. Each referenced property MUST have\n`type: 'string'` and MUST NOT have `readOnly: true` or `sessionMutable: true`.\nClients MUST use these ids rather than hardcoding repository field names.\n\nValues travel through `resolveSessionConfig.config` and `createSession.config`,\nnot a separate command or `_meta`. Schema discovery MUST NOT clone or prepare\na repository. The host accepts repository intent only when this descriptor\nis advertised.", - "properties": { - "urlProperty": { - "type": "string", - "description": "Property id for a credential-free repository URI." - }, - "revisionProperty": { - "type": "string", - "description": "Property id for an optional branch, tag, or commit revision.\nA revision value without a repository URI is invalid." - } - }, - "required": [ - "urlProperty" - ] - }, "SessionConfigSchema": { "type": "object", - "description": "A JSON Schema object describing available session configuration metadata.", + "description": "A JSON Schema object describing available session configuration metadata.\n\nRepository-backed creation uses the standard optional config keys\n`repositorySource` (a credential-free repository URI) and\n`repositoryRevision` (a branch, tag, or commit). Support is advertised by\n`properties.repositorySource`; `properties.repositoryRevision` MUST NOT be\nadvertised without it. Each advertised property MUST have `type: 'string'`\nand MUST NOT have `readOnly: true` or `sessionMutable: true`.\n\nThe host MUST NOT accept repository inputs unless their corresponding\nproperties are advertised. Values travel through `resolveSessionConfig.config`\nand `createSession.config`; schema discovery MUST NOT prepare a repository.\nNeither key is globally required. Without repository intent, existing\ndirectory/default behavior is unchanged. Other property ids remain host-defined.", "properties": { "type": { "type": "string", @@ -1516,10 +1499,6 @@ "type": "string" }, "description": "JSON Schema: list of required property ids" - }, - "repository": { - "$ref": "#/$defs/RepositorySessionConfig", - "description": "Opt-in capability for repository-backed creation using existing config\nproperties. The descriptor does not itself require a repository value.\nWithout repository intent, existing directory/default behavior is unchanged." } }, "required": [ diff --git a/schema/state.schema.json b/schema/state.schema.json index aa1f0b7a9..b87000e99 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -797,7 +797,7 @@ }, "config": { "$ref": "#/$defs/SessionConfigState", - "description": "Session configuration schema and current values. For repository-backed\ncreation, this includes the advertised repository descriptor and requested\nintent, so joining and reconnecting clients can recover it from state." + "description": "Session configuration schema and current values. For repository-backed\ncreation, this includes the advertised standard properties and requested\n`repositorySource` and optional `repositoryRevision` values throughout\n`creating`, `ready`, and `failed`, so clients can recover intent from state." }, "customizations": { "type": "array", @@ -1219,26 +1219,9 @@ "title" ] }, - "RepositorySessionConfig": { - "type": "object", - "description": "Opt-in descriptor for preparing one repository during session creation.\n\nProperty ids are host-chosen and MUST name distinct entries in\n{@link SessionConfigSchema.properties}. Each referenced property MUST have\n`type: 'string'` and MUST NOT have `readOnly: true` or `sessionMutable: true`.\nClients MUST use these ids rather than hardcoding repository field names.\n\nValues travel through `resolveSessionConfig.config` and `createSession.config`,\nnot a separate command or `_meta`. Schema discovery MUST NOT clone or prepare\na repository. The host accepts repository intent only when this descriptor\nis advertised.", - "properties": { - "urlProperty": { - "type": "string", - "description": "Property id for a credential-free repository URI." - }, - "revisionProperty": { - "type": "string", - "description": "Property id for an optional branch, tag, or commit revision.\nA revision value without a repository URI is invalid." - } - }, - "required": [ - "urlProperty" - ] - }, "SessionConfigSchema": { "type": "object", - "description": "A JSON Schema object describing available session configuration metadata.", + "description": "A JSON Schema object describing available session configuration metadata.\n\nRepository-backed creation uses the standard optional config keys\n`repositorySource` (a credential-free repository URI) and\n`repositoryRevision` (a branch, tag, or commit). Support is advertised by\n`properties.repositorySource`; `properties.repositoryRevision` MUST NOT be\nadvertised without it. Each advertised property MUST have `type: 'string'`\nand MUST NOT have `readOnly: true` or `sessionMutable: true`.\n\nThe host MUST NOT accept repository inputs unless their corresponding\nproperties are advertised. Values travel through `resolveSessionConfig.config`\nand `createSession.config`; schema discovery MUST NOT prepare a repository.\nNeither key is globally required. Without repository intent, existing\ndirectory/default behavior is unchanged. Other property ids remain host-defined.", "properties": { "type": { "type": "string", @@ -1260,10 +1243,6 @@ "type": "string" }, "description": "JSON Schema: list of required property ids" - }, - "repository": { - "$ref": "#/$defs/RepositorySessionConfig", - "description": "Opt-in capability for repository-backed creation using existing config\nproperties. The descriptor does not itself require a repository value.\nWithout repository intent, existing directory/default behavior is unchanged." } }, "required": [ diff --git a/scripts/generate-csharp.ts b/scripts/generate-csharp.ts index b95dbb5e0..207212210 100644 --- a/scripts/generate-csharp.ts +++ b/scripts/generate-csharp.ts @@ -691,7 +691,6 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; csName?: strin { name: 'ChangesSummary' }, { name: 'ProjectInfo' }, { name: 'SessionConfigPropertySchema' }, - { name: 'RepositorySessionConfig' }, { name: 'SessionConfigSchema' }, { name: 'SessionConfigState', mutable: true }, { name: 'Turn', mutable: true }, diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index d5c13d455..71926ac2f 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -758,7 +758,6 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin { name: 'PendingMessage' }, { name: 'ProjectInfo' }, { name: 'SessionConfigPropertySchema' }, - { name: 'RepositorySessionConfig' }, { name: 'SessionConfigSchema' }, { name: 'SessionConfigState' }, { name: 'Turn' }, diff --git a/scripts/generate-json-schema.test.ts b/scripts/generate-json-schema.test.ts index 2232dabba..65bac66a0 100644 --- a/scripts/generate-json-schema.test.ts +++ b/scripts/generate-json-schema.test.ts @@ -200,19 +200,21 @@ describe('generated JSON schemas', () => { assert.match(expiresIn.description as string, /MUST be a positive integer/); }); - it('keeps repository session descriptors optional and validates their wire shape', () => { + it('keeps session config schema generic without repository metadata', () => { const defs = schema.$defs as Record>; const configSchema = defs.SessionConfigSchema; const properties = configSchema.properties as Record>; - const repository = defs.RepositorySessionConfig; - const repositoryProperties = repository.properties as Record>; - - assert.deepEqual(configSchema.required, ['type', 'properties']); - assert.equal(properties.repository.$ref, '#/$defs/RepositorySessionConfig'); - assert.deepEqual(repository.required, ['urlProperty']); - assert.deepEqual(Object.keys(repositoryProperties).sort(), ['revisionProperty', 'urlProperty']); - assert.equal(repositoryProperties.urlProperty.type, 'string'); - assert.equal(repositoryProperties.revisionProperty.type, 'string'); + assert.deepEqual({ + fields: Object.keys(properties).sort(), + required: configSchema.required, + propertySchema: properties.properties.additionalProperties, + repositoryType: defs.RepositorySessionConfig, + }, { + fields: ['properties', 'required', 'type'], + required: ['type', 'properties'], + propertySchema: { $ref: '#/$defs/SessionConfigPropertySchema' }, + repositoryType: undefined, + }); const legacy = { type: 'object', @@ -221,35 +223,23 @@ describe('generated JSON schemas', () => { }; assert.equal(schemaAccepts(schema, configSchema, legacy), true); - const repositorySchema = { - type: 'object', - properties: { - host_source: { type: 'string', title: 'Repository', readOnly: false, sessionMutable: false }, - host_revision: { type: 'string', title: 'Revision', readOnly: false, sessionMutable: false }, - }, - }; - for (const descriptor of [ - { urlProperty: 'host_source' }, - { urlProperty: 'host_source', revisionProperty: 'host_revision' }, - ]) { + for (const withRevision of [false, true]) { + const repositorySchema = { + type: 'object', + properties: { + repositorySource: { type: 'string', title: 'Repository', readOnly: false, sessionMutable: false }, + ...(withRevision ? { + repositoryRevision: { type: 'string', title: 'Revision', readOnly: false, sessionMutable: false }, + } : {}), + mode: { type: 'string', title: 'Mode' }, + }, + required: ['mode'], + }; assert.equal( - schemaAccepts(schema, configSchema, { ...repositorySchema, repository: descriptor }), + schemaAccepts(schema, configSchema, repositorySchema), true, ); } - for (const descriptor of [ - {}, - { revisionProperty: 'host_revision' }, - { urlProperty: 42 }, - { urlProperty: 'host_source', revisionProperty: false }, - null, - [], - ]) { - assert.equal( - schemaAccepts(schema, configSchema, { ...repositorySchema, repository: descriptor }), - false, - ); - } }); it('retains generic config inputs for repository-backed creation', () => { @@ -257,19 +247,28 @@ describe('generated JSON schemas', () => { return; } const defs = schema.$defs as Record>; - const config = { - host_source: 'https://example.org/team/project.git', - host_revision: 'refs/tags/v1.2.3', - mode: 'review', - }; for (const [definition, channel] of [ ['ResolveSessionConfigParams', 'ahp-root://'], ['CreateSessionParams', 'ahp-session:/repository-test'], ]) { const properties = defs[definition].properties as Record>; assert.equal(properties.config.type, 'object'); - assert.equal(properties.repository, undefined); - assert.equal(schemaAccepts(schema, defs[definition], { channel, config }), true); + assert.deepEqual( + Object.keys(properties).filter(name => ['repository', 'repositorySource', 'repositoryRevision'].includes(name)), + [], + ); + assert.equal(schemaAccepts(schema, defs[definition], { channel }), true); + for (const config of [ + { mode: 'review' }, + { mode: 'review', repositorySource: 'https://example.org/team/project.git' }, + { + mode: 'review', + repositorySource: 'https://example.org/team/project.git', + repositoryRevision: 'refs/tags/v1.2.3', + }, + ]) { + assert.equal(schemaAccepts(schema, defs[definition], { channel, config }), true); + } } }); diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 5921f428f..ea13b41e5 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -1746,7 +1746,7 @@ const COMMAND_STRUCTS = [ 'AuthenticateParams', 'AuthenticateResult', 'CreateTerminalParams', 'DisposeTerminalParams', 'ResolveSessionConfigParams', 'ResolveSessionConfigResult', - 'SessionConfigPropertySchema', 'RepositorySessionConfig', 'SessionConfigSchema', + 'SessionConfigPropertySchema', 'SessionConfigSchema', 'SessionConfigCompletionsParams', 'SessionConfigCompletionsResult', 'SessionConfigValueItem', 'CompletionsParams', 'CompletionItem', 'CompletionsResult', diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 853b6b1fa..4e999bbcf 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -819,7 +819,6 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str { name: 'ChangesSummary' }, { name: 'ProjectInfo' }, { name: 'SessionConfigPropertySchema' }, - { name: 'RepositorySessionConfig' }, { name: 'SessionConfigSchema' }, { name: 'SessionConfigState' }, { name: 'Turn' }, diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 0211b3ac5..89fa9bcdf 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -1652,7 +1652,7 @@ const COMMAND_STRUCTS = [ 'AuthenticateParams', 'AuthenticateResult', 'CreateTerminalParams', 'DisposeTerminalParams', 'ResolveSessionConfigParams', 'ResolveSessionConfigResult', - 'SessionConfigPropertySchema', 'RepositorySessionConfig', 'SessionConfigSchema', + 'SessionConfigPropertySchema', 'SessionConfigSchema', 'SessionConfigCompletionsParams', 'SessionConfigCompletionsResult', 'SessionConfigValueItem', 'CompletionsParams', 'CompletionItem', 'CompletionsResult', diff --git a/types/channels-root/commands.ts b/types/channels-root/commands.ts index 5dc5b0851..3bc0d265b 100644 --- a/types/channels-root/commands.ts +++ b/types/channels-root/commands.ts @@ -13,7 +13,7 @@ import type { SessionSummary, SessionConfigSchema } from '../channels-session/st // Re-export schema types so the legacy `commands.ts` aggregator continues to // expose them from the same import path. export type { ConfigPropertySchema, ConfigSchema } from '../common/state.js'; -export type { RepositorySessionConfig, SessionConfigPropertySchema, SessionConfigSchema } from '../channels-session/state.js'; +export type { SessionConfigPropertySchema, SessionConfigSchema } from '../channels-session/state.js'; // ─── listSessions ──────────────────────────────────────────────────────────── @@ -79,9 +79,12 @@ export interface ListSessionsResult extends PaginatedResult { * the full current property set (not a delta). The returned `values` contain * server-resolved defaults to pass to `createSession`. * - * Repository-backed creation is advertised by `schema.repository`. Resolving - * that schema or its values MUST NOT clone or prepare a repository; preparation - * belongs to `createSession`. + * Repository-backed creation is advertised by a valid + * `schema.properties.repositorySource`, with optional + * `schema.properties.repositoryRevision`; see {@link SessionConfigSchema}. + * Values use those fixed keys in `config`. Resolving the schema or its values, + * including discovery without a working directory, MUST NOT clone or prepare + * a repository; preparation belongs to `createSession`. * * @category Commands * @method resolveSessionConfig @@ -134,7 +137,13 @@ export interface ResolveSessionConfigParams extends BaseParams { provider?: string; /** Working directory for the session */ workingDirectory?: URI; - /** Current user-filled configuration values */ + /** + * Current user-filled configuration values. Repository intent uses + * `repositorySource` and optional `repositoryRevision` only when advertised + * by the session config schema. Invalid or unsupported repository input MUST + * produce `InvalidParams` (`-32602`), not silently select directory/default + * behavior. + */ config?: Record; } diff --git a/types/channels-session/commands.ts b/types/channels-session/commands.ts index 59d820558..6de9b8650 100644 --- a/types/channels-session/commands.ts +++ b/types/channels-session/commands.ts @@ -26,12 +26,14 @@ import type { * updates. The server also broadcasts a `root/sessionAdded` notification to all * clients. * - * For repository intent advertised by {@link RepositorySessionConfig}, the - * host MUST authorize the request before repository side effects and prepare - * the repository before executing turns. It MUST publish the requested intent - * in {@link SessionState.config} and any resolved `workingDirectories` before - * `session/ready` or `session/creationFailed`. Clients recover the outcome from - * session state, not progress notifications. + * For repository intent advertised by {@link SessionConfigSchema.properties}, + * the host MUST authorize the request before repository side effects and + * prepare the repository before executing turns. It MUST publish the requested + * `repositorySource` and optional `repositoryRevision` in + * {@link SessionState.config} from the initial `creating` snapshot and retain + * them through `ready` or `failed`. Any resolved `workingDirectories` MUST be + * published before `session/ready` or `session/creationFailed`. Clients recover + * the outcome from session state, not progress notifications. * * @category Commands * @method createSession @@ -75,16 +77,21 @@ export interface CreateSessionParams extends BaseParams { * after the session has started. * * A non-empty list and repository intent in `config` are mutually exclusive. - * A repository URI is not a working-directory URI. + * A repository URI identifies the source, not a working-directory URI; one + * source may produce multiple directories. */ workingDirectories?: URI[]; /** - * Agent-specific configuration values collected via `resolveSessionConfig`. + * Session configuration values collected via `resolveSessionConfig`. * Keys and values correspond to the schema returned by the server. - * Repository intent uses only the properties identified by the advertised - * {@link SessionConfigSchema.repository} descriptor. A revision without a - * repository URI is invalid. Omitting repository intent preserves existing - * directory/default behavior. + * Repository intent uses the standard `repositorySource` and optional + * `repositoryRevision` keys only when advertised by + * {@link SessionConfigSchema.properties}. Values MUST be non-empty strings; + * the source MUST be a credential-free repository URI. A revision without a + * source, unsupported input, or conflicting directories MUST produce + * `InvalidParams` (`-32602`), not silently fall back. Omitting repository + * intent preserves existing directory/default behavior. Other keys remain + * host-defined. */ config?: Record; /** diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index 710421afa..fb7fce37c 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -189,8 +189,9 @@ export interface SessionState extends SessionMetadata { defaultChat?: URI; /** * Session configuration schema and current values. For repository-backed - * creation, this includes the advertised repository descriptor and requested - * intent, so joining and reconnecting clients can recover it from state. + * creation, this includes the advertised standard properties and requested + * `repositorySource` and optional `repositoryRevision` values throughout + * `creating`, `ready`, and `failed`, so clients can recover intent from state. */ config?: SessionConfigState; /** @@ -573,32 +574,20 @@ export interface SessionConfigPropertySchema extends ConfigPropertySchema { } /** - * Opt-in descriptor for preparing one repository during session creation. - * - * Property ids are host-chosen and MUST name distinct entries in - * {@link SessionConfigSchema.properties}. Each referenced property MUST have - * `type: 'string'` and MUST NOT have `readOnly: true` or `sessionMutable: true`. - * Clients MUST use these ids rather than hardcoding repository field names. + * A JSON Schema object describing available session configuration metadata. * - * Values travel through `resolveSessionConfig.config` and `createSession.config`, - * not a separate command or `_meta`. Schema discovery MUST NOT clone or prepare - * a repository. The host accepts repository intent only when this descriptor - * is advertised. + * Repository-backed creation uses the standard optional config keys + * `repositorySource` (a credential-free repository URI) and + * `repositoryRevision` (a branch, tag, or commit). Support is advertised by + * `properties.repositorySource`; `properties.repositoryRevision` MUST NOT be + * advertised without it. Each advertised property MUST have `type: 'string'` + * and MUST NOT have `readOnly: true` or `sessionMutable: true`. * - * @category Session Config Types - */ -export interface RepositorySessionConfig { - /** Property id for a credential-free repository URI. */ - urlProperty: string; - /** - * Property id for an optional branch, tag, or commit revision. - * A revision value without a repository URI is invalid. - */ - revisionProperty?: string; -} - -/** - * A JSON Schema object describing available session configuration metadata. + * The host MUST NOT accept repository inputs unless their corresponding + * properties are advertised. Values travel through `resolveSessionConfig.config` + * and `createSession.config`; schema discovery MUST NOT prepare a repository. + * Neither key is globally required. Without repository intent, existing + * directory/default behavior is unchanged. Other property ids remain host-defined. * * @category Session Config Types */ @@ -609,12 +598,6 @@ export interface SessionConfigSchema { properties: Record; /** JSON Schema: list of required property ids */ required?: string[]; - /** - * Opt-in capability for repository-backed creation using existing config - * properties. The descriptor does not itself require a repository value. - * Without repository intent, existing directory/default behavior is unchanged. - */ - repository?: RepositorySessionConfig; } /** diff --git a/types/test-cases/round-trips/045-session-config-without-repository.json b/types/test-cases/round-trips/045-session-config-without-repository.json index b2b01bb6b..1b930eb09 100644 --- a/types/test-cases/round-trips/045-session-config-without-repository.json +++ b/types/test-cases/round-trips/045-session-config-without-repository.json @@ -1,7 +1,7 @@ { "name": "session-config-without-repository", "group": "A", - "description": "An existing directory-backed session config remains valid without a repository descriptor.", + "description": "An existing directory-backed session config remains valid without repository properties or values.", "type": "Snapshot", "input": { "resource": "ahp-session:/directory-session", diff --git a/types/test-cases/round-trips/046-repository-session-url-only.json b/types/test-cases/round-trips/046-repository-session-source-only.json similarity index 52% rename from types/test-cases/round-trips/046-repository-session-url-only.json rename to types/test-cases/round-trips/046-repository-session-source-only.json index d8d715cd5..8dea4511d 100644 --- a/types/test-cases/round-trips/046-repository-session-url-only.json +++ b/types/test-cases/round-trips/046-repository-session-source-only.json @@ -1,7 +1,7 @@ { - "name": "repository-session-url-only", + "name": "repository-session-source-only", "group": "A", - "description": "A ready session preserves a repository descriptor without a revision field, requested URI, and resolved directory.", + "description": "A ready session preserves the standard repositorySource property and requested URI, omits the optional revision, and resolves one source to multiple directories.", "type": "Snapshot", "input": { "resource": "ahp-session:/repository-session", @@ -12,16 +12,15 @@ "lifecycle": "ready", "activeClients": [], "chats": [], - "workingDirectories": ["file:///work/project"], + "workingDirectories": ["file:///work/project", "file:///work/project-worktree"], "config": { "schema": { "type": "object", "properties": { - "source": { "type": "string", "title": "Repository" } - }, - "repository": { "urlProperty": "source" } + "repositorySource": { "type": "string", "title": "Repository" } + } }, - "values": { "source": "https://example.org/team/project.git" } + "values": { "repositorySource": "https://example.org/team/project.git" } } }, "fromSeq": 2 @@ -35,16 +34,15 @@ "lifecycle": "ready", "activeClients": [], "chats": [], - "workingDirectories": ["file:///work/project"], + "workingDirectories": ["file:///work/project", "file:///work/project-worktree"], "config": { "schema": { "type": "object", "properties": { - "source": { "type": "string", "title": "Repository" } - }, - "repository": { "urlProperty": "source" } + "repositorySource": { "type": "string", "title": "Repository" } + } }, - "values": { "source": "https://example.org/team/project.git" } + "values": { "repositorySource": "https://example.org/team/project.git" } } }, "fromSeq": 2 diff --git a/types/test-cases/round-trips/047-repository-session-revision.json b/types/test-cases/round-trips/047-repository-session-revision.json index b8a52ef4e..b22e75a52 100644 --- a/types/test-cases/round-trips/047-repository-session-revision.json +++ b/types/test-cases/round-trips/047-repository-session-revision.json @@ -1,7 +1,7 @@ { "name": "repository-session-revision", "group": "A", - "description": "A creating session preserves host-chosen repository and revision field ids and requested values before a directory is resolved.", + "description": "A creating session preserves the standard repositorySource and repositoryRevision properties and requested values before a directory is resolved.", "type": "Snapshot", "input": { "resource": "ahp-session:/repository-session", @@ -16,12 +16,11 @@ "schema": { "type": "object", "properties": { - "source_uri": { "type": "string", "title": "Repository", "readOnly": false, "sessionMutable": false }, - "source_ref": { "type": "string", "title": "Revision", "readOnly": false, "sessionMutable": false } - }, - "repository": { "urlProperty": "source_uri", "revisionProperty": "source_ref" } + "repositorySource": { "type": "string", "title": "Repository", "readOnly": false, "sessionMutable": false }, + "repositoryRevision": { "type": "string", "title": "Revision", "readOnly": false, "sessionMutable": false } + } }, - "values": { "source_uri": "https://example.org/team/project.git", "source_ref": "refs/tags/v1.2.3" } + "values": { "repositorySource": "https://example.org/team/project.git", "repositoryRevision": "refs/tags/v1.2.3" } } }, "fromSeq": 0 @@ -39,12 +38,11 @@ "schema": { "type": "object", "properties": { - "source_uri": { "type": "string", "title": "Repository", "readOnly": false, "sessionMutable": false }, - "source_ref": { "type": "string", "title": "Revision", "readOnly": false, "sessionMutable": false } - }, - "repository": { "urlProperty": "source_uri", "revisionProperty": "source_ref" } + "repositorySource": { "type": "string", "title": "Repository", "readOnly": false, "sessionMutable": false }, + "repositoryRevision": { "type": "string", "title": "Revision", "readOnly": false, "sessionMutable": false } + } }, - "values": { "source_uri": "https://example.org/team/project.git", "source_ref": "refs/tags/v1.2.3" } + "values": { "repositorySource": "https://example.org/team/project.git", "repositoryRevision": "refs/tags/v1.2.3" } } }, "fromSeq": 0 diff --git a/types/test-cases/round-trips/048-repository-session-failed.json b/types/test-cases/round-trips/048-repository-session-failed.json new file mode 100644 index 000000000..d2bee1f5d --- /dev/null +++ b/types/test-cases/round-trips/048-repository-session-failed.json @@ -0,0 +1,52 @@ +{ + "name": "repository-session-failed", + "group": "A", + "description": "A failed session retains the requested repository source and revision without claiming a directory was resolved.", + "type": "Snapshot", + "input": { + "resource": "ahp-session:/repository-session", + "state": { + "provider": "example", + "title": "Repository session", + "status": 1, + "lifecycle": "failed", + "creationError": { "errorType": "preparationFailed", "message": "Preparation failed" }, + "activeClients": [], + "chats": [], + "config": { + "schema": { + "type": "object", + "properties": { + "repositorySource": { "type": "string", "title": "Repository" }, + "repositoryRevision": { "type": "string", "title": "Revision" } + } + }, + "values": { "repositorySource": "https://example.org/team/project.git", "repositoryRevision": "main" } + } + }, + "fromSeq": 1 + }, + "acceptableOutputs": [{ + "resource": "ahp-session:/repository-session", + "state": { + "provider": "example", + "title": "Repository session", + "status": 1, + "lifecycle": "failed", + "creationError": { "errorType": "preparationFailed", "message": "Preparation failed" }, + "activeClients": [], + "chats": [], + "config": { + "schema": { + "type": "object", + "properties": { + "repositorySource": { "type": "string", "title": "Repository" }, + "repositoryRevision": { "type": "string", "title": "Revision" } + } + }, + "values": { "repositorySource": "https://example.org/team/project.git", "repositoryRevision": "main" } + } + }, + "fromSeq": 1 + }] +} From 572c0342bd1667c566d0709966193ab2c166cfe8 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 17 Sep 2026 16:42:30 -0700 Subject: [PATCH 3/6] docs: Trim repository configuration comments Replace repeated configuration rules with links to the shared schema documentation and keep progress guidance operation-neutral. Retain the normative specification and the source/directory, readiness, and disposal safeguards. Regenerate SDK comments and schema descriptions without changing declarations or wire structure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 79731f48-d288-483f-8809-136aa141d1eb --- .../Generated/Commands.generated.cs | 34 ++++--------------- .../Generated/Notifications.generated.cs | 3 +- clients/go/ahptypes/commands.generated.go | 34 ++++--------------- .../go/ahptypes/notifications.generated.go | 3 +- .../generated/Commands.generated.kt | 16 ++------- clients/rust/crates/ahp-types/src/commands.rs | 34 ++++--------------- .../crates/ahp-types/src/notifications.rs | 3 +- .../Generated/Commands.generated.swift | 16 ++------- schema/commands.schema.json | 8 ++--- schema/errors.schema.json | 8 ++--- schema/notifications.schema.json | 2 +- types/channels-root/commands.ts | 16 ++------- types/channels-root/notifications.ts | 3 +- types/channels-session/commands.ts | 20 ++--------- 14 files changed, 41 insertions(+), 159 deletions(-) diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs index 96ac60a71..203138b05 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs @@ -464,14 +464,8 @@ public sealed record SubscribeResult /// updates. The server also broadcasts a `root/sessionAdded` notification to all /// clients. /// -/// For repository intent advertised by {@link SessionConfigSchema.properties}, -/// the host MUST authorize the request before repository side effects and -/// prepare the repository before executing turns. It MUST publish the requested -/// `repositorySource` and optional `repositoryRevision` in -/// {@link SessionState.config} from the initial `creating` snapshot and retain -/// them through `ready` or `failed`. Any resolved `workingDirectories` MUST be -/// published before `session/ready` or `session/creationFailed`. Clients recover -/// the outcome from session state, not progress notifications. +/// Repository preparation MUST finish before `session/ready` or executing turns. +/// Clients recover the outcome from session state, not progress notifications. public sealed record CreateSessionParams { /// Session URI (client-chosen, e.g. `ahp-session:/<uuid>`) @@ -508,15 +502,7 @@ public sealed record CreateSessionParams public List? WorkingDirectories { get; init; } /// Session configuration values collected via `resolveSessionConfig`. - /// Keys and values correspond to the schema returned by the server. - /// Repository intent uses the standard `repositorySource` and optional - /// `repositoryRevision` keys only when advertised by - /// {@link SessionConfigSchema.properties}. Values MUST be non-empty strings; - /// the source MUST be a credential-free repository URI. A revision without a - /// source, unsupported input, or conflicting directories MUST produce - /// `InvalidParams` (`-32602`), not silently fall back. Omitting repository - /// intent preserves existing directory/default behavior. Other keys remain - /// host-defined. + /// Keys and values follow the advertised {@link SessionConfigSchema}. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Dictionary? Config { get; init; } @@ -1372,12 +1358,8 @@ public sealed record DisposeTerminalParams /// the full current property set (not a delta). The returned `values` contain /// server-resolved defaults to pass to `createSession`. /// -/// Repository-backed creation is advertised by a valid -/// `schema.properties.repositorySource`, with optional -/// `schema.properties.repositoryRevision`; see {@link SessionConfigSchema}. -/// Values use those fixed keys in `config`. Resolving the schema or its values, -/// including discovery without a working directory, MUST NOT clone or prepare -/// a repository; preparation belongs to `createSession`. +/// This command MUST NOT clone or prepare a repository. Standard repository +/// inputs and their advertisement requirements are defined by {@link SessionConfigSchema}. public sealed record ResolveSessionConfigParams { public required string Channel { get; init; } @@ -1396,11 +1378,7 @@ public sealed record ResolveSessionConfigParams [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? WorkingDirectory { get; init; } - /// Current user-filled configuration values. Repository intent uses - /// `repositorySource` and optional `repositoryRevision` only when advertised - /// by the session config schema. Invalid or unsupported repository input MUST - /// produce `InvalidParams` (`-32602`), not silently select directory/default - /// behavior. + /// Current user-filled configuration values; see {@link SessionConfigSchema}. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Dictionary? Config { get; init; } } diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs index 06510d6f8..b4c3e3834 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs @@ -119,8 +119,7 @@ public sealed record SessionSummaryChangedParams /// reconnect. A client that never receives the terminal frame SHOULD expire /// the indicator after an idle timeout. /// - Completion of reported work does not establish session readiness. -/// Repository-backed creation uses session state and the existing -/// `session/ready` or `session/creationFailed` actions for its durable outcome. +/// Observe session lifecycle state for the durable outcome. public sealed record ProgressParams { /// Channel URI this notification belongs to (the root channel). diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index 51e1b1ca1..49d4e6e98 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -376,14 +376,8 @@ type SubscribeResult struct { // updates. The server also broadcasts a `root/sessionAdded` notification to all // clients. // -// For repository intent advertised by {@link SessionConfigSchema.properties}, -// the host MUST authorize the request before repository side effects and -// prepare the repository before executing turns. It MUST publish the requested -// `repositorySource` and optional `repositoryRevision` in -// {@link SessionState.config} from the initial `creating` snapshot and retain -// them through `ready` or `failed`. Any resolved `workingDirectories` MUST be -// published before `session/ready` or `session/creationFailed`. Clients recover -// the outcome from session state, not progress notifications. +// Repository preparation MUST finish before `session/ready` or executing turns. +// Clients recover the outcome from session state, not progress notifications. type CreateSessionParams struct { // Channel URI this command targets. Channel URI `json:"channel"` @@ -411,15 +405,7 @@ type CreateSessionParams struct { // source may produce multiple directories. WorkingDirectories []URI `json:"workingDirectories,omitempty"` // Session configuration values collected via `resolveSessionConfig`. - // Keys and values correspond to the schema returned by the server. - // Repository intent uses the standard `repositorySource` and optional - // `repositoryRevision` keys only when advertised by - // {@link SessionConfigSchema.properties}. Values MUST be non-empty strings; - // the source MUST be a credential-free repository URI. A revision without a - // source, unsupported input, or conflicting directories MUST produce - // `InvalidParams` (`-32602`), not silently fall back. Omitting repository - // intent preserves existing directory/default behavior. Other keys remain - // host-defined. + // Keys and values follow the advertised {@link SessionConfigSchema}. Config map[string]json.RawMessage `json:"config,omitempty"` // Eagerly claim an active client role for the new session. // @@ -1095,12 +1081,8 @@ type DisposeTerminalParams struct { // the full current property set (not a delta). The returned `values` contain // server-resolved defaults to pass to `createSession`. // -// Repository-backed creation is advertised by a valid -// `schema.properties.repositorySource`, with optional -// `schema.properties.repositoryRevision`; see {@link SessionConfigSchema}. -// Values use those fixed keys in `config`. Resolving the schema or its values, -// including discovery without a working directory, MUST NOT clone or prepare -// a repository; preparation belongs to `createSession`. +// This command MUST NOT clone or prepare a repository. Standard repository +// inputs and their advertisement requirements are defined by {@link SessionConfigSchema}. type ResolveSessionConfigParams struct { // Channel URI this command targets. Channel URI `json:"channel"` @@ -1111,11 +1093,7 @@ type ResolveSessionConfigParams struct { Provider *string `json:"provider,omitempty"` // Working directory for the session WorkingDirectory *URI `json:"workingDirectory,omitempty"` - // Current user-filled configuration values. Repository intent uses - // `repositorySource` and optional `repositoryRevision` only when advertised - // by the session config schema. Invalid or unsupported repository input MUST - // produce `InvalidParams` (`-32602`), not silently select directory/default - // behavior. + // Current user-filled configuration values; see {@link SessionConfigSchema}. Config map[string]json.RawMessage `json:"config,omitempty"` } diff --git a/clients/go/ahptypes/notifications.generated.go b/clients/go/ahptypes/notifications.generated.go index 9dab1d823..dc858228e 100644 --- a/clients/go/ahptypes/notifications.generated.go +++ b/clients/go/ahptypes/notifications.generated.go @@ -116,8 +116,7 @@ type SessionSummaryChangedParams struct { // reconnect. A client that never receives the terminal frame SHOULD expire // the indicator after an idle timeout. // - Completion of reported work does not establish session readiness. -// Repository-backed creation uses session state and the existing -// `session/ready` or `session/creationFailed` actions for its durable outcome. +// Observe session lifecycle state for the durable outcome. type ProgressParams struct { // Channel URI this notification belongs to (the root channel). Channel URI `json:"channel"` 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 0e8a67af4..f7307da84 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 @@ -625,15 +625,7 @@ data class CreateSessionParams( val workingDirectories: List? = null, /** * Session configuration values collected via `resolveSessionConfig`. - * Keys and values correspond to the schema returned by the server. - * Repository intent uses the standard `repositorySource` and optional - * `repositoryRevision` keys only when advertised by - * {@link SessionConfigSchema.properties}. Values MUST be non-empty strings; - * the source MUST be a credential-free repository URI. A revision without a - * source, unsupported input, or conflicting directories MUST produce - * `InvalidParams` (`-32602`), not silently fall back. Omitting repository - * intent preserves existing directory/default behavior. Other keys remain - * host-defined. + * Keys and values follow the advertised {@link SessionConfigSchema}. */ val config: Map? = null, /** @@ -1329,11 +1321,7 @@ data class ResolveSessionConfigParams( */ val workingDirectory: String? = null, /** - * Current user-filled configuration values. Repository intent uses - * `repositorySource` and optional `repositoryRevision` only when advertised - * by the session config schema. Invalid or unsupported repository input MUST - * produce `InvalidParams` (`-32602`), not silently select directory/default - * behavior. + * Current user-filled configuration values; see {@link SessionConfigSchema}. */ val config: Map? = null ) diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index 2cad0b487..6352ecd16 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -559,14 +559,8 @@ pub struct SubscribeResult { /// updates. The server also broadcasts a `root/sessionAdded` notification to all /// clients. /// -/// For repository intent advertised by {@link SessionConfigSchema.properties}, -/// the host MUST authorize the request before repository side effects and -/// prepare the repository before executing turns. It MUST publish the requested -/// `repositorySource` and optional `repositoryRevision` in -/// {@link SessionState.config} from the initial `creating` snapshot and retain -/// them through `ready` or `failed`. Any resolved `workingDirectories` MUST be -/// published before `session/ready` or `session/creationFailed`. Clients recover -/// the outcome from session state, not progress notifications. +/// Repository preparation MUST finish before `session/ready` or executing turns. +/// Clients recover the outcome from session state, not progress notifications. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateSessionParams { @@ -599,15 +593,7 @@ pub struct CreateSessionParams { #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, /// Session configuration values collected via `resolveSessionConfig`. - /// Keys and values correspond to the schema returned by the server. - /// Repository intent uses the standard `repositorySource` and optional - /// `repositoryRevision` keys only when advertised by - /// {@link SessionConfigSchema.properties}. Values MUST be non-empty strings; - /// the source MUST be a credential-free repository URI. A revision without a - /// source, unsupported input, or conflicting directories MUST produce - /// `InvalidParams` (`-32602`), not silently fall back. Omitting repository - /// intent preserves existing directory/default behavior. Other keys remain - /// host-defined. + /// Keys and values follow the advertised {@link SessionConfigSchema}. #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option, /// Eagerly claim an active client role for the new session. @@ -1399,12 +1385,8 @@ pub struct DisposeTerminalParams { /// the full current property set (not a delta). The returned `values` contain /// server-resolved defaults to pass to `createSession`. /// -/// Repository-backed creation is advertised by a valid -/// `schema.properties.repositorySource`, with optional -/// `schema.properties.repositoryRevision`; see {@link SessionConfigSchema}. -/// Values use those fixed keys in `config`. Resolving the schema or its values, -/// including discovery without a working directory, MUST NOT clone or prepare -/// a repository; preparation belongs to `createSession`. +/// This command MUST NOT clone or prepare a repository. Standard repository +/// inputs and their advertisement requirements are defined by {@link SessionConfigSchema}. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ResolveSessionConfigParams { @@ -1420,11 +1402,7 @@ pub struct ResolveSessionConfigParams { /// Working directory for the session #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directory: Option, - /// Current user-filled configuration values. Repository intent uses - /// `repositorySource` and optional `repositoryRevision` only when advertised - /// by the session config schema. Invalid or unsupported repository input MUST - /// produce `InvalidParams` (`-32602`), not silently select directory/default - /// behavior. + /// Current user-filled configuration values; see {@link SessionConfigSchema}. #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option, } diff --git a/clients/rust/crates/ahp-types/src/notifications.rs b/clients/rust/crates/ahp-types/src/notifications.rs index bee3ba0be..447ce60ac 100644 --- a/clients/rust/crates/ahp-types/src/notifications.rs +++ b/clients/rust/crates/ahp-types/src/notifications.rs @@ -154,8 +154,7 @@ pub struct SessionSummaryChangedParams { /// reconnect. A client that never receives the terminal frame SHOULD expire /// the indicator after an idle timeout. /// - Completion of reported work does not establish session readiness. -/// Repository-backed creation uses session state and the existing -/// `session/ready` or `session/creationFailed` actions for its durable outcome. +/// Observe session lifecycle state for the durable outcome. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ProgressParams { diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index 967e96c98..c20120d0b 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -657,15 +657,7 @@ public struct CreateSessionParams: Codable, Sendable { /// source may produce multiple directories. public var workingDirectories: [String]? /// Session configuration values collected via `resolveSessionConfig`. - /// Keys and values correspond to the schema returned by the server. - /// Repository intent uses the standard `repositorySource` and optional - /// `repositoryRevision` keys only when advertised by - /// {@link SessionConfigSchema.properties}. Values MUST be non-empty strings; - /// the source MUST be a credential-free repository URI. A revision without a - /// source, unsupported input, or conflicting directories MUST produce - /// `InvalidParams` (`-32602`), not silently fall back. Omitting repository - /// intent preserves existing directory/default behavior. Other keys remain - /// host-defined. + /// Keys and values follow the advertised {@link SessionConfigSchema}. public var config: [String: AnyCodable]? /// Eagerly claim an active client role for the new session. /// @@ -1608,11 +1600,7 @@ public struct ResolveSessionConfigParams: Codable, Sendable { public var provider: String? /// Working directory for the session public var workingDirectory: String? - /// Current user-filled configuration values. Repository intent uses - /// `repositorySource` and optional `repositoryRevision` only when advertised - /// by the session config schema. Invalid or unsupported repository input MUST - /// produce `InvalidParams` (`-32602`), not silently select directory/default - /// behavior. + /// Current user-filled configuration values; see {@link SessionConfigSchema}. public var config: [String: AnyCodable]? enum CodingKeys: String, CodingKey { diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 629b183ac..cb6f7eed9 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -949,7 +949,7 @@ }, "ResolveSessionConfigParams": { "type": "object", - "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.\n\nRepository-backed creation is advertised by a valid\n`schema.properties.repositorySource`, with optional\n`schema.properties.repositoryRevision`; see {@link SessionConfigSchema}.\nValues use those fixed keys in `config`. Resolving the schema or its values,\nincluding discovery without a working directory, MUST NOT clone or prepare\na repository; preparation belongs to `createSession`.", + "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.\n\nThis command MUST NOT clone or prepare a repository. Standard repository\ninputs and their advertisement requirements are defined by {@link SessionConfigSchema}.", "properties": { "channel": { "type": "string", @@ -973,7 +973,7 @@ "config": { "type": "object", "additionalProperties": {}, - "description": "Current user-filled configuration values. Repository intent uses\n`repositorySource` and optional `repositoryRevision` only when advertised\nby the session config schema. Invalid or unsupported repository input MUST\nproduce `InvalidParams` (`-32602`), not silently select directory/default\nbehavior." + "description": "Current user-filled configuration values; see {@link SessionConfigSchema}." } }, "required": [ @@ -1081,7 +1081,7 @@ }, "CreateSessionParams": { "type": "object", - "description": "Creates a new session with the specified agent provider.\n\nIf the session URI already exists, the server MUST return an error with code\n`-32003` (`SessionAlreadyExists`).\n\nAfter creation, the client should subscribe to the session URI to receive state\nupdates. The server also broadcasts a `root/sessionAdded` notification to all\nclients.\n\nFor repository intent advertised by {@link SessionConfigSchema.properties},\nthe host MUST authorize the request before repository side effects and\nprepare the repository before executing turns. It MUST publish the requested\n`repositorySource` and optional `repositoryRevision` in\n{@link SessionState.config} from the initial `creating` snapshot and retain\nthem through `ready` or `failed`. Any resolved `workingDirectories` MUST be\npublished before `session/ready` or `session/creationFailed`. Clients recover\nthe outcome from session state, not progress notifications.", + "description": "Creates a new session with the specified agent provider.\n\nIf the session URI already exists, the server MUST return an error with code\n`-32003` (`SessionAlreadyExists`).\n\nAfter creation, the client should subscribe to the session URI to receive state\nupdates. The server also broadcasts a `root/sessionAdded` notification to all\nclients.\n\nRepository preparation MUST finish before `session/ready` or executing turns.\nClients recover the outcome from session state, not progress notifications.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -1106,7 +1106,7 @@ "config": { "type": "object", "additionalProperties": {}, - "description": "Session configuration values collected via `resolveSessionConfig`.\nKeys and values correspond to the schema returned by the server.\nRepository intent uses the standard `repositorySource` and optional\n`repositoryRevision` keys only when advertised by\n{@link SessionConfigSchema.properties}. Values MUST be non-empty strings;\nthe source MUST be a credential-free repository URI. A revision without a\nsource, unsupported input, or conflicting directories MUST produce\n`InvalidParams` (`-32602`), not silently fall back. Omitting repository\nintent preserves existing directory/default behavior. Other keys remain\nhost-defined." + "description": "Session configuration values collected via `resolveSessionConfig`.\nKeys and values follow the advertised {@link SessionConfigSchema}." }, "activeClient": { "$ref": "#/$defs/SessionActiveClient", diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 8c0860a77..a513ea799 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -6601,7 +6601,7 @@ }, "ResolveSessionConfigParams": { "type": "object", - "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.\n\nRepository-backed creation is advertised by a valid\n`schema.properties.repositorySource`, with optional\n`schema.properties.repositoryRevision`; see {@link SessionConfigSchema}.\nValues use those fixed keys in `config`. Resolving the schema or its values,\nincluding discovery without a working directory, MUST NOT clone or prepare\na repository; preparation belongs to `createSession`.", + "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.\n\nThis command MUST NOT clone or prepare a repository. Standard repository\ninputs and their advertisement requirements are defined by {@link SessionConfigSchema}.", "properties": { "channel": { "type": "string", @@ -6625,7 +6625,7 @@ "config": { "type": "object", "additionalProperties": {}, - "description": "Current user-filled configuration values. Repository intent uses\n`repositorySource` and optional `repositoryRevision` only when advertised\nby the session config schema. Invalid or unsupported repository input MUST\nproduce `InvalidParams` (`-32602`), not silently select directory/default\nbehavior." + "description": "Current user-filled configuration values; see {@link SessionConfigSchema}." } }, "required": [ @@ -6733,7 +6733,7 @@ }, "CreateSessionParams": { "type": "object", - "description": "Creates a new session with the specified agent provider.\n\nIf the session URI already exists, the server MUST return an error with code\n`-32003` (`SessionAlreadyExists`).\n\nAfter creation, the client should subscribe to the session URI to receive state\nupdates. The server also broadcasts a `root/sessionAdded` notification to all\nclients.\n\nFor repository intent advertised by {@link SessionConfigSchema.properties},\nthe host MUST authorize the request before repository side effects and\nprepare the repository before executing turns. It MUST publish the requested\n`repositorySource` and optional `repositoryRevision` in\n{@link SessionState.config} from the initial `creating` snapshot and retain\nthem through `ready` or `failed`. Any resolved `workingDirectories` MUST be\npublished before `session/ready` or `session/creationFailed`. Clients recover\nthe outcome from session state, not progress notifications.", + "description": "Creates a new session with the specified agent provider.\n\nIf the session URI already exists, the server MUST return an error with code\n`-32003` (`SessionAlreadyExists`).\n\nAfter creation, the client should subscribe to the session URI to receive state\nupdates. The server also broadcasts a `root/sessionAdded` notification to all\nclients.\n\nRepository preparation MUST finish before `session/ready` or executing turns.\nClients recover the outcome from session state, not progress notifications.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -6758,7 +6758,7 @@ "config": { "type": "object", "additionalProperties": {}, - "description": "Session configuration values collected via `resolveSessionConfig`.\nKeys and values correspond to the schema returned by the server.\nRepository intent uses the standard `repositorySource` and optional\n`repositoryRevision` keys only when advertised by\n{@link SessionConfigSchema.properties}. Values MUST be non-empty strings;\nthe source MUST be a credential-free repository URI. A revision without a\nsource, unsupported input, or conflicting directories MUST produce\n`InvalidParams` (`-32602`), not silently fall back. Omitting repository\nintent preserves existing directory/default behavior. Other keys remain\nhost-defined." + "description": "Session configuration values collected via `resolveSessionConfig`.\nKeys and values follow the advertised {@link SessionConfigSchema}." }, "activeClient": { "$ref": "#/$defs/SessionActiveClient", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index a528b0d54..2c346f337 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -146,7 +146,7 @@ }, "ProgressParams": { "type": "object", - "description": "Generic progress notification for a long-running operation.\n\nA client opts in to progress for a request by including a `progressToken` in\nthat request (today: the `progressToken` field on `createSession`). If the\nserver does long-running work to service the request — e.g. lazily\ndownloading an agent's native SDK the first time a session of that provider\nis materialized — it emits `progress` notifications carrying the same token.\n\nThe notification is operation-agnostic: it says nothing about *what* is\nprogressing. The client correlates `progressToken` back to the request it\noriginated from (and thus the UI surface awaiting it) and renders its own\nlocalized indicator. The same channel serves any future long-running\noperation without a new method.\n\nSemantics:\n\n- `progress` is monotonically non-decreasing for a given `progressToken`.\n- `total` is present only when the server knows the magnitude up front\n (e.g. a `Content-Length`); when absent the client SHOULD show an\n indeterminate indicator.\n- The operation is complete when `progress === total`. The server MUST emit a\n final frame satisfying `progress === total`; when the total was never\n known, it sets `total` to the final `progress` on that frame. No further\n frames reference the token afterwards.\n- The server MAY emit no progress at all (e.g. the work was already done);\n the client then never shows an indicator.\n- Like all notifications this is ephemeral and is **not** replayed on\n reconnect. A client that never receives the terminal frame SHOULD expire\n the indicator after an idle timeout.\n- Completion of reported work does not establish session readiness.\n Repository-backed creation uses session state and the existing\n `session/ready` or `session/creationFailed` actions for its durable outcome.", + "description": "Generic progress notification for a long-running operation.\n\nA client opts in to progress for a request by including a `progressToken` in\nthat request (today: the `progressToken` field on `createSession`). If the\nserver does long-running work to service the request — e.g. lazily\ndownloading an agent's native SDK the first time a session of that provider\nis materialized — it emits `progress` notifications carrying the same token.\n\nThe notification is operation-agnostic: it says nothing about *what* is\nprogressing. The client correlates `progressToken` back to the request it\noriginated from (and thus the UI surface awaiting it) and renders its own\nlocalized indicator. The same channel serves any future long-running\noperation without a new method.\n\nSemantics:\n\n- `progress` is monotonically non-decreasing for a given `progressToken`.\n- `total` is present only when the server knows the magnitude up front\n (e.g. a `Content-Length`); when absent the client SHOULD show an\n indeterminate indicator.\n- The operation is complete when `progress === total`. The server MUST emit a\n final frame satisfying `progress === total`; when the total was never\n known, it sets `total` to the final `progress` on that frame. No further\n frames reference the token afterwards.\n- The server MAY emit no progress at all (e.g. the work was already done);\n the client then never shows an indicator.\n- Like all notifications this is ephemeral and is **not** replayed on\n reconnect. A client that never receives the terminal frame SHOULD expire\n the indicator after an idle timeout.\n- Completion of reported work does not establish session readiness.\n Observe session lifecycle state for the durable outcome.", "properties": { "channel": { "$ref": "#/$defs/URI", diff --git a/types/channels-root/commands.ts b/types/channels-root/commands.ts index 3bc0d265b..d85bfb353 100644 --- a/types/channels-root/commands.ts +++ b/types/channels-root/commands.ts @@ -79,12 +79,8 @@ export interface ListSessionsResult extends PaginatedResult { * the full current property set (not a delta). The returned `values` contain * server-resolved defaults to pass to `createSession`. * - * Repository-backed creation is advertised by a valid - * `schema.properties.repositorySource`, with optional - * `schema.properties.repositoryRevision`; see {@link SessionConfigSchema}. - * Values use those fixed keys in `config`. Resolving the schema or its values, - * including discovery without a working directory, MUST NOT clone or prepare - * a repository; preparation belongs to `createSession`. + * This command MUST NOT clone or prepare a repository. Standard repository + * inputs and their advertisement requirements are defined by {@link SessionConfigSchema}. * * @category Commands * @method resolveSessionConfig @@ -137,13 +133,7 @@ export interface ResolveSessionConfigParams extends BaseParams { provider?: string; /** Working directory for the session */ workingDirectory?: URI; - /** - * Current user-filled configuration values. Repository intent uses - * `repositorySource` and optional `repositoryRevision` only when advertised - * by the session config schema. Invalid or unsupported repository input MUST - * produce `InvalidParams` (`-32602`), not silently select directory/default - * behavior. - */ + /** Current user-filled configuration values; see {@link SessionConfigSchema}. */ config?: Record; } diff --git a/types/channels-root/notifications.ts b/types/channels-root/notifications.ts index 752d6dc51..8ae4aebd0 100644 --- a/types/channels-root/notifications.ts +++ b/types/channels-root/notifications.ts @@ -176,8 +176,7 @@ export interface SessionSummaryChangedParams { * reconnect. A client that never receives the terminal frame SHOULD expire * the indicator after an idle timeout. * - Completion of reported work does not establish session readiness. - * Repository-backed creation uses session state and the existing - * `session/ready` or `session/creationFailed` actions for its durable outcome. + * Observe session lifecycle state for the durable outcome. * * @category Protocol Notifications * @method root/progress diff --git a/types/channels-session/commands.ts b/types/channels-session/commands.ts index 6de9b8650..c77c77173 100644 --- a/types/channels-session/commands.ts +++ b/types/channels-session/commands.ts @@ -26,14 +26,8 @@ import type { * updates. The server also broadcasts a `root/sessionAdded` notification to all * clients. * - * For repository intent advertised by {@link SessionConfigSchema.properties}, - * the host MUST authorize the request before repository side effects and - * prepare the repository before executing turns. It MUST publish the requested - * `repositorySource` and optional `repositoryRevision` in - * {@link SessionState.config} from the initial `creating` snapshot and retain - * them through `ready` or `failed`. Any resolved `workingDirectories` MUST be - * published before `session/ready` or `session/creationFailed`. Clients recover - * the outcome from session state, not progress notifications. + * Repository preparation MUST finish before `session/ready` or executing turns. + * Clients recover the outcome from session state, not progress notifications. * * @category Commands * @method createSession @@ -83,15 +77,7 @@ export interface CreateSessionParams extends BaseParams { workingDirectories?: URI[]; /** * Session configuration values collected via `resolveSessionConfig`. - * Keys and values correspond to the schema returned by the server. - * Repository intent uses the standard `repositorySource` and optional - * `repositoryRevision` keys only when advertised by - * {@link SessionConfigSchema.properties}. Values MUST be non-empty strings; - * the source MUST be a credential-free repository URI. A revision without a - * source, unsupported input, or conflicting directories MUST produce - * `InvalidParams` (`-32602`), not silently fall back. Omitting repository - * intent preserves existing directory/default behavior. Other keys remain - * host-defined. + * Keys and values follow the advertised {@link SessionConfigSchema}. */ config?: Record; /** From fa44ef3fae7c4750012024c106b70577e7a027c4 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 17 Sep 2026 17:58:44 -0700 Subject: [PATCH 4/6] feat: Add typed repository source inputs Move repository source and revision out of provider configuration into existing request types and immutable session metadata. Advertise source and revision support explicitly through agent capabilities and keep the preparation lifecycle unchanged. Regenerate all SDKs and schemas, and cover typed requests, capability discovery, and recoverable state in shared wire fixtures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 79731f48-d288-483f-8809-136aa141d1eb --- .../Generated/Commands.generated.cs | 30 +++++++- .../JsonSerializerContext.generated.cs | 1 + .../Generated/Notifications.generated.cs | 8 +++ .../Generated/State.generated.cs | 48 ++++++++----- clients/go/ahptypes/commands.generated.go | 18 ++++- .../go/ahptypes/notifications.generated.go | 4 ++ clients/go/ahptypes/state.generated.go | 34 ++++----- .../generated/Commands.generated.kt | 26 ++++++- .../generated/Notifications.generated.kt | 8 +++ .../generated/State.generated.kt | 33 +++++++-- clients/rust/crates/ahp-types/src/commands.rs | 24 ++++++- .../crates/ahp-types/src/notifications.rs | 6 ++ clients/rust/crates/ahp-types/src/state.rs | 42 ++++++----- .../Generated/Commands.generated.swift | 32 ++++++++- .../Generated/Notifications.generated.swift | 10 +++ .../Generated/State.generated.swift | 40 +++++++++-- clients/typescript/test/client.test.ts | 51 +++++++------- .../20260915-repository-session-config.json | 2 +- docs/specification/session-channel.md | 46 ++++++------ schema/actions.schema.json | 42 ++++++++++- schema/commands.schema.json | 70 +++++++++++++++++-- schema/errors.schema.json | 70 +++++++++++++++++-- schema/notifications.schema.json | 50 ++++++++++++- schema/state.schema.json | 42 ++++++++++- scripts/generate-csharp.ts | 1 + scripts/generate-go.ts | 1 + scripts/generate-json-schema.test.ts | 53 +++++++------- scripts/generate-kotlin.ts | 1 + scripts/generate-rust.ts | 1 + scripts/generate-swift.ts | 1 + types/channels-root/commands.ts | 12 +++- types/channels-root/state.ts | 11 +++ types/channels-session/commands.ts | 6 +- types/channels-session/state.ts | 24 ++----- .../046-repository-session-source-only.json | 26 ++----- .../047-repository-session-revision.json | 26 ++----- .../048-repository-session-failed.json | 24 ++----- .../049-repository-source-capability.json | 58 +++++++++++++++ 38 files changed, 736 insertions(+), 246 deletions(-) create mode 100644 types/test-cases/round-trips/049-repository-source-capability.json diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs index 203138b05..92f176023 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs @@ -495,12 +495,20 @@ public sealed record CreateSessionParams /// and ignores the rest. Dispatch working-directory actions to change the set /// after the session has started. /// - /// A non-empty list and repository intent in `config` are mutually exclusive. + /// A non-empty list and `repositorySource` are mutually exclusive. /// A repository URI identifies the source, not a working-directory URI; one /// source may produce multiple directories. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? WorkingDirectories { get; init; } + /// Credential-free source to prepare; requires the agent's repositorySource capability. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? RepositorySource { get; init; } + + /// Requested branch, tag, or commit; requires a source and the capability's revision option. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? RepositoryRevision { get; init; } + /// Session configuration values collected via `resolveSessionConfig`. /// Keys and values follow the advertised {@link SessionConfigSchema}. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -1358,8 +1366,8 @@ public sealed record DisposeTerminalParams /// the full current property set (not a delta). The returned `values` contain /// server-resolved defaults to pass to `createSession`. /// -/// This command MUST NOT clone or prepare a repository. Standard repository -/// inputs and their advertisement requirements are defined by {@link SessionConfigSchema}. +/// This command MUST NOT clone or prepare a repository. Repository context +/// requires the agent's `repositorySource` capability. public sealed record ResolveSessionConfigParams { public required string Channel { get; init; } @@ -1378,6 +1386,14 @@ public sealed record ResolveSessionConfigParams [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? WorkingDirectory { get; init; } + /// Credential-free source context; not a working-directory URI. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? RepositorySource { get; init; } + + /// Requested revision; requires a source and the capability's revision option. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? RepositoryRevision { get; init; } + /// Current user-filled configuration values; see {@link SessionConfigSchema}. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Dictionary? Config { get; init; } @@ -1416,6 +1432,14 @@ public sealed record SessionConfigCompletionsParams [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? WorkingDirectory { get; init; } + /// Repository context for configuration completions; this MUST NOT prepare a checkout. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? RepositorySource { get; init; } + + /// Requested revision; requires a source and the capability's revision option. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? RepositoryRevision { get; init; } + /// Current user-filled configuration values (provides context for the query) [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Dictionary? Config { get; init; } diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs index 9b1640215..0283d2b0e 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs @@ -264,6 +264,7 @@ namespace Microsoft.AgentHostProtocol; [JsonSerializable(typeof(ReconnectResult))] [JsonSerializable(typeof(ReconnectResultType))] [JsonSerializable(typeof(ReconnectSnapshotResult))] +[JsonSerializable(typeof(RepositorySourceCapability))] [JsonSerializable(typeof(ResolveSessionConfigParams))] [JsonSerializable(typeof(ResolveSessionConfigResult))] [JsonSerializable(typeof(ResourceChange))] diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs index b4c3e3834..b50e89466 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs @@ -269,6 +269,14 @@ public sealed record PartialSessionSummary [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? WorkingDirectories { get; init; } + /// Immutable requested source, separate from the host-resolved working directories. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? RepositorySource { get; init; } + + /// Immutable requested revision, not the checkout's current HEAD. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? RepositoryRevision { get; init; } + /// Lightweight summary of this session's inline annotations channel /// (`ahp-session:/<uuid>/annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index 7ad15ecb4..1e9235905 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -861,6 +861,10 @@ public sealed record AgentInfo /// per-capability options. public sealed record AgentCapabilities { + /// The host accepts typed repository inputs for session creation and configuration queries. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public RepositorySourceCapability? RepositorySource { get; init; } + /// The agent can host more than one concurrent chat per session. When absent, /// clients MUST NOT call `createChat` to open chats beyond the default one the /// session starts with. An empty object `{}` advertises multi-chat without @@ -940,6 +944,14 @@ public sealed record MultipleWorkingDirectoriesCapability public bool? PrimaryReplacement { get; init; } } +/// Options for repository-backed session creation. +public sealed record RepositorySourceCapability +{ + /// When true, clients may supply an explicit repositoryRevision. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? Revision { get; init; } +} + public sealed record SessionModelInfo { /// Model identifier @@ -1547,6 +1559,14 @@ public sealed class SessionState [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? WorkingDirectories { get; set; } + /// Immutable requested source, separate from the host-resolved working directories. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? RepositorySource { get; set; } + + /// Immutable requested revision, not the checkout's current HEAD. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? RepositoryRevision { get; set; } + /// Lightweight summary of this session's inline annotations channel /// (`ahp-session:/<uuid>/annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -1586,10 +1606,7 @@ public sealed class SessionState [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? DefaultChat { get; set; } - /// Session configuration schema and current values. For repository-backed - /// creation, this includes the advertised standard properties and requested - /// `repositorySource` and optional `repositoryRevision` values throughout - /// `creating`, `ready`, and `failed`, so clients can recover intent from state. + /// Provider-specific session configuration schema and current values. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public SessionConfigState? Config { get; set; } @@ -1885,6 +1902,14 @@ public sealed class SessionSummary [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? WorkingDirectories { get; set; } + /// Immutable requested source, separate from the host-resolved working directories. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? RepositorySource { get; set; } + + /// Immutable requested revision, not the checkout's current HEAD. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? RepositoryRevision { get; set; } + /// Lightweight summary of this session's inline annotations channel /// (`ahp-session:/<uuid>/annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -2010,20 +2035,7 @@ public sealed record SessionConfigPropertySchema public bool? SessionMutable { get; init; } } -/// A JSON Schema object describing available session configuration metadata. -/// -/// Repository-backed creation uses the standard optional config keys -/// `repositorySource` (a credential-free repository URI) and -/// `repositoryRevision` (a branch, tag, or commit). Support is advertised by -/// `properties.repositorySource`; `properties.repositoryRevision` MUST NOT be -/// advertised without it. Each advertised property MUST have `type: 'string'` -/// and MUST NOT have `readOnly: true` or `sessionMutable: true`. -/// -/// The host MUST NOT accept repository inputs unless their corresponding -/// properties are advertised. Values travel through `resolveSessionConfig.config` -/// and `createSession.config`; schema discovery MUST NOT prepare a repository. -/// Neither key is globally required. Without repository intent, existing -/// directory/default behavior is unchanged. Other property ids remain host-defined. +/// A JSON Schema object describing available session configuration metadata. public sealed record SessionConfigSchema { /// JSON Schema: always `'object'` diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index 49d4e6e98..915981dfc 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -400,10 +400,14 @@ type CreateSessionParams struct { // and ignores the rest. Dispatch working-directory actions to change the set // after the session has started. // - // A non-empty list and repository intent in `config` are mutually exclusive. + // A non-empty list and `repositorySource` are mutually exclusive. // A repository URI identifies the source, not a working-directory URI; one // source may produce multiple directories. WorkingDirectories []URI `json:"workingDirectories,omitempty"` + // Credential-free source to prepare; requires the agent's repositorySource capability. + RepositorySource *URI `json:"repositorySource,omitempty"` + // Requested branch, tag, or commit; requires a source and the capability's revision option. + RepositoryRevision *string `json:"repositoryRevision,omitempty"` // Session configuration values collected via `resolveSessionConfig`. // Keys and values follow the advertised {@link SessionConfigSchema}. Config map[string]json.RawMessage `json:"config,omitempty"` @@ -1081,8 +1085,8 @@ type DisposeTerminalParams struct { // the full current property set (not a delta). The returned `values` contain // server-resolved defaults to pass to `createSession`. // -// This command MUST NOT clone or prepare a repository. Standard repository -// inputs and their advertisement requirements are defined by {@link SessionConfigSchema}. +// This command MUST NOT clone or prepare a repository. Repository context +// requires the agent's `repositorySource` capability. type ResolveSessionConfigParams struct { // Channel URI this command targets. Channel URI `json:"channel"` @@ -1093,6 +1097,10 @@ type ResolveSessionConfigParams struct { Provider *string `json:"provider,omitempty"` // Working directory for the session WorkingDirectory *URI `json:"workingDirectory,omitempty"` + // Credential-free source context; not a working-directory URI. + RepositorySource *URI `json:"repositorySource,omitempty"` + // Requested revision; requires a source and the capability's revision option. + RepositoryRevision *string `json:"repositoryRevision,omitempty"` // Current user-filled configuration values; see {@link SessionConfigSchema}. Config map[string]json.RawMessage `json:"config,omitempty"` } @@ -1120,6 +1128,10 @@ type SessionConfigCompletionsParams struct { Provider *string `json:"provider,omitempty"` // Working directory for the session WorkingDirectory *URI `json:"workingDirectory,omitempty"` + // Repository context for configuration completions; this MUST NOT prepare a checkout. + RepositorySource *URI `json:"repositorySource,omitempty"` + // Requested revision; requires a source and the capability's revision option. + RepositoryRevision *string `json:"repositoryRevision,omitempty"` // Current user-filled configuration values (provides context for the query) Config map[string]json.RawMessage `json:"config,omitempty"` // Property id from the schema to query values for diff --git a/clients/go/ahptypes/notifications.generated.go b/clients/go/ahptypes/notifications.generated.go index dc858228e..32e4ee12f 100644 --- a/clients/go/ahptypes/notifications.generated.go +++ b/clients/go/ahptypes/notifications.generated.go @@ -234,6 +234,10 @@ type PartialSessionSummary struct { // {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a // chat that sets none operates against this full set. WorkingDirectories []URI `json:"workingDirectories,omitempty"` + // Immutable requested source, separate from the host-resolved working directories. + RepositorySource *URI `json:"repositorySource,omitempty"` + // Immutable requested revision, not the checkout's current HEAD. + RepositoryRevision *string `json:"repositoryRevision,omitempty"` // Lightweight summary of this session's inline annotations channel // (`ahp-session://annotations`). Surfaced so badge UI can render // annotation / entry counts without subscribing. Absent when the session diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index ce1f59a48..b438323c9 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -676,6 +676,8 @@ type AgentInfo struct { // corresponding client commands MUST NOT be used. Sub-fields carry // per-capability options. type AgentCapabilities struct { + // The host accepts typed repository inputs for session creation and configuration queries. + RepositorySource *RepositorySourceCapability `json:"repositorySource,omitempty"` // The agent can host more than one concurrent chat per session. When absent, // clients MUST NOT call `createChat` to open chats beyond the default one the // session starts with. An empty object `{}` advertises multi-chat without @@ -744,6 +746,12 @@ type MultipleWorkingDirectoriesCapability struct { PrimaryReplacement *bool `json:"primaryReplacement,omitempty"` } +// Options for repository-backed session creation. +type RepositorySourceCapability struct { + // When true, clients may supply an explicit repositoryRevision. + Revision *bool `json:"revision,omitempty"` +} + type SessionModelInfo struct { // Model identifier Id string `json:"id"` @@ -877,6 +885,10 @@ type SessionState struct { // {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a // chat that sets none operates against this full set. WorkingDirectories []URI `json:"workingDirectories,omitempty"` + // Immutable requested source, separate from the host-resolved working directories. + RepositorySource *URI `json:"repositorySource,omitempty"` + // Immutable requested revision, not the checkout's current HEAD. + RepositoryRevision *string `json:"repositoryRevision,omitempty"` // Lightweight summary of this session's inline annotations channel // (`ahp-session://annotations`). Surfaced so badge UI can render // annotation / entry counts without subscribing. Absent when the session @@ -905,10 +917,7 @@ type SessionState struct { // marker — chats remain equal peers at the protocol level. Hosts MAY change // this over the session's lifetime. DefaultChat *URI `json:"defaultChat,omitempty"` - // Session configuration schema and current values. For repository-backed - // creation, this includes the advertised standard properties and requested - // `repositorySource` and optional `repositoryRevision` values throughout - // `creating`, `ready`, and `failed`, so clients can recover intent from state. + // Provider-specific session configuration schema and current values. Config *SessionConfigState `json:"config,omitempty"` // Top-level customizations active in this session. // @@ -1156,6 +1165,10 @@ type SessionSummary struct { // {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a // chat that sets none operates against this full set. WorkingDirectories []URI `json:"workingDirectories,omitempty"` + // Immutable requested source, separate from the host-resolved working directories. + RepositorySource *URI `json:"repositorySource,omitempty"` + // Immutable requested revision, not the checkout's current HEAD. + RepositoryRevision *string `json:"repositoryRevision,omitempty"` // Lightweight summary of this session's inline annotations channel // (`ahp-session://annotations`). Surfaced so badge UI can render // annotation / entry counts without subscribing. Absent when the session @@ -1368,19 +1381,6 @@ type SessionConfigPropertySchema struct { } // A JSON Schema object describing available session configuration metadata. -// -// Repository-backed creation uses the standard optional config keys -// `repositorySource` (a credential-free repository URI) and -// `repositoryRevision` (a branch, tag, or commit). Support is advertised by -// `properties.repositorySource`; `properties.repositoryRevision` MUST NOT be -// advertised without it. Each advertised property MUST have `type: 'string'` -// and MUST NOT have `readOnly: true` or `sessionMutable: true`. -// -// The host MUST NOT accept repository inputs unless their corresponding -// properties are advertised. Values travel through `resolveSessionConfig.config` -// and `createSession.config`; schema discovery MUST NOT prepare a repository. -// Neither key is globally required. Without repository intent, existing -// directory/default behavior is unchanged. Other property ids remain host-defined. type SessionConfigSchema struct { // JSON Schema: always `'object'` Type string `json:"type"` 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 f7307da84..65a61d25a 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 @@ -618,11 +618,19 @@ data class CreateSessionParams( * and ignores the rest. Dispatch working-directory actions to change the set * after the session has started. * - * A non-empty list and repository intent in `config` are mutually exclusive. + * A non-empty list and `repositorySource` are mutually exclusive. * A repository URI identifies the source, not a working-directory URI; one * source may produce multiple directories. */ val workingDirectories: List? = null, + /** + * Credential-free source to prepare; requires the agent's repositorySource capability. + */ + val repositorySource: String? = null, + /** + * Requested branch, tag, or commit; requires a source and the capability's revision option. + */ + val repositoryRevision: String? = null, /** * Session configuration values collected via `resolveSessionConfig`. * Keys and values follow the advertised {@link SessionConfigSchema}. @@ -1320,6 +1328,14 @@ data class ResolveSessionConfigParams( * Working directory for the session */ val workingDirectory: String? = null, + /** + * Credential-free source context; not a working-directory URI. + */ + val repositorySource: String? = null, + /** + * Requested revision; requires a source and the capability's revision option. + */ + val repositoryRevision: String? = null, /** * Current user-filled configuration values; see {@link SessionConfigSchema}. */ @@ -1437,6 +1453,14 @@ data class SessionConfigCompletionsParams( * Working directory for the session */ val workingDirectory: String? = null, + /** + * Repository context for configuration completions; this MUST NOT prepare a checkout. + */ + val repositorySource: String? = null, + /** + * Requested revision; requires a source and the capability's revision option. + */ + val repositoryRevision: String? = null, /** * Current user-filled configuration values (provides context for the query) */ diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt index 21d0f0ffd..e6c262269 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt @@ -225,6 +225,14 @@ data class PartialSessionSummary( * chat that sets none operates against this full set. */ val workingDirectories: List? = null, + /** + * Immutable requested source, separate from the host-resolved working directories. + */ + val repositorySource: String? = null, + /** + * Immutable requested revision, not the checkout's current HEAD. + */ + val repositoryRevision: String? = null, /** * Lightweight summary of this session's inline annotations channel * (`ahp-session://annotations`). Surfaced so badge UI can render 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 5f9c83ce9..dc1cf07d2 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 @@ -1335,6 +1335,10 @@ data class AgentInfo( @Serializable data class AgentCapabilities( + /** + * The host accepts typed repository inputs for session creation and configuration queries. + */ + val repositorySource: RepositorySourceCapability? = null, /** * The agent can host more than one concurrent chat per session. When absent, * clients MUST NOT call `createChat` to open chats beyond the default one the @@ -1415,6 +1419,14 @@ data class MultipleWorkingDirectoriesCapability( val primaryReplacement: Boolean? = null ) +@Serializable +data class RepositorySourceCapability( + /** + * When true, clients may supply an explicit repositoryRevision. + */ + val revision: Boolean? = null +) + @Serializable data class SessionModelInfo( /** @@ -1759,6 +1771,14 @@ data class SessionState( * chat that sets none operates against this full set. */ val workingDirectories: List? = null, + /** + * Immutable requested source, separate from the host-resolved working directories. + */ + val repositorySource: String? = null, + /** + * Immutable requested revision, not the checkout's current HEAD. + */ + val repositoryRevision: String? = null, /** * Lightweight summary of this session's inline annotations channel * (`ahp-session://annotations`). Surfaced so badge UI can render @@ -1802,10 +1822,7 @@ data class SessionState( */ val defaultChat: String? = null, /** - * Session configuration schema and current values. For repository-backed - * creation, this includes the advertised standard properties and requested - * `repositorySource` and optional `repositoryRevision` values throughout - * `creating`, `ready`, and `failed`, so clients can recover intent from state. + * Provider-specific session configuration schema and current values. */ val config: SessionConfigState? = null, /** @@ -2043,6 +2060,14 @@ data class SessionSummary( * chat that sets none operates against this full set. */ val workingDirectories: List? = null, + /** + * Immutable requested source, separate from the host-resolved working directories. + */ + val repositorySource: String? = null, + /** + * Immutable requested revision, not the checkout's current HEAD. + */ + val repositoryRevision: String? = null, /** * Lightweight summary of this session's inline annotations channel * (`ahp-session://annotations`). Surfaced so badge UI can render diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index 6352ecd16..049601cba 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -587,11 +587,17 @@ pub struct CreateSessionParams { /// and ignores the rest. Dispatch working-directory actions to change the set /// after the session has started. /// - /// A non-empty list and repository intent in `config` are mutually exclusive. + /// A non-empty list and `repositorySource` are mutually exclusive. /// A repository URI identifies the source, not a working-directory URI; one /// source may produce multiple directories. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, + /// Credential-free source to prepare; requires the agent's repositorySource capability. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repository_source: Option, + /// Requested branch, tag, or commit; requires a source and the capability's revision option. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repository_revision: Option, /// Session configuration values collected via `resolveSessionConfig`. /// Keys and values follow the advertised {@link SessionConfigSchema}. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1385,8 +1391,8 @@ pub struct DisposeTerminalParams { /// the full current property set (not a delta). The returned `values` contain /// server-resolved defaults to pass to `createSession`. /// -/// This command MUST NOT clone or prepare a repository. Standard repository -/// inputs and their advertisement requirements are defined by {@link SessionConfigSchema}. +/// This command MUST NOT clone or prepare a repository. Repository context +/// requires the agent's `repositorySource` capability. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ResolveSessionConfigParams { @@ -1402,6 +1408,12 @@ pub struct ResolveSessionConfigParams { /// Working directory for the session #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directory: Option, + /// Credential-free source context; not a working-directory URI. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repository_source: Option, + /// Requested revision; requires a source and the capability's revision option. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repository_revision: Option, /// Current user-filled configuration values; see {@link SessionConfigSchema}. #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option, @@ -1437,6 +1449,12 @@ pub struct SessionConfigCompletionsParams { /// Working directory for the session #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directory: Option, + /// Repository context for configuration completions; this MUST NOT prepare a checkout. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repository_source: Option, + /// Requested revision; requires a source and the capability's revision option. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repository_revision: Option, /// Current user-filled configuration values (provides context for the query) #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option, diff --git a/clients/rust/crates/ahp-types/src/notifications.rs b/clients/rust/crates/ahp-types/src/notifications.rs index 447ce60ac..d915b627c 100644 --- a/clients/rust/crates/ahp-types/src/notifications.rs +++ b/clients/rust/crates/ahp-types/src/notifications.rs @@ -294,6 +294,12 @@ pub struct PartialSessionSummary { /// chat that sets none operates against this full set. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, + /// Immutable requested source, separate from the host-resolved working directories. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repository_source: Option, + /// Immutable requested revision, not the checkout's current HEAD. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repository_revision: Option, /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 635b991ea..d5fd78ed6 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -1623,6 +1623,9 @@ pub struct AgentInfo { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct AgentCapabilities { + /// The host accepts typed repository inputs for session creation and configuration queries. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repository_source: Option, /// The agent can host more than one concurrent chat per session. When absent, /// clients MUST NOT call `createChat` to open chats beyond the default one the /// session starts with. An empty object `{}` advertises multi-chat without @@ -1701,6 +1704,15 @@ pub struct MultipleWorkingDirectoriesCapability { pub primary_replacement: Option, } +/// Options for repository-backed session creation. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct RepositorySourceCapability { + /// When true, clients may supply an explicit repositoryRevision. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub revision: Option, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionModelInfo { @@ -2021,6 +2033,12 @@ pub struct SessionState { /// chat that sets none operates against this full set. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, + /// Immutable requested source, separate from the host-resolved working directories. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repository_source: Option, + /// Immutable requested revision, not the checkout's current HEAD. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repository_revision: Option, /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -2053,10 +2071,7 @@ pub struct SessionState { /// this over the session's lifetime. #[serde(default, skip_serializing_if = "Option::is_none")] pub default_chat: Option, - /// Session configuration schema and current values. For repository-backed - /// creation, this includes the advertised standard properties and requested - /// `repositorySource` and optional `repositoryRevision` values throughout - /// `creating`, `ready`, and `failed`, so clients can recover intent from state. + /// Provider-specific session configuration schema and current values. #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option, /// Top-level customizations active in this session. @@ -2360,6 +2375,12 @@ pub struct SessionSummary { /// chat that sets none operates against this full set. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, + /// Immutable requested source, separate from the host-resolved working directories. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repository_source: Option, + /// Immutable requested revision, not the checkout's current HEAD. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repository_revision: Option, /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -2467,19 +2488,6 @@ pub struct SessionConfigPropertySchema { } /// A JSON Schema object describing available session configuration metadata. -/// -/// Repository-backed creation uses the standard optional config keys -/// `repositorySource` (a credential-free repository URI) and -/// `repositoryRevision` (a branch, tag, or commit). Support is advertised by -/// `properties.repositorySource`; `properties.repositoryRevision` MUST NOT be -/// advertised without it. Each advertised property MUST have `type: 'string'` -/// and MUST NOT have `readOnly: true` or `sessionMutable: true`. -/// -/// The host MUST NOT accept repository inputs unless their corresponding -/// properties are advertised. Values travel through `resolveSessionConfig.config` -/// and `createSession.config`; schema discovery MUST NOT prepare a repository. -/// Neither key is globally required. Without repository intent, existing -/// directory/default behavior is unchanged. Other property ids remain host-defined. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionConfigSchema { diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index c20120d0b..fd5f68304 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -652,10 +652,14 @@ public struct CreateSessionParams: Codable, Sendable { /// and ignores the rest. Dispatch working-directory actions to change the set /// after the session has started. /// - /// A non-empty list and repository intent in `config` are mutually exclusive. + /// A non-empty list and `repositorySource` are mutually exclusive. /// A repository URI identifies the source, not a working-directory URI; one /// source may produce multiple directories. public var workingDirectories: [String]? + /// Credential-free source to prepare; requires the agent's repositorySource capability. + public var repositorySource: String? + /// Requested branch, tag, or commit; requires a source and the capability's revision option. + public var repositoryRevision: String? /// Session configuration values collected via `resolveSessionConfig`. /// Keys and values follow the advertised {@link SessionConfigSchema}. public var config: [String: AnyCodable]? @@ -683,6 +687,8 @@ public struct CreateSessionParams: Codable, Sendable { case meta = "_meta" case provider case workingDirectories + case repositorySource + case repositoryRevision case config case activeClient case progressToken @@ -693,6 +699,8 @@ public struct CreateSessionParams: Codable, Sendable { meta: [String: AnyCodable]? = nil, provider: String? = nil, workingDirectories: [String]? = nil, + repositorySource: String? = nil, + repositoryRevision: String? = nil, config: [String: AnyCodable]? = nil, activeClient: SessionActiveClient? = nil, progressToken: String? = nil @@ -701,6 +709,8 @@ public struct CreateSessionParams: Codable, Sendable { self.meta = meta self.provider = provider self.workingDirectories = workingDirectories + self.repositorySource = repositorySource + self.repositoryRevision = repositoryRevision self.config = config self.activeClient = activeClient self.progressToken = progressToken @@ -1600,6 +1610,10 @@ public struct ResolveSessionConfigParams: Codable, Sendable { public var provider: String? /// Working directory for the session public var workingDirectory: String? + /// Credential-free source context; not a working-directory URI. + public var repositorySource: String? + /// Requested revision; requires a source and the capability's revision option. + public var repositoryRevision: String? /// Current user-filled configuration values; see {@link SessionConfigSchema}. public var config: [String: AnyCodable]? @@ -1608,6 +1622,8 @@ public struct ResolveSessionConfigParams: Codable, Sendable { case meta = "_meta" case provider case workingDirectory + case repositorySource + case repositoryRevision case config } @@ -1616,12 +1632,16 @@ public struct ResolveSessionConfigParams: Codable, Sendable { meta: [String: AnyCodable]? = nil, provider: String? = nil, workingDirectory: String? = nil, + repositorySource: String? = nil, + repositoryRevision: String? = nil, config: [String: AnyCodable]? = nil ) { self.channel = channel self.meta = meta self.provider = provider self.workingDirectory = workingDirectory + self.repositorySource = repositorySource + self.repositoryRevision = repositoryRevision self.config = config } } @@ -1753,6 +1773,10 @@ public struct SessionConfigCompletionsParams: Codable, Sendable { public var provider: String? /// Working directory for the session public var workingDirectory: String? + /// Repository context for configuration completions; this MUST NOT prepare a checkout. + public var repositorySource: String? + /// Requested revision; requires a source and the capability's revision option. + public var repositoryRevision: String? /// Current user-filled configuration values (provides context for the query) public var config: [String: AnyCodable]? /// Property id from the schema to query values for @@ -1765,6 +1789,8 @@ public struct SessionConfigCompletionsParams: Codable, Sendable { case meta = "_meta" case provider case workingDirectory + case repositorySource + case repositoryRevision case config case property case query @@ -1775,6 +1801,8 @@ public struct SessionConfigCompletionsParams: Codable, Sendable { meta: [String: AnyCodable]? = nil, provider: String? = nil, workingDirectory: String? = nil, + repositorySource: String? = nil, + repositoryRevision: String? = nil, config: [String: AnyCodable]? = nil, property: String, query: String? = nil @@ -1783,6 +1811,8 @@ public struct SessionConfigCompletionsParams: Codable, Sendable { self.meta = meta self.provider = provider self.workingDirectory = workingDirectory + self.repositorySource = repositorySource + self.repositoryRevision = repositoryRevision self.config = config self.property = property self.query = query diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift index f865befde..271f2ae13 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift @@ -217,6 +217,10 @@ public struct PartialSessionSummary: Codable, Sendable { /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a /// chat that sets none operates against this full set. public var workingDirectories: [String]? + /// Immutable requested source, separate from the host-resolved working directories. + public var repositorySource: String? + /// Immutable requested revision, not the checkout's current HEAD. + public var repositoryRevision: String? /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -247,6 +251,8 @@ public struct PartialSessionSummary: Codable, Sendable { case origin case project case workingDirectories + case repositorySource + case repositoryRevision case annotations case resource case createdAt @@ -263,6 +269,8 @@ public struct PartialSessionSummary: Codable, Sendable { origin: SessionOrigin? = nil, project: ProjectInfo? = nil, workingDirectories: [String]? = nil, + repositorySource: String? = nil, + repositoryRevision: String? = nil, annotations: AnnotationsSummary? = nil, resource: String? = nil, createdAt: String? = nil, @@ -277,6 +285,8 @@ public struct PartialSessionSummary: Codable, Sendable { self.origin = origin self.project = project self.workingDirectories = workingDirectories + self.repositorySource = repositorySource + self.repositoryRevision = repositoryRevision self.annotations = annotations self.resource = resource self.createdAt = createdAt diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 11b821124..b5c031ebc 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -1321,6 +1321,8 @@ public struct AgentInfo: Codable, Sendable { } public struct AgentCapabilities: Codable, Sendable { + /// The host accepts typed repository inputs for session creation and configuration queries. + public var repositorySource: RepositorySourceCapability? /// The agent can host more than one concurrent chat per session. When absent, /// clients MUST NOT call `createChat` to open chats beyond the default one the /// session starts with. An empty object `{}` advertises multi-chat without @@ -1338,9 +1340,11 @@ public struct AgentCapabilities: Codable, Sendable { public var multipleWorkingDirectories: MultipleWorkingDirectoriesCapability? public init( + repositorySource: RepositorySourceCapability? = nil, multipleChats: MultipleChatsCapability? = nil, multipleWorkingDirectories: MultipleWorkingDirectoriesCapability? = nil ) { + self.repositorySource = repositorySource self.multipleChats = multipleChats self.multipleWorkingDirectories = multipleWorkingDirectories } @@ -1411,6 +1415,17 @@ public struct MultipleWorkingDirectoriesCapability: Codable, Sendable { } } +public struct RepositorySourceCapability: Codable, Sendable { + /// When true, clients may supply an explicit repositoryRevision. + public var revision: Bool? + + public init( + revision: Bool? = nil + ) { + self.revision = revision + } +} + public struct SessionModelInfo: Codable, Sendable { /// Model identifier public var id: String @@ -1811,6 +1826,10 @@ public struct SessionState: Codable, Sendable { /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a /// chat that sets none operates against this full set. public var workingDirectories: [String]? + /// Immutable requested source, separate from the host-resolved working directories. + public var repositorySource: String? + /// Immutable requested revision, not the checkout's current HEAD. + public var repositoryRevision: String? /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -1839,10 +1858,7 @@ public struct SessionState: Codable, Sendable { /// marker — chats remain equal peers at the protocol level. Hosts MAY change /// this over the session's lifetime. public var defaultChat: String? - /// Session configuration schema and current values. For repository-backed - /// creation, this includes the advertised standard properties and requested - /// `repositorySource` and optional `repositoryRevision` values throughout - /// `creating`, `ready`, and `failed`, so clients can recover intent from state. + /// Provider-specific session configuration schema and current values. public var config: SessionConfigState? /// Top-level customizations active in this session. /// @@ -1903,6 +1919,8 @@ public struct SessionState: Codable, Sendable { case origin case project case workingDirectories + case repositorySource + case repositoryRevision case annotations case lifecycle case creationError @@ -1925,6 +1943,8 @@ public struct SessionState: Codable, Sendable { origin: SessionOrigin? = nil, project: ProjectInfo? = nil, workingDirectories: [String]? = nil, + repositorySource: String? = nil, + repositoryRevision: String? = nil, annotations: AnnotationsSummary? = nil, lifecycle: SessionLifecycle, creationError: ErrorInfo? = nil, @@ -1945,6 +1965,8 @@ public struct SessionState: Codable, Sendable { self.origin = origin self.project = project self.workingDirectories = workingDirectories + self.repositorySource = repositorySource + self.repositoryRevision = repositoryRevision self.annotations = annotations self.lifecycle = lifecycle self.creationError = creationError @@ -2142,6 +2164,10 @@ public struct SessionSummary: Codable, Sendable { /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a /// chat that sets none operates against this full set. public var workingDirectories: [String]? + /// Immutable requested source, separate from the host-resolved working directories. + public var repositorySource: String? + /// Immutable requested revision, not the checkout's current HEAD. + public var repositoryRevision: String? /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -2172,6 +2198,8 @@ public struct SessionSummary: Codable, Sendable { case origin case project case workingDirectories + case repositorySource + case repositoryRevision case annotations case resource case createdAt @@ -2188,6 +2216,8 @@ public struct SessionSummary: Codable, Sendable { origin: SessionOrigin? = nil, project: ProjectInfo? = nil, workingDirectories: [String]? = nil, + repositorySource: String? = nil, + repositoryRevision: String? = nil, annotations: AnnotationsSummary? = nil, resource: String, createdAt: String, @@ -2202,6 +2232,8 @@ public struct SessionSummary: Codable, Sendable { self.origin = origin self.project = project self.workingDirectories = workingDirectories + self.repositorySource = repositorySource + self.repositoryRevision = repositoryRevision self.annotations = annotations self.resource = resource self.createdAt = createdAt diff --git a/clients/typescript/test/client.test.ts b/clients/typescript/test/client.test.ts index 5c68acfd2..56fe0c21b 100644 --- a/clients/typescript/test/client.test.ts +++ b/clients/typescript/test/client.test.ts @@ -115,7 +115,7 @@ test('initialize round-trip', async () => { }); for (const withRevision of [false, true]) { - test(`generic session config round-trips repository intent ${withRevision ? 'with' : 'without'} a revision`, async t => { + test(`typed requests round-trip repository intent ${withRevision ? 'with' : 'without'} a revision outside config`, async t => { const [c, s] = InMemoryTransport.pair(); const client = new AhpClient(c); t.after(() => client.shutdown()); @@ -124,14 +124,9 @@ for (const withRevision of [false, true]) { const schema: SessionConfigSchema = { type: 'object', properties: { - repositorySource: { type: 'string', title: 'Repository' }, mode: { type: 'string', title: 'Mode', default: 'review' }, }, }; - if (withRevision) { - schema.properties.repositoryRevision = { type: 'string', title: 'Revision' }; - } - const discovery = client.request('resolveSessionConfig', { channel: ROOT }); const discoveryRequest = await readRequest(s); assert.equal(discoveryRequest.method, 'resolveSessionConfig'); @@ -140,20 +135,26 @@ for (const withRevision of [false, true]) { const discovered = await discovery; assert.deepEqual(discovered.schema, schema); - const config = { - ...discovered.values, + const repository = { repositorySource: 'https://example.org/team/project.git', ...(withRevision ? { repositoryRevision: 'refs/tags/v1.2.3' } : {}), }; - const resolution = client.request('resolveSessionConfig', { channel: ROOT, config }); + const config = discovered.values; + const resolution = client.request('resolveSessionConfig', { channel: ROOT, ...repository, config }); const resolveRequest = await readRequest(s); assert.equal(resolveRequest.method, 'resolveSessionConfig'); - assert.deepEqual(resolveRequest.params, { channel: ROOT, config }); + assert.deepEqual(resolveRequest.params, { channel: ROOT, ...repository, config }); reply(s, resolveRequest.id, { schema, values: config }); const resolved = await resolution; assert.deepEqual(resolved.values, config); - const params = { channel: 'ahp-session:/repository-test', config: resolved.values }; + const completions = client.request('sessionConfigCompletions', { channel: ROOT, ...repository, config: resolved.values, property: 'mode' }); + const completionRequest = await readRequest(s); + assert.deepEqual(completionRequest.params, { channel: ROOT, ...repository, config: resolved.values, property: 'mode' }); + reply(s, completionRequest.id, { items: [] }); + await completions; + + const params = { channel: 'ahp-session:/repository-test', ...repository, config: resolved.values }; const creation = client.request('createSession', params); const createRequest = await readRequest(s); assert.equal(createRequest.method, 'createSession'); @@ -163,22 +164,26 @@ for (const withRevision of [false, true]) { }); } -for (const method of ['resolveSessionConfig', 'createSession'] as const) { +for (const method of ['resolveSessionConfig', 'sessionConfigCompletions', 'createSession'] as const) { test(`${method} surfaces host rejection of repository intent`, async t => { const [c, s] = InMemoryTransport.pair(); const client = new AhpClient(c); t.after(() => client.shutdown()); client.connect(); - const channel = method === 'resolveSessionConfig' ? ROOT : 'ahp-session:/repository-test'; - const config = { + const channel = method === 'createSession' ? 'ahp-session:/repository-test' : ROOT; + const params = { + channel, repositorySource: 'https://example.org/team/project.git', repositoryRevision: 'unsupported', + ...(method === 'sessionConfigCompletions' ? { property: 'mode' } : {}), }; - const request = client.request(method, { channel, config }); + const request = method === 'sessionConfigCompletions' + ? client.request(method, { ...params, property: 'mode' }) + : client.request(method, params); const rejected = assert.rejects(request, new RpcError(JsonRpcErrorCodes.InvalidParams, 'Unsupported repository revision')); const sent = await readRequest(s); - assert.deepEqual({ method: sent.method, params: sent.params }, { method, params: { channel, config } }); + assert.deepEqual({ method: sent.method, params: sent.params }, { method, params }); replyError(s, sent.id, JsonRpcErrorCodes.InvalidParams, 'Unsupported repository revision'); await rejected; }); @@ -195,15 +200,11 @@ for (const failed of [false, true]) { activeClients: [], chats: [], workingDirectories: [], + repositorySource: 'https://example.org/team/project.git', + repositoryRevision: 'main', config: { - schema: { - type: 'object', - properties: { - repositorySource: { type: 'string', title: 'Repository' }, - repositoryRevision: { type: 'string', title: 'Revision' }, - }, - }, - values: { repositorySource: 'https://example.org/team/project.git', repositoryRevision: 'main' }, + schema: { type: 'object', properties: { mode: { type: 'string', title: 'Mode' } } }, + values: { mode: 'review' }, }, }; const mirror = new AhpStateMirror(); @@ -224,6 +225,7 @@ for (const failed of [false, true]) { assert.ok(preparing); assert.equal(preparing.lifecycle, SessionLifecycle.Creating); assert.deepEqual(preparing.config, initial.config); + assert.deepEqual([preparing.repositorySource, preparing.repositoryRevision], [initial.repositorySource, initial.repositoryRevision]); const joining = new AhpStateMirror(); joining.applySnapshot({ resource, state: preparing, fromSeq: 2 }); @@ -243,6 +245,7 @@ for (const failed of [false, true]) { assert.ok(completed); assert.equal(completed.lifecycle, failed ? SessionLifecycle.Failed : SessionLifecycle.Ready); assert.deepEqual(completed.config, initial.config); + assert.deepEqual([completed.repositorySource, completed.repositoryRevision], [initial.repositorySource, initial.repositoryRevision]); assert.deepEqual(completed.workingDirectories, ['file:///work/project', 'file:///work/project-worktree']); assert.deepEqual(joining.getSession(resource), completed); if (failed) { diff --git a/docs/.changes/20260915-repository-session-config.json b/docs/.changes/20260915-repository-session-config.json index ab57b6476..864da15d2 100644 --- a/docs/.changes/20260915-repository-session-config.json +++ b/docs/.changes/20260915-repository-session-config.json @@ -1,4 +1,4 @@ { "type": "added", - "message": "Standard optional `repositorySource` and `repositoryRevision` configuration keys for schema-advertised, host-owned repository-backed session creation through existing configuration and lifecycle messages." + "message": "Typed optional `repositorySource` and `repositoryRevision` request and session metadata fields, with an explicit agent capability for host-owned repository preparation." } diff --git a/docs/specification/session-channel.md b/docs/specification/session-channel.md index d0c7f7397..9ca534ba8 100644 --- a/docs/specification/session-channel.md +++ b/docs/specification/session-channel.md @@ -37,43 +37,38 @@ Subscribers receive a [`SessionState`](/reference/session#sessionstate) snapshot #### Repository-backed creation -A host can offer to prepare **one repository for a new session** through the existing session configuration flow. The client collects repository intent; the host owns authorization, credentials, preparation, and cleanup. This capability does not define reusable projects, a repository catalogue, or a general-purpose clone command. +A host can offer to prepare **one repository for a new session** through typed session-creation inputs. The client collects repository intent; the host owns authorization, credentials, preparation, and cleanup. This capability does not define reusable projects, a repository catalogue, or a general-purpose clone command. ##### Capability and field constraints -The host opts in by returning a valid `schema.properties.repositorySource` from [`resolveSessionConfig`](/reference/root#resolvesessionconfig). [`SessionConfigSchema`](/reference/session#sessionconfigschema) remains a generic configuration schema: repository support uses standard property names, not separate repository metadata or a host-selected key mapping. +The agent opts in through [`AgentCapabilities.repositorySource`](/reference/root#agentcapabilities). As with other agent capabilities, absence means unsupported and `{}` advertises source-based creation. `{ "revision": true }` additionally supports an explicit revision. -| Configuration key | Meaning | +| Request field | Meaning | |---|---| | `repositorySource` | Credential-free repository URI string identifying the requested source. | | `repositoryRevision` | Optional branch, tag, or commit string. | -Each advertised property MUST have `type: "string"` and be writable at creation (`readOnly` MUST NOT be `true`). Neither property may have `sessionMutable: true`: these values describe creation intent, not a request to switch repositories or revisions in an existing session. `schema.properties.repositoryRevision` is optional and MUST NOT be advertised without a valid `schema.properties.repositorySource`. +Both fields are optional typed properties of [`CreateSessionParams`](/reference/session#createsessionparams), [`ResolveSessionConfigParams`](/reference/root#resolvesessionconfigparams), and [`SessionConfigCompletionsParams`](/reference/root#sessionconfigcompletionsparams). The query fields provide context for provider-specific configuration; they are not entries in `config`. -Clients MUST use these exact keys after checking the advertised properties. They MUST NOT infer repository support from a provider name, protocol version, `_meta`, or another property whose name resembles a repository field. A host MUST NOT accept `repositorySource` unless it advertises a valid source property, and MUST NOT accept `repositoryRevision` unless it advertises a valid revision property. There are no alternate standard keys or aliases. Other configuration keys remain host-defined. +Clients MUST check the capability rather than infer support from a provider name, protocol version, `_meta`, or configuration property. A host MUST NOT accept source input without the capability, or an explicit revision unless `revision` is `true`. Supplying either input in `config` is invalid; hosts MUST reject it rather than silently choose directory/default behavior. There are no alternative standard keys or field-name descriptors. -Advertising support does not itself make either value required. The existing `required` list still describes form requirements; AHP adds no globally required repository property. A host without repository support, or a request without repository intent, retains its existing directory/default behavior. The generated schema describes the generic configuration shape; the host remains responsible for enforcing these semantic rules. +Advertising support does not make either value required. A request without repository intent retains its existing directory/default behavior. The generated request types and schemas declare the fields and their types; the host enforces capability, authorization, and cross-field constraints. Provider-specific `config` and its schema remain independent. ##### Values and validation -Values travel in `resolveSessionConfig.config`, then in `createSession.config`. Discovery and iterative configuration resolution MUST NOT clone or prepare a repository. The host MAY advertise supported URI schemes and revision choices through the existing property descriptions, enums, and completions. +The client supplies the same typed source and optional revision when resolving configuration, requesting configuration completions, and creating the session. Discovery and iterative configuration queries MUST NOT clone or prepare a repository. -For example, directory-free discovery can return: +For example, the root's agent entry can advertise: ```json { - "schema": { - "type": "object", - "properties": { - "repositorySource": { "type": "string", "title": "Repository" }, - "repositoryRevision": { "type": "string", "title": "Revision" } - } - }, - "values": {} + "capabilities": { + "repositorySource": { "revision": true } + } } ``` -The client can submit `{"repositorySource":"https://example.org/team/project.git","repositoryRevision":"main"}` as `resolveSessionConfig.config`, without a `workingDirectory`, and pass the returned values to creation: +The client can resolve configuration with `repositorySource` and `repositoryRevision` beside `config`, without a `workingDirectory`, then pass the returned provider configuration to creation: ```json { @@ -82,17 +77,16 @@ The client can submit `{"repositorySource":"https://example.org/team/project.git "method": "createSession", "params": { "channel": "ahp-session:/new-session", - "config": { - "repositorySource": "https://example.org/team/project.git", - "repositoryRevision": "main" - } + "repositorySource": "https://example.org/team/project.git", + "repositoryRevision": "main", + "config": { "mode": "interactive" } } } ``` -The repository URI identifies the source, not a checkout or host filesystem directory. One source can produce multiple directories, including separate checkouts or worktrees; clients MUST NOT use the source URI as a directory identity. Repository intent and a non-empty `createSession.workingDirectories` list are mutually exclusive. +The repository URI identifies the source, not a checkout or host filesystem directory. One source can produce multiple directories, including separate checkouts or worktrees; clients MUST NOT use the source URI as a directory identity. `createSession.repositorySource` and a non-empty `createSession.workingDirectories` list are mutually exclusive. Configuration queries may also include an existing `workingDirectory` as context; they do not perform preparation. -When supplied, each value MUST be a non-empty string. A revision without a source is invalid. Omit an unused source or revision instead of supplying an empty string. For both configuration resolution and creation, the host MUST reject invalid or unsupported intent with `InvalidParams` (`-32602`), including an unadvertised source or revision, a malformed or credential-bearing source URI, an unsupported revision, or conflicting creation directories. It MUST NOT silently drop explicit input, select a default directory, or replace an unsupported revision with its default. A repository-aware client MUST surface invalid capability declarations or unsupported input instead of silently dropping the user's intent. +When supplied, each value MUST be a non-empty string. A revision without a source is invalid. Omit an unused source or revision instead of supplying an empty string. For creation and configuration queries, the host MUST reject invalid or unsupported intent with `InvalidParams` (`-32602`), including an unsupported source or revision, a malformed or credential-bearing source URI, or conflicting creation directories. It MUST NOT silently drop explicit input, select a default directory, or replace an unsupported revision with its default. A repository-aware client MUST surface invalid capability declarations or unsupported input instead of silently dropping the user's intent. Repository URIs and configuration values MUST NOT contain credentials such as passwords or access tokens. Authentication uses the existing [authentication contract](./authentication); the host MUST authorize the requesting client before repository side effects and use only credentials permitted for that request. Credentials MUST NOT appear in session state, progress messages, or logs. @@ -100,7 +94,7 @@ Repository URIs and configuration values MUST NOT contain credentials such as pa Repository preparation is part of the existing `creating` lifecycle. The host MUST finish preparation before executing turns or publishing `session/ready`. No additional lifecycle state is introduced. -The host MUST publish the accepted, requested source and optional revision as `SessionState.config.values.repositorySource` and `SessionState.config.values.repositoryRevision`, together with their advertised properties in `SessionState.config.schema`. Make this intent available in the initial `creating` snapshot so another client joining during preparation can understand the session. Preserve requested intent even if the host resolves a branch or tag to a commit; the resolved working location is a separate fact. +The host MUST publish the accepted, requested source and optional revision as `SessionState.repositorySource` and `SessionState.repositoryRevision` from the initial `creating` snapshot and preserve them through `ready` or `failed`. These immutable fields belong to [`SessionMetadata`](/reference/session#sessionmetadata), so summaries carry the same intent. Preserve requested intent even if the host resolves a branch or tag to a commit; the resolved working location is a separate fact. No configuration action changes these fields. Before dispatching `session/ready` or `session/creationFailed`, the host MUST publish the actual resolved `workingDirectories` in session state, using the existing snapshot and working-directory actions. While no directory has been resolved, `workingDirectories` MAY be absent or empty; do not claim a checkout was prepared when preparation failed. On failure, the existing `session/creationFailed` action records `lifecycle: "failed"` and `creationError`. Both outcomes retain the requested intent and any resolved directories so clients can recover them from a snapshot or replay. @@ -108,13 +102,13 @@ The host MAY report preparation through the existing `createSession.progressToke ##### Reattachment, retry, and cleanup -`createSession` is not an idempotent preparation command. A duplicate URI still returns `SessionAlreadyExists` (`-32003`), including while preparation is running or after creation has failed; it MUST NOT start another preparation for that session. After a lost response, the client should reattach to the same session URI through subscription or [reconnection](./lifecycle#reconnection) and inspect its state. Before treating the recovered session as the requested creation, it MUST verify that `config.values.repositorySource` and `config.values.repositoryRevision` match the requested intent and inspect the lifecycle. A mismatch is a conflict, not successful recovery. It MUST NOT treat a duplicate creation error as successful recovery. After a failure is addressed, a user can explicitly retry with a new session URI rather than overwrite the failed session. +`createSession` is not an idempotent preparation command. A duplicate URI still returns `SessionAlreadyExists` (`-32003`), including while preparation is running or after creation has failed; it MUST NOT start another preparation for that session. After a lost response, the client should reattach to the same session URI through subscription or [reconnection](./lifecycle#reconnection) and inspect its state. Before treating the recovered session as the requested creation, it MUST verify that its typed `repositorySource` and `repositoryRevision` match the requested intent and inspect the lifecycle. A mismatch is a conflict, not successful recovery. It MUST NOT treat a duplicate creation error as successful recovery. After a failure is addressed, a user can explicitly retry with a new session URI rather than overwrite the failed session. Cancelling a local wait, disconnecting, or unsubscribing does not grant permission to delete repository data. When the user intends to dispose the session, use the existing `disposeSession` command; this capability adds no cancellation RPC. The host MUST NOT erase a shared checkout or uncommitted user changes during cancellation or disposal. Cleanup of exclusively owned temporary preparation resources remains a host responsibility. ##### Minimal-client behavior -A minimal client can render the ordinary advertised configuration fields, pass resolved values through `config`, and render the existing session lifecycle and `workingDirectories`. It needs neither Git support nor a repository-specific form, clone RPC, or progress implementation. It can also omit this optional creation capability entirely and continue using directory/default creation. A joining or reconnecting client renders the authoritative state without repeating repository preparation. +A client supporting this capability collects the source and optional revision separately from provider configuration and sends them as typed request fields. It needs no Git implementation, clone RPC, or progress implementation. Minimal clients can omit the optional capability and continue using directory/default creation. Joining or reconnecting clients read the source, revision, lifecycle and working directories from authoritative session state without repeating preparation. ### Active session diff --git a/schema/actions.schema.json b/schema/actions.schema.json index f3c73f485..33cba59b5 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -2943,6 +2943,10 @@ "type": "object", "description": "Static capabilities an {@link AgentInfo} advertises. Modelled after MCP\ncapabilities: each field is opt-in and its presence (an empty object `{}`)\nsignals support, while absence means the feature is unsupported and the\ncorresponding client commands MUST NOT be used. Sub-fields carry\nper-capability options.", "properties": { + "repositorySource": { + "$ref": "#/$defs/RepositorySourceCapability", + "description": "The host accepts typed repository inputs for session creation and configuration queries." + }, "multipleChats": { "$ref": "#/$defs/MultipleChatsCapability", "description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nsource-based creation; set {@link MultipleChatsCapability.fork} or\n{@link MultipleChatsCapability.sideChat} to allow the corresponding mode." @@ -2953,6 +2957,16 @@ } } }, + "RepositorySourceCapability": { + "type": "object", + "description": "Options for repository-backed session creation.", + "properties": { + "revision": { + "type": "boolean", + "description": "When true, clients may supply an explicit repositoryRevision." + } + } + }, "MultipleChatsCapability": { "type": "object", "description": "Options for the {@link AgentCapabilities.multipleChats} capability.", @@ -3128,6 +3142,14 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, + "repositorySource": { + "$ref": "#/$defs/URI", + "description": "Immutable requested source, separate from the host-resolved working directories." + }, + "repositoryRevision": { + "type": "string", + "description": "Immutable requested revision, not the checkout's current HEAD." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -3174,6 +3196,14 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, + "repositorySource": { + "$ref": "#/$defs/URI", + "description": "Immutable requested source, separate from the host-resolved working directories." + }, + "repositoryRevision": { + "type": "string", + "description": "Immutable requested revision, not the checkout's current HEAD." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -3213,7 +3243,7 @@ }, "config": { "$ref": "#/$defs/SessionConfigState", - "description": "Session configuration schema and current values. For repository-backed\ncreation, this includes the advertised standard properties and requested\n`repositorySource` and optional `repositoryRevision` values throughout\n`creating`, `ready`, and `failed`, so clients can recover intent from state." + "description": "Provider-specific session configuration schema and current values." }, "customizations": { "type": "array", @@ -3482,6 +3512,14 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, + "repositorySource": { + "$ref": "#/$defs/URI", + "description": "Immutable requested source, separate from the host-resolved working directories." + }, + "repositoryRevision": { + "type": "string", + "description": "Immutable requested revision, not the checkout's current HEAD." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -3637,7 +3675,7 @@ }, "SessionConfigSchema": { "type": "object", - "description": "A JSON Schema object describing available session configuration metadata.\n\nRepository-backed creation uses the standard optional config keys\n`repositorySource` (a credential-free repository URI) and\n`repositoryRevision` (a branch, tag, or commit). Support is advertised by\n`properties.repositorySource`; `properties.repositoryRevision` MUST NOT be\nadvertised without it. Each advertised property MUST have `type: 'string'`\nand MUST NOT have `readOnly: true` or `sessionMutable: true`.\n\nThe host MUST NOT accept repository inputs unless their corresponding\nproperties are advertised. Values travel through `resolveSessionConfig.config`\nand `createSession.config`; schema discovery MUST NOT prepare a repository.\nNeither key is globally required. Without repository intent, existing\ndirectory/default behavior is unchanged. Other property ids remain host-defined.", + "description": "A JSON Schema object describing available session configuration metadata.", "properties": { "type": { "type": "string", diff --git a/schema/commands.schema.json b/schema/commands.schema.json index cb6f7eed9..f9a3deca4 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -949,7 +949,7 @@ }, "ResolveSessionConfigParams": { "type": "object", - "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.\n\nThis command MUST NOT clone or prepare a repository. Standard repository\ninputs and their advertisement requirements are defined by {@link SessionConfigSchema}.", + "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.\n\nThis command MUST NOT clone or prepare a repository. Repository context\nrequires the agent's `repositorySource` capability.", "properties": { "channel": { "type": "string", @@ -970,6 +970,14 @@ "$ref": "#/$defs/URI", "description": "Working directory for the session" }, + "repositorySource": { + "$ref": "#/$defs/URI", + "description": "Credential-free source context; not a working-directory URI." + }, + "repositoryRevision": { + "type": "string", + "description": "Requested revision; requires a source and the capability's revision option." + }, "config": { "type": "object", "additionalProperties": {}, @@ -1044,6 +1052,14 @@ "$ref": "#/$defs/URI", "description": "Working directory for the session" }, + "repositorySource": { + "$ref": "#/$defs/URI", + "description": "Repository context for configuration completions; this MUST NOT prepare a checkout." + }, + "repositoryRevision": { + "type": "string", + "description": "Requested revision; requires a source and the capability's revision option." + }, "config": { "type": "object", "additionalProperties": {}, @@ -1101,7 +1117,15 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises a protected-primary capability. An\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary | immutable\nprimary} is fixed, while a\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement | replaceable\nprimary} is changed only with `session/workingDirectoryReplaced`.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch working-directory actions to change the set\nafter the session has started.\n\nA non-empty list and repository intent in `config` are mutually exclusive.\nA repository URI identifies the source, not a working-directory URI; one\nsource may produce multiple directories." + "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises a protected-primary capability. An\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary | immutable\nprimary} is fixed, while a\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement | replaceable\nprimary} is changed only with `session/workingDirectoryReplaced`.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch working-directory actions to change the set\nafter the session has started.\n\nA non-empty list and `repositorySource` are mutually exclusive.\nA repository URI identifies the source, not a working-directory URI; one\nsource may produce multiple directories." + }, + "repositorySource": { + "$ref": "#/$defs/URI", + "description": "Credential-free source to prepare; requires the agent's repositorySource capability." + }, + "repositoryRevision": { + "type": "string", + "description": "Requested branch, tag, or commit; requires a source and the capability's revision option." }, "config": { "type": "object", @@ -2187,6 +2211,10 @@ "type": "object", "description": "Static capabilities an {@link AgentInfo} advertises. Modelled after MCP\ncapabilities: each field is opt-in and its presence (an empty object `{}`)\nsignals support, while absence means the feature is unsupported and the\ncorresponding client commands MUST NOT be used. Sub-fields carry\nper-capability options.", "properties": { + "repositorySource": { + "$ref": "#/$defs/RepositorySourceCapability", + "description": "The host accepts typed repository inputs for session creation and configuration queries." + }, "multipleChats": { "$ref": "#/$defs/MultipleChatsCapability", "description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nsource-based creation; set {@link MultipleChatsCapability.fork} or\n{@link MultipleChatsCapability.sideChat} to allow the corresponding mode." @@ -2197,6 +2225,16 @@ } } }, + "RepositorySourceCapability": { + "type": "object", + "description": "Options for repository-backed session creation.", + "properties": { + "revision": { + "type": "boolean", + "description": "When true, clients may supply an explicit repositoryRevision." + } + } + }, "MultipleChatsCapability": { "type": "object", "description": "Options for the {@link AgentCapabilities.multipleChats} capability.", @@ -2372,6 +2410,14 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, + "repositorySource": { + "$ref": "#/$defs/URI", + "description": "Immutable requested source, separate from the host-resolved working directories." + }, + "repositoryRevision": { + "type": "string", + "description": "Immutable requested revision, not the checkout's current HEAD." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -2418,6 +2464,14 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, + "repositorySource": { + "$ref": "#/$defs/URI", + "description": "Immutable requested source, separate from the host-resolved working directories." + }, + "repositoryRevision": { + "type": "string", + "description": "Immutable requested revision, not the checkout's current HEAD." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -2457,7 +2511,7 @@ }, "config": { "$ref": "#/$defs/SessionConfigState", - "description": "Session configuration schema and current values. For repository-backed\ncreation, this includes the advertised standard properties and requested\n`repositorySource` and optional `repositoryRevision` values throughout\n`creating`, `ready`, and `failed`, so clients can recover intent from state." + "description": "Provider-specific session configuration schema and current values." }, "customizations": { "type": "array", @@ -2726,6 +2780,14 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, + "repositorySource": { + "$ref": "#/$defs/URI", + "description": "Immutable requested source, separate from the host-resolved working directories." + }, + "repositoryRevision": { + "type": "string", + "description": "Immutable requested revision, not the checkout's current HEAD." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -2881,7 +2943,7 @@ }, "SessionConfigSchema": { "type": "object", - "description": "A JSON Schema object describing available session configuration metadata.\n\nRepository-backed creation uses the standard optional config keys\n`repositorySource` (a credential-free repository URI) and\n`repositoryRevision` (a branch, tag, or commit). Support is advertised by\n`properties.repositorySource`; `properties.repositoryRevision` MUST NOT be\nadvertised without it. Each advertised property MUST have `type: 'string'`\nand MUST NOT have `readOnly: true` or `sessionMutable: true`.\n\nThe host MUST NOT accept repository inputs unless their corresponding\nproperties are advertised. Values travel through `resolveSessionConfig.config`\nand `createSession.config`; schema discovery MUST NOT prepare a repository.\nNeither key is globally required. Without repository intent, existing\ndirectory/default behavior is unchanged. Other property ids remain host-defined.", + "description": "A JSON Schema object describing available session configuration metadata.", "properties": { "type": { "type": "string", diff --git a/schema/errors.schema.json b/schema/errors.schema.json index a513ea799..74175b8aa 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -616,6 +616,10 @@ "type": "object", "description": "Static capabilities an {@link AgentInfo} advertises. Modelled after MCP\ncapabilities: each field is opt-in and its presence (an empty object `{}`)\nsignals support, while absence means the feature is unsupported and the\ncorresponding client commands MUST NOT be used. Sub-fields carry\nper-capability options.", "properties": { + "repositorySource": { + "$ref": "#/$defs/RepositorySourceCapability", + "description": "The host accepts typed repository inputs for session creation and configuration queries." + }, "multipleChats": { "$ref": "#/$defs/MultipleChatsCapability", "description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nsource-based creation; set {@link MultipleChatsCapability.fork} or\n{@link MultipleChatsCapability.sideChat} to allow the corresponding mode." @@ -626,6 +630,16 @@ } } }, + "RepositorySourceCapability": { + "type": "object", + "description": "Options for repository-backed session creation.", + "properties": { + "revision": { + "type": "boolean", + "description": "When true, clients may supply an explicit repositoryRevision." + } + } + }, "MultipleChatsCapability": { "type": "object", "description": "Options for the {@link AgentCapabilities.multipleChats} capability.", @@ -801,6 +815,14 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, + "repositorySource": { + "$ref": "#/$defs/URI", + "description": "Immutable requested source, separate from the host-resolved working directories." + }, + "repositoryRevision": { + "type": "string", + "description": "Immutable requested revision, not the checkout's current HEAD." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -847,6 +869,14 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, + "repositorySource": { + "$ref": "#/$defs/URI", + "description": "Immutable requested source, separate from the host-resolved working directories." + }, + "repositoryRevision": { + "type": "string", + "description": "Immutable requested revision, not the checkout's current HEAD." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -886,7 +916,7 @@ }, "config": { "$ref": "#/$defs/SessionConfigState", - "description": "Session configuration schema and current values. For repository-backed\ncreation, this includes the advertised standard properties and requested\n`repositorySource` and optional `repositoryRevision` values throughout\n`creating`, `ready`, and `failed`, so clients can recover intent from state." + "description": "Provider-specific session configuration schema and current values." }, "customizations": { "type": "array", @@ -1155,6 +1185,14 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, + "repositorySource": { + "$ref": "#/$defs/URI", + "description": "Immutable requested source, separate from the host-resolved working directories." + }, + "repositoryRevision": { + "type": "string", + "description": "Immutable requested revision, not the checkout's current HEAD." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -1310,7 +1348,7 @@ }, "SessionConfigSchema": { "type": "object", - "description": "A JSON Schema object describing available session configuration metadata.\n\nRepository-backed creation uses the standard optional config keys\n`repositorySource` (a credential-free repository URI) and\n`repositoryRevision` (a branch, tag, or commit). Support is advertised by\n`properties.repositorySource`; `properties.repositoryRevision` MUST NOT be\nadvertised without it. Each advertised property MUST have `type: 'string'`\nand MUST NOT have `readOnly: true` or `sessionMutable: true`.\n\nThe host MUST NOT accept repository inputs unless their corresponding\nproperties are advertised. Values travel through `resolveSessionConfig.config`\nand `createSession.config`; schema discovery MUST NOT prepare a repository.\nNeither key is globally required. Without repository intent, existing\ndirectory/default behavior is unchanged. Other property ids remain host-defined.", + "description": "A JSON Schema object describing available session configuration metadata.", "properties": { "type": { "type": "string", @@ -6601,7 +6639,7 @@ }, "ResolveSessionConfigParams": { "type": "object", - "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.\n\nThis command MUST NOT clone or prepare a repository. Standard repository\ninputs and their advertisement requirements are defined by {@link SessionConfigSchema}.", + "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.\n\nThis command MUST NOT clone or prepare a repository. Repository context\nrequires the agent's `repositorySource` capability.", "properties": { "channel": { "type": "string", @@ -6622,6 +6660,14 @@ "$ref": "#/$defs/URI", "description": "Working directory for the session" }, + "repositorySource": { + "$ref": "#/$defs/URI", + "description": "Credential-free source context; not a working-directory URI." + }, + "repositoryRevision": { + "type": "string", + "description": "Requested revision; requires a source and the capability's revision option." + }, "config": { "type": "object", "additionalProperties": {}, @@ -6696,6 +6742,14 @@ "$ref": "#/$defs/URI", "description": "Working directory for the session" }, + "repositorySource": { + "$ref": "#/$defs/URI", + "description": "Repository context for configuration completions; this MUST NOT prepare a checkout." + }, + "repositoryRevision": { + "type": "string", + "description": "Requested revision; requires a source and the capability's revision option." + }, "config": { "type": "object", "additionalProperties": {}, @@ -6753,7 +6807,15 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises a protected-primary capability. An\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary | immutable\nprimary} is fixed, while a\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement | replaceable\nprimary} is changed only with `session/workingDirectoryReplaced`.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch working-directory actions to change the set\nafter the session has started.\n\nA non-empty list and repository intent in `config` are mutually exclusive.\nA repository URI identifies the source, not a working-directory URI; one\nsource may produce multiple directories." + "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises a protected-primary capability. An\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary | immutable\nprimary} is fixed, while a\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement | replaceable\nprimary} is changed only with `session/workingDirectoryReplaced`.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch working-directory actions to change the set\nafter the session has started.\n\nA non-empty list and `repositorySource` are mutually exclusive.\nA repository URI identifies the source, not a working-directory URI; one\nsource may produce multiple directories." + }, + "repositorySource": { + "$ref": "#/$defs/URI", + "description": "Credential-free source to prepare; requires the agent's repositorySource capability." + }, + "repositoryRevision": { + "type": "string", + "description": "Requested branch, tag, or commit; requires a source and the capability's revision option." }, "config": { "type": "object", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 2c346f337..f7148dc2f 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -109,6 +109,14 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, + "repositorySource": { + "$ref": "#/$defs/URI", + "description": "Immutable requested source, separate from the host-resolved working directories." + }, + "repositoryRevision": { + "type": "string", + "description": "Immutable requested revision, not the checkout's current HEAD." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -783,6 +791,10 @@ "type": "object", "description": "Static capabilities an {@link AgentInfo} advertises. Modelled after MCP\ncapabilities: each field is opt-in and its presence (an empty object `{}`)\nsignals support, while absence means the feature is unsupported and the\ncorresponding client commands MUST NOT be used. Sub-fields carry\nper-capability options.", "properties": { + "repositorySource": { + "$ref": "#/$defs/RepositorySourceCapability", + "description": "The host accepts typed repository inputs for session creation and configuration queries." + }, "multipleChats": { "$ref": "#/$defs/MultipleChatsCapability", "description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nsource-based creation; set {@link MultipleChatsCapability.fork} or\n{@link MultipleChatsCapability.sideChat} to allow the corresponding mode." @@ -793,6 +805,16 @@ } } }, + "RepositorySourceCapability": { + "type": "object", + "description": "Options for repository-backed session creation.", + "properties": { + "revision": { + "type": "boolean", + "description": "When true, clients may supply an explicit repositoryRevision." + } + } + }, "MultipleChatsCapability": { "type": "object", "description": "Options for the {@link AgentCapabilities.multipleChats} capability.", @@ -968,6 +990,14 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, + "repositorySource": { + "$ref": "#/$defs/URI", + "description": "Immutable requested source, separate from the host-resolved working directories." + }, + "repositoryRevision": { + "type": "string", + "description": "Immutable requested revision, not the checkout's current HEAD." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -1014,6 +1044,14 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, + "repositorySource": { + "$ref": "#/$defs/URI", + "description": "Immutable requested source, separate from the host-resolved working directories." + }, + "repositoryRevision": { + "type": "string", + "description": "Immutable requested revision, not the checkout's current HEAD." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -1053,7 +1091,7 @@ }, "config": { "$ref": "#/$defs/SessionConfigState", - "description": "Session configuration schema and current values. For repository-backed\ncreation, this includes the advertised standard properties and requested\n`repositorySource` and optional `repositoryRevision` values throughout\n`creating`, `ready`, and `failed`, so clients can recover intent from state." + "description": "Provider-specific session configuration schema and current values." }, "customizations": { "type": "array", @@ -1322,6 +1360,14 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, + "repositorySource": { + "$ref": "#/$defs/URI", + "description": "Immutable requested source, separate from the host-resolved working directories." + }, + "repositoryRevision": { + "type": "string", + "description": "Immutable requested revision, not the checkout's current HEAD." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -1477,7 +1523,7 @@ }, "SessionConfigSchema": { "type": "object", - "description": "A JSON Schema object describing available session configuration metadata.\n\nRepository-backed creation uses the standard optional config keys\n`repositorySource` (a credential-free repository URI) and\n`repositoryRevision` (a branch, tag, or commit). Support is advertised by\n`properties.repositorySource`; `properties.repositoryRevision` MUST NOT be\nadvertised without it. Each advertised property MUST have `type: 'string'`\nand MUST NOT have `readOnly: true` or `sessionMutable: true`.\n\nThe host MUST NOT accept repository inputs unless their corresponding\nproperties are advertised. Values travel through `resolveSessionConfig.config`\nand `createSession.config`; schema discovery MUST NOT prepare a repository.\nNeither key is globally required. Without repository intent, existing\ndirectory/default behavior is unchanged. Other property ids remain host-defined.", + "description": "A JSON Schema object describing available session configuration metadata.", "properties": { "type": { "type": "string", diff --git a/schema/state.schema.json b/schema/state.schema.json index b87000e99..3e4a1b6a5 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -527,6 +527,10 @@ "type": "object", "description": "Static capabilities an {@link AgentInfo} advertises. Modelled after MCP\ncapabilities: each field is opt-in and its presence (an empty object `{}`)\nsignals support, while absence means the feature is unsupported and the\ncorresponding client commands MUST NOT be used. Sub-fields carry\nper-capability options.", "properties": { + "repositorySource": { + "$ref": "#/$defs/RepositorySourceCapability", + "description": "The host accepts typed repository inputs for session creation and configuration queries." + }, "multipleChats": { "$ref": "#/$defs/MultipleChatsCapability", "description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nsource-based creation; set {@link MultipleChatsCapability.fork} or\n{@link MultipleChatsCapability.sideChat} to allow the corresponding mode." @@ -537,6 +541,16 @@ } } }, + "RepositorySourceCapability": { + "type": "object", + "description": "Options for repository-backed session creation.", + "properties": { + "revision": { + "type": "boolean", + "description": "When true, clients may supply an explicit repositoryRevision." + } + } + }, "MultipleChatsCapability": { "type": "object", "description": "Options for the {@link AgentCapabilities.multipleChats} capability.", @@ -712,6 +726,14 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, + "repositorySource": { + "$ref": "#/$defs/URI", + "description": "Immutable requested source, separate from the host-resolved working directories." + }, + "repositoryRevision": { + "type": "string", + "description": "Immutable requested revision, not the checkout's current HEAD." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -758,6 +780,14 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, + "repositorySource": { + "$ref": "#/$defs/URI", + "description": "Immutable requested source, separate from the host-resolved working directories." + }, + "repositoryRevision": { + "type": "string", + "description": "Immutable requested revision, not the checkout's current HEAD." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -797,7 +827,7 @@ }, "config": { "$ref": "#/$defs/SessionConfigState", - "description": "Session configuration schema and current values. For repository-backed\ncreation, this includes the advertised standard properties and requested\n`repositorySource` and optional `repositoryRevision` values throughout\n`creating`, `ready`, and `failed`, so clients can recover intent from state." + "description": "Provider-specific session configuration schema and current values." }, "customizations": { "type": "array", @@ -1066,6 +1096,14 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, + "repositorySource": { + "$ref": "#/$defs/URI", + "description": "Immutable requested source, separate from the host-resolved working directories." + }, + "repositoryRevision": { + "type": "string", + "description": "Immutable requested revision, not the checkout's current HEAD." + }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", "description": "Lightweight summary of this session's inline annotations channel\n(`ahp-session://annotations`). Surfaced so badge UI can render\nannotation / entry counts without subscribing. Absent when the session\ndoes not expose an annotations channel." @@ -1221,7 +1259,7 @@ }, "SessionConfigSchema": { "type": "object", - "description": "A JSON Schema object describing available session configuration metadata.\n\nRepository-backed creation uses the standard optional config keys\n`repositorySource` (a credential-free repository URI) and\n`repositoryRevision` (a branch, tag, or commit). Support is advertised by\n`properties.repositorySource`; `properties.repositoryRevision` MUST NOT be\nadvertised without it. Each advertised property MUST have `type: 'string'`\nand MUST NOT have `readOnly: true` or `sessionMutable: true`.\n\nThe host MUST NOT accept repository inputs unless their corresponding\nproperties are advertised. Values travel through `resolveSessionConfig.config`\nand `createSession.config`; schema discovery MUST NOT prepare a repository.\nNeither key is globally required. Without repository intent, existing\ndirectory/default behavior is unchanged. Other property ids remain host-defined.", + "description": "A JSON Schema object describing available session configuration metadata.", "properties": { "type": { "type": "string", diff --git a/scripts/generate-csharp.ts b/scripts/generate-csharp.ts index 207212210..1f2be5bd9 100644 --- a/scripts/generate-csharp.ts +++ b/scripts/generate-csharp.ts @@ -659,6 +659,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; csName?: strin { name: 'AgentCapabilities' }, { name: 'MultipleChatsCapability' }, { name: 'MultipleWorkingDirectoriesCapability' }, + { name: 'RepositorySourceCapability' }, { name: 'SessionModelInfo' }, { name: 'ModelSelection' }, { name: 'AgentSelection' }, diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 71926ac2f..71f736b01 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -739,6 +739,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin { name: 'AgentCapabilities' }, { name: 'MultipleChatsCapability' }, { name: 'MultipleWorkingDirectoriesCapability' }, + { name: 'RepositorySourceCapability' }, { name: 'SessionModelInfo' }, { name: 'ModelSelection' }, { name: 'AgentSelection' }, diff --git a/scripts/generate-json-schema.test.ts b/scripts/generate-json-schema.test.ts index 65bac66a0..c104f4c83 100644 --- a/scripts/generate-json-schema.test.ts +++ b/scripts/generate-json-schema.test.ts @@ -222,56 +222,57 @@ describe('generated JSON schemas', () => { required: ['mode'], }; assert.equal(schemaAccepts(schema, configSchema, legacy), true); - - for (const withRevision of [false, true]) { - const repositorySchema = { - type: 'object', - properties: { - repositorySource: { type: 'string', title: 'Repository', readOnly: false, sessionMutable: false }, - ...(withRevision ? { - repositoryRevision: { type: 'string', title: 'Revision', readOnly: false, sessionMutable: false }, - } : {}), - mode: { type: 'string', title: 'Mode' }, - }, - required: ['mode'], - }; - assert.equal( - schemaAccepts(schema, configSchema, repositorySchema), - true, - ); - } }); - it('retains generic config inputs for repository-backed creation', () => { + it('declares optional typed repository inputs beside generic config', () => { if (file !== 'commands.schema.json') { return; } const defs = schema.$defs as Record>; for (const [definition, channel] of [ ['ResolveSessionConfigParams', 'ahp-root://'], + ['SessionConfigCompletionsParams', 'ahp-root://'], ['CreateSessionParams', 'ahp-session:/repository-test'], ]) { const properties = defs[definition].properties as Record>; assert.equal(properties.config.type, 'object'); assert.deepEqual( Object.keys(properties).filter(name => ['repository', 'repositorySource', 'repositoryRevision'].includes(name)), - [], + ['repositorySource', 'repositoryRevision'], ); - assert.equal(schemaAccepts(schema, defs[definition], { channel }), true); - for (const config of [ - { mode: 'review' }, - { mode: 'review', repositorySource: 'https://example.org/team/project.git' }, + const base = { channel, ...(definition === 'SessionConfigCompletionsParams' ? { property: 'mode' } : {}) }; + assert.equal(schemaAccepts(schema, defs[definition], base), true); + for (const source of [ + {}, + { repositorySource: 'https://example.org/team/project.git' }, { - mode: 'review', repositorySource: 'https://example.org/team/project.git', repositoryRevision: 'refs/tags/v1.2.3', }, ]) { - assert.equal(schemaAccepts(schema, defs[definition], { channel, config }), true); + assert.equal(schemaAccepts(schema, defs[definition], { ...base, ...source, config: { mode: 'review' } }), true); + } + for (const invalid of [{ repositorySource: 42 }, { repositorySource: null }, { repositoryRevision: 42 }]) { + assert.equal(schemaAccepts(schema, defs[definition], { ...base, ...invalid }), false); } } }); + it('declares immutable source metadata and an opt-in repository capability', () => { + const defs = schema.$defs as Record>; + for (const name of ['SessionState', 'SessionSummary']) { + const properties = defs[name].properties as Record>; + assert.equal(dereferenceSchema(schema, properties.repositorySource).type, 'string'); + assert.equal(dereferenceSchema(schema, properties.repositoryRevision).type, 'string'); + } + const capabilities = defs.AgentCapabilities.properties as Record>; + assert.deepEqual(capabilities.repositorySource.$ref, '#/$defs/RepositorySourceCapability'); + for (const value of [{}, { repositorySource: {} }, { repositorySource: { revision: true } }]) { + assert.equal(schemaAccepts(schema, defs.AgentCapabilities, value), true); + } + assert.equal(schemaAccepts(schema, defs.AgentCapabilities, { repositorySource: true }), false); + }); + it('constrains every ChatOrigin branch to a distinct kind', () => { const defs = schema.$defs as Record>; const chatOrigin = defs.ChatOrigin; diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index ea13b41e5..c4e284405 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -987,6 +987,7 @@ const STATE_STRUCTS = [ 'AgentCapabilities', 'MultipleChatsCapability', 'MultipleWorkingDirectoriesCapability', + 'RepositorySourceCapability', 'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema', 'PendingMessage', 'ChatState', 'ChatSummary', 'SideChatSelection', 'SessionState', 'SessionActiveClient', 'SessionChatInputRequest', 'SessionToolConfirmationRequest', 'SessionToolClientExecutionRequest', diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 4e999bbcf..9bad0bbc0 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -800,6 +800,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str { name: 'AgentCapabilities' }, { name: 'MultipleChatsCapability' }, { name: 'MultipleWorkingDirectoriesCapability' }, + { name: 'RepositorySourceCapability' }, { name: 'SessionModelInfo' }, { name: 'ModelSelection' }, { name: 'AgentSelection' }, diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 89fa9bcdf..6ca96c394 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -692,6 +692,7 @@ const STATE_STRUCTS = [ 'AgentCapabilities', 'MultipleChatsCapability', 'MultipleWorkingDirectoriesCapability', + 'RepositorySourceCapability', 'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema', 'PendingMessage', 'ChatState', 'ChatSummary', 'SideChatSelection', 'SessionState', 'SessionActiveClient', 'SessionChatInputRequest', 'SessionToolConfirmationRequest', 'SessionToolClientExecutionRequest', diff --git a/types/channels-root/commands.ts b/types/channels-root/commands.ts index d85bfb353..00635e1ca 100644 --- a/types/channels-root/commands.ts +++ b/types/channels-root/commands.ts @@ -79,8 +79,8 @@ export interface ListSessionsResult extends PaginatedResult { * the full current property set (not a delta). The returned `values` contain * server-resolved defaults to pass to `createSession`. * - * This command MUST NOT clone or prepare a repository. Standard repository - * inputs and their advertisement requirements are defined by {@link SessionConfigSchema}. + * This command MUST NOT clone or prepare a repository. Repository context + * requires the agent's `repositorySource` capability. * * @category Commands * @method resolveSessionConfig @@ -133,6 +133,10 @@ export interface ResolveSessionConfigParams extends BaseParams { provider?: string; /** Working directory for the session */ workingDirectory?: URI; + /** Credential-free source context; not a working-directory URI. */ + repositorySource?: URI; + /** Requested revision; requires a source and the capability's revision option. */ + repositoryRevision?: string; /** Current user-filled configuration values; see {@link SessionConfigSchema}. */ config?: Record; } @@ -198,6 +202,10 @@ export interface SessionConfigCompletionsParams extends BaseParams { provider?: string; /** Working directory for the session */ workingDirectory?: URI; + /** Repository context for configuration completions; this MUST NOT prepare a checkout. */ + repositorySource?: URI; + /** Requested revision; requires a source and the capability's revision option. */ + repositoryRevision?: string; /** Current user-filled configuration values (provides context for the query) */ config?: Record; /** Property id from the schema to query values for */ diff --git a/types/channels-root/state.ts b/types/channels-root/state.ts index b6c123810..4c2700251 100644 --- a/types/channels-root/state.ts +++ b/types/channels-root/state.ts @@ -106,6 +106,8 @@ export interface AgentInfo { * @category Root State */ export interface AgentCapabilities { + /** The host accepts typed repository inputs for session creation and configuration queries. */ + repositorySource?: RepositorySourceCapability; /** * The agent can host more than one concurrent chat per session. When absent, * clients MUST NOT call `createChat` to open chats beyond the default one the @@ -127,6 +129,15 @@ export interface AgentCapabilities { multipleWorkingDirectories?: MultipleWorkingDirectoriesCapability; } +/** + * Options for repository-backed session creation. + * @category Root State + */ +export interface RepositorySourceCapability { + /** When true, clients may supply an explicit repositoryRevision. */ + revision?: boolean; +} + /** * Options for the {@link AgentCapabilities.multipleChats} capability. * diff --git a/types/channels-session/commands.ts b/types/channels-session/commands.ts index c77c77173..b58e53549 100644 --- a/types/channels-session/commands.ts +++ b/types/channels-session/commands.ts @@ -70,11 +70,15 @@ export interface CreateSessionParams extends BaseParams { * and ignores the rest. Dispatch working-directory actions to change the set * after the session has started. * - * A non-empty list and repository intent in `config` are mutually exclusive. + * A non-empty list and `repositorySource` are mutually exclusive. * A repository URI identifies the source, not a working-directory URI; one * source may produce multiple directories. */ workingDirectories?: URI[]; + /** Credential-free source to prepare; requires the agent's repositorySource capability. */ + repositorySource?: URI; + /** Requested branch, tag, or commit; requires a source and the capability's revision option. */ + repositoryRevision?: string; /** * Session configuration values collected via `resolveSessionConfig`. * Keys and values follow the advertised {@link SessionConfigSchema}. diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index fb7fce37c..cd78b33fd 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -139,6 +139,10 @@ export interface SessionMetadata { * chat that sets none operates against this full set. */ workingDirectories?: URI[]; + /** Immutable requested source, separate from the host-resolved working directories. */ + repositorySource?: URI; + /** Immutable requested revision, not the checkout's current HEAD. */ + repositoryRevision?: string; /** * Lightweight summary of this session's inline annotations channel * (`ahp-session://annotations`). Surfaced so badge UI can render @@ -187,12 +191,7 @@ export interface SessionState extends SessionMetadata { * this over the session's lifetime. */ defaultChat?: URI; - /** - * Session configuration schema and current values. For repository-backed - * creation, this includes the advertised standard properties and requested - * `repositorySource` and optional `repositoryRevision` values throughout - * `creating`, `ready`, and `failed`, so clients can recover intent from state. - */ + /** Provider-specific session configuration schema and current values. */ config?: SessionConfigState; /** * Top-level customizations active in this session. @@ -576,19 +575,6 @@ export interface SessionConfigPropertySchema extends ConfigPropertySchema { /** * A JSON Schema object describing available session configuration metadata. * - * Repository-backed creation uses the standard optional config keys - * `repositorySource` (a credential-free repository URI) and - * `repositoryRevision` (a branch, tag, or commit). Support is advertised by - * `properties.repositorySource`; `properties.repositoryRevision` MUST NOT be - * advertised without it. Each advertised property MUST have `type: 'string'` - * and MUST NOT have `readOnly: true` or `sessionMutable: true`. - * - * The host MUST NOT accept repository inputs unless their corresponding - * properties are advertised. Values travel through `resolveSessionConfig.config` - * and `createSession.config`; schema discovery MUST NOT prepare a repository. - * Neither key is globally required. Without repository intent, existing - * directory/default behavior is unchanged. Other property ids remain host-defined. - * * @category Session Config Types */ export interface SessionConfigSchema { diff --git a/types/test-cases/round-trips/046-repository-session-source-only.json b/types/test-cases/round-trips/046-repository-session-source-only.json index 8dea4511d..591ba53a6 100644 --- a/types/test-cases/round-trips/046-repository-session-source-only.json +++ b/types/test-cases/round-trips/046-repository-session-source-only.json @@ -1,7 +1,7 @@ { "name": "repository-session-source-only", "group": "A", - "description": "A ready session preserves the standard repositorySource property and requested URI, omits the optional revision, and resolves one source to multiple directories.", + "description": "A ready session preserves typed source metadata, omits the optional revision, and resolves one source to multiple directories.", "type": "Snapshot", "input": { "resource": "ahp-session:/repository-session", @@ -12,16 +12,8 @@ "lifecycle": "ready", "activeClients": [], "chats": [], - "workingDirectories": ["file:///work/project", "file:///work/project-worktree"], - "config": { - "schema": { - "type": "object", - "properties": { - "repositorySource": { "type": "string", "title": "Repository" } - } - }, - "values": { "repositorySource": "https://example.org/team/project.git" } - } + "repositorySource": "https://example.org/team/project.git", + "workingDirectories": ["file:///work/project", "file:///work/project-worktree"] }, "fromSeq": 2 }, @@ -34,16 +26,8 @@ "lifecycle": "ready", "activeClients": [], "chats": [], - "workingDirectories": ["file:///work/project", "file:///work/project-worktree"], - "config": { - "schema": { - "type": "object", - "properties": { - "repositorySource": { "type": "string", "title": "Repository" } - } - }, - "values": { "repositorySource": "https://example.org/team/project.git" } - } + "repositorySource": "https://example.org/team/project.git", + "workingDirectories": ["file:///work/project", "file:///work/project-worktree"] }, "fromSeq": 2 }] diff --git a/types/test-cases/round-trips/047-repository-session-revision.json b/types/test-cases/round-trips/047-repository-session-revision.json index b22e75a52..46570667c 100644 --- a/types/test-cases/round-trips/047-repository-session-revision.json +++ b/types/test-cases/round-trips/047-repository-session-revision.json @@ -1,7 +1,7 @@ { "name": "repository-session-revision", "group": "A", - "description": "A creating session preserves the standard repositorySource and repositoryRevision properties and requested values before a directory is resolved.", + "description": "A creating session preserves typed source and revision metadata before a directory is resolved.", "type": "Snapshot", "input": { "resource": "ahp-session:/repository-session", @@ -12,16 +12,8 @@ "lifecycle": "creating", "activeClients": [], "chats": [], - "config": { - "schema": { - "type": "object", - "properties": { - "repositorySource": { "type": "string", "title": "Repository", "readOnly": false, "sessionMutable": false }, - "repositoryRevision": { "type": "string", "title": "Revision", "readOnly": false, "sessionMutable": false } - } - }, - "values": { "repositorySource": "https://example.org/team/project.git", "repositoryRevision": "refs/tags/v1.2.3" } - } + "repositorySource": "https://example.org/team/project.git", + "repositoryRevision": "refs/tags/v1.2.3" }, "fromSeq": 0 }, @@ -34,16 +26,8 @@ "lifecycle": "creating", "activeClients": [], "chats": [], - "config": { - "schema": { - "type": "object", - "properties": { - "repositorySource": { "type": "string", "title": "Repository", "readOnly": false, "sessionMutable": false }, - "repositoryRevision": { "type": "string", "title": "Revision", "readOnly": false, "sessionMutable": false } - } - }, - "values": { "repositorySource": "https://example.org/team/project.git", "repositoryRevision": "refs/tags/v1.2.3" } - } + "repositorySource": "https://example.org/team/project.git", + "repositoryRevision": "refs/tags/v1.2.3" }, "fromSeq": 0 }] diff --git a/types/test-cases/round-trips/048-repository-session-failed.json b/types/test-cases/round-trips/048-repository-session-failed.json index d2bee1f5d..36487a383 100644 --- a/types/test-cases/round-trips/048-repository-session-failed.json +++ b/types/test-cases/round-trips/048-repository-session-failed.json @@ -13,16 +13,8 @@ "creationError": { "errorType": "preparationFailed", "message": "Preparation failed" }, "activeClients": [], "chats": [], - "config": { - "schema": { - "type": "object", - "properties": { - "repositorySource": { "type": "string", "title": "Repository" }, - "repositoryRevision": { "type": "string", "title": "Revision" } - } - }, - "values": { "repositorySource": "https://example.org/team/project.git", "repositoryRevision": "main" } - } + "repositorySource": "https://example.org/team/project.git", + "repositoryRevision": "main" }, "fromSeq": 1 }, @@ -36,16 +28,8 @@ "creationError": { "errorType": "preparationFailed", "message": "Preparation failed" }, "activeClients": [], "chats": [], - "config": { - "schema": { - "type": "object", - "properties": { - "repositorySource": { "type": "string", "title": "Repository" }, - "repositoryRevision": { "type": "string", "title": "Revision" } - } - }, - "values": { "repositorySource": "https://example.org/team/project.git", "repositoryRevision": "main" } - } + "repositorySource": "https://example.org/team/project.git", + "repositoryRevision": "main" }, "fromSeq": 1 }] diff --git a/types/test-cases/round-trips/049-repository-source-capability.json b/types/test-cases/round-trips/049-repository-source-capability.json new file mode 100644 index 000000000..89ecefbf0 --- /dev/null +++ b/types/test-cases/round-trips/049-repository-source-capability.json @@ -0,0 +1,58 @@ +{ + "name": "repository-source-capability", + "group": "A", + "description": "Agents advertise source-only or source-with-revision preparation independently of config schemas.", + "type": "InitializeResult", + "input": { + "protocolVersion": "0.9.0", + "serverSeq": 0, + "snapshots": [{ + "resource": "ahp-root://", + "state": { + "agents": [ + { + "provider": "source-only", + "displayName": "Source only", + "description": "Repository preparation", + "models": [], + "capabilities": { "repositorySource": {} } + }, + { + "provider": "source-with-revision", + "displayName": "Source with revision", + "description": "Repository preparation", + "models": [], + "capabilities": { "repositorySource": { "revision": true } } + } + ] + }, + "fromSeq": 0 + }] + }, + "acceptableOutputs": [{ + "protocolVersion": "0.9.0", + "serverSeq": 0, + "snapshots": [{ + "resource": "ahp-root://", + "state": { + "agents": [ + { + "provider": "source-only", + "displayName": "Source only", + "description": "Repository preparation", + "models": [], + "capabilities": { "repositorySource": {} } + }, + { + "provider": "source-with-revision", + "displayName": "Source with revision", + "description": "Repository preparation", + "models": [], + "capabilities": { "repositorySource": { "revision": true } } + } + ] + }, + "fromSeq": 0 + }] + }] +} From 7541739a465b5f2213fe924b7ba7c8da8703242c Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Sun, 20 Sep 2026 19:55:29 -0700 Subject: [PATCH 5/6] test: Complete typed repository fields in Rust fixtures Initialize the new optional source and revision fields in reducer, client and multi-host fixtures so cargo test --workspace compiles all targets. This changes tests only; generated protocol types and the wire contract are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 79731f48-d288-483f-8809-136aa141d1eb --- clients/rust/crates/ahp/src/reducers.rs | 2 ++ clients/rust/crates/ahp/tests/client_roundtrip.rs | 2 ++ clients/rust/crates/ahp/tests/hosts.rs | 2 ++ clients/rust/crates/ahp/tests/multi_host_state_mirror.rs | 4 ++++ 4 files changed, 10 insertions(+) diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 95b02ff4d..13293d513 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -2174,6 +2174,8 @@ mod tests { origin: None, project: None, working_directories: None, + repository_source: None, + repository_revision: None, annotations: None, lifecycle: SessionLifecycle::Creating, creation_error: None, diff --git a/clients/rust/crates/ahp/tests/client_roundtrip.rs b/clients/rust/crates/ahp/tests/client_roundtrip.rs index 1e49931ec..21e58e991 100644 --- a/clients/rust/crates/ahp/tests/client_roundtrip.rs +++ b/clients/rust/crates/ahp/tests/client_roundtrip.rs @@ -376,6 +376,8 @@ async fn session_config_completions_send_wrapper_targets_root_channel() { meta: None, provider: None, working_directory: None, + repository_source: None, + repository_revision: None, config: None, property: "baseBranch".into(), query: Some("ma".into()), diff --git a/clients/rust/crates/ahp/tests/hosts.rs b/clients/rust/crates/ahp/tests/hosts.rs index 654038c2e..37f73035a 100644 --- a/clients/rust/crates/ahp/tests/hosts.rs +++ b/clients/rust/crates/ahp/tests/hosts.rs @@ -1096,6 +1096,8 @@ fn make_summary(uri: &str, title: &str, modified_at: i64) -> ahp_types::state::S modified_at: modified, project: None, working_directories: None, + repository_source: None, + repository_revision: None, changes: None, annotations: None, meta: None, diff --git a/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs b/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs index 4dd42c919..577126cf5 100644 --- a/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs +++ b/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs @@ -55,6 +55,8 @@ fn session_state(title: &str, _resource: &str) -> SessionState { origin: None, project: None, working_directories: None, + repository_source: None, + repository_revision: None, annotations: None, lifecycle: SessionLifecycle::Ready, creation_error: None, @@ -476,6 +478,8 @@ fn non_action_event_is_ignored() { modified_at: "1970-01-01T00:00:00.000Z".into(), project: None, working_directories: None, + repository_source: None, + repository_revision: None, changes: None, annotations: None, meta: None, From 043bc34700935951efc4528c59e9d3158f463bfc Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Sun, 20 Sep 2026 22:20:51 -0700 Subject: [PATCH 6/6] feat: Use repository lists and host preparation capability Group repository source and optional revision into typed lists on creation, configuration queries, and immutable session metadata. Advertise host-owned preparation on initialize, with single-repository support unless multipleRepositories is enabled. Restore generic configuration, disposal, and progress comments. Preserve exact repository intent through lifecycle recovery, and regenerate all client types with shared wire and reducer coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 79731f48-d288-483f-8809-136aa141d1eb --- .../Generated/Commands.generated.cs | 70 +++++---- .../JsonSerializerContext.generated.cs | 3 +- .../Generated/Notifications.generated.cs | 15 +- .../Generated/State.generated.cs | 41 +++-- .../TypesRoundTripFixtures.cs | 6 + clients/go/ahptypes/commands.generated.go | 56 ++++--- .../go/ahptypes/notifications.generated.go | 11 +- clients/go/ahptypes/roundtrip_fixture_test.go | 12 ++ clients/go/ahptypes/state.generated.go | 34 ++-- .../generated/Commands.generated.kt | 59 ++++--- .../generated/Notifications.generated.kt | 11 +- .../generated/State.generated.kt | 38 +++-- .../agenthostprotocol/RoundTripCorpusTest.kt | 6 + clients/rust/crates/ahp-types/src/commands.rs | 69 +++++---- .../crates/ahp-types/src/notifications.rs | 16 +- clients/rust/crates/ahp-types/src/state.rs | 39 ++--- .../ahp-types/tests/roundtrip_corpus.rs | 8 +- clients/rust/crates/ahp/src/reducers.rs | 3 +- .../rust/crates/ahp/tests/client_roundtrip.rs | 3 +- clients/rust/crates/ahp/tests/hosts.rs | 3 +- .../ahp/tests/multi_host_state_mirror.rs | 6 +- .../Generated/Commands.generated.swift | 84 +++++----- .../Generated/Notifications.generated.swift | 18 +-- .../Generated/State.generated.swift | 54 +++---- .../TypesRoundTripFixtureTests.swift | 6 + clients/typescript/test/client.test.ts | 62 ++++++-- .../typescript/test/types-round-trip.test.ts | 5 + .../20260915-repository-session-config.json | 2 +- docs/specification/root-channel.md | 2 - docs/specification/session-channel.md | 57 ++++--- schema/actions.schema.json | 75 ++++----- schema/commands.schema.json | 145 ++++++++++-------- schema/errors.schema.json | 145 ++++++++++-------- schema/notifications.schema.json | 91 +++++------ schema/state.schema.json | 75 ++++----- scripts/generate-csharp.ts | 3 +- scripts/generate-go.ts | 3 +- scripts/generate-json-schema.test.ts | 87 ++++++++--- scripts/generate-json-schema.ts | 10 ++ scripts/generate-kotlin.ts | 3 +- scripts/generate-rust.ts | 7 +- scripts/generate-swift.ts | 3 +- types/channels-root/commands.ts | 30 ++-- types/channels-root/notifications.ts | 2 - types/channels-root/state.ts | 11 -- types/channels-session/commands.ts | 25 +-- types/channels-session/state.ts | 29 +++- types/common/commands.ts | 24 +++ ...-survive-ready-config-and-directories.json | 46 ++++++ ...repositories-survive-creation-failure.json | 34 ++++ .../046-repository-session-source-only.json | 4 +- .../047-repository-session-revision.json | 6 +- .../048-repository-session-failed.json | 6 +- ...049-repository-preparation-capability.json | 18 +++ .../049-repository-source-capability.json | 58 ------- .../050-repository-preparation-revisions.json | 18 +++ .../051-repository-preparation-single.json | 18 +++ .../052-repository-preparation-multiple.json | 18 +++ ...tory-session-request-default-revision.json | 16 ++ ...4-repository-session-request-multiple.json | 20 +++ .../055-repository-config-query-context.json | 18 +++ ...repository-config-completions-context.json | 22 +++ .../057-repository-session-summary.json | 34 ++++ ...058-repository-session-multiple-ready.json | 40 +++++ 64 files changed, 1223 insertions(+), 720 deletions(-) create mode 100644 types/test-cases/reducers/272-repositories-survive-ready-config-and-directories.json create mode 100644 types/test-cases/reducers/273-repositories-survive-creation-failure.json create mode 100644 types/test-cases/round-trips/049-repository-preparation-capability.json delete mode 100644 types/test-cases/round-trips/049-repository-source-capability.json create mode 100644 types/test-cases/round-trips/050-repository-preparation-revisions.json create mode 100644 types/test-cases/round-trips/051-repository-preparation-single.json create mode 100644 types/test-cases/round-trips/052-repository-preparation-multiple.json create mode 100644 types/test-cases/round-trips/053-repository-session-request-default-revision.json create mode 100644 types/test-cases/round-trips/054-repository-session-request-multiple.json create mode 100644 types/test-cases/round-trips/055-repository-config-query-context.json create mode 100644 types/test-cases/round-trips/056-repository-config-completions-context.json create mode 100644 types/test-cases/round-trips/057-repository-session-summary.json create mode 100644 types/test-cases/round-trips/058-repository-session-multiple-ready.json diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs index 92f176023..f09cdb4ee 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs @@ -191,6 +191,12 @@ public sealed record InitializeResult [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? DefaultDirectory { get; init; } + /// Host-owned repository preparation for session creation and repository + /// context in configuration queries. Absence means unsupported; an empty + /// object supports one repository at its default revision. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public RepositoryPreparationCapabilities? RepositoryPreparation { get; init; } + /// Characters that, when typed in a {@link Message} input, SHOULD cause /// the client to issue a `completions` request with /// {@link CompletionItemKind.UserMessage}. Typically includes characters like @@ -271,6 +277,22 @@ public sealed record ClientCapabilities public Dictionary? McpApps { get; init; } } +/// Repository preparation supported by this host, independent of the selected +/// agent. Resulting working directories must still fit that agent's existing +/// directory capabilities. +public sealed record RepositoryPreparationCapabilities +{ + /// When true, clients may supply {@link RepositorySource.revision}. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? Revision { get; init; } + + /// When true, clients may supply more than one repository. When absent or + /// false, the host MUST reject lists with more than one entry with + /// `InvalidParams` before preparation. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? MultipleRepositories { get; init; } +} + /// Automation features supported by this host authority. /// /// The presence of this object advertises the baseline `ahp-automations://` @@ -495,22 +517,19 @@ public sealed record CreateSessionParams /// and ignores the rest. Dispatch working-directory actions to change the set /// after the session has started. /// - /// A non-empty list and `repositorySource` are mutually exclusive. - /// A repository URI identifies the source, not a working-directory URI; one - /// source may produce multiple directories. + /// A non-empty list and `repositories` are mutually exclusive. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? WorkingDirectories { get; init; } - /// Credential-free source to prepare; requires the agent's repositorySource capability. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? RepositorySource { get; init; } - - /// Requested branch, tag, or commit; requires a source and the capability's revision option. + /// Non-empty repository list to prepare, supported only when the host + /// advertises {@link InitializeResult.repositoryPreparation}. Omit to retain + /// directory/default creation. The resulting working directories MUST fit + /// the selected agent's existing directory capabilities. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? RepositoryRevision { get; init; } + public List? Repositories { get; init; } - /// Session configuration values collected via `resolveSessionConfig`. - /// Keys and values follow the advertised {@link SessionConfigSchema}. + /// Agent-specific configuration values collected via `resolveSessionConfig`. + /// Keys and values correspond to the schema returned by the server. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Dictionary? Config { get; init; } @@ -539,10 +558,7 @@ public sealed record CreateSessionParams /// Disposes a session and cleans up server-side resources. /// -/// The server broadcasts a `root/sessionRemoved` notification to all clients. -/// Disposal MUST NOT erase a shared checkout or uncommitted user changes. -/// Repository cleanup remains host-owned; ending a client's wait or subscription -/// does not grant permission to delete repository data. +/// The server broadcasts a `root/sessionRemoved` notification to all clients. public sealed record DisposeSessionParams { /// Channel URI this command targets. @@ -1366,8 +1382,8 @@ public sealed record DisposeTerminalParams /// the full current property set (not a delta). The returned `values` contain /// server-resolved defaults to pass to `createSession`. /// -/// This command MUST NOT clone or prepare a repository. Repository context -/// requires the agent's `repositorySource` capability. +/// `resolveSessionConfig` and `sessionConfigCompletions` MUST NOT clone or +/// prepare repositories: editing a draft should not create checkouts. public sealed record ResolveSessionConfigParams { public required string Channel { get; init; } @@ -1386,15 +1402,12 @@ public sealed record ResolveSessionConfigParams [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? WorkingDirectory { get; init; } - /// Credential-free source context; not a working-directory URI. + /// Non-empty repository context, subject to + /// {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? RepositorySource { get; init; } + public List? Repositories { get; init; } - /// Requested revision; requires a source and the capability's revision option. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? RepositoryRevision { get; init; } - - /// Current user-filled configuration values; see {@link SessionConfigSchema}. + /// Current user-filled configuration values [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Dictionary? Config { get; init; } } @@ -1432,13 +1445,10 @@ public sealed record SessionConfigCompletionsParams [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? WorkingDirectory { get; init; } - /// Repository context for configuration completions; this MUST NOT prepare a checkout. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? RepositorySource { get; init; } - - /// Requested revision; requires a source and the capability's revision option. + /// Non-empty repository context, subject to + /// {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? RepositoryRevision { get; init; } + public List? Repositories { get; init; } /// Current user-filled configuration values (provides context for the query) [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs index 8dc4c4fbb..041917c48 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs @@ -264,7 +264,8 @@ namespace Microsoft.AgentHostProtocol; [JsonSerializable(typeof(ReconnectResult))] [JsonSerializable(typeof(ReconnectResultType))] [JsonSerializable(typeof(ReconnectSnapshotResult))] -[JsonSerializable(typeof(RepositorySourceCapability))] +[JsonSerializable(typeof(RepositoryPreparationCapabilities))] +[JsonSerializable(typeof(RepositorySource))] [JsonSerializable(typeof(ResolveSessionConfigParams))] [JsonSerializable(typeof(ResolveSessionConfigResult))] [JsonSerializable(typeof(ResourceChange))] diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs index ee43bee44..2640d70f1 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs @@ -117,9 +117,7 @@ public sealed record SessionSummaryChangedParams /// the client then never shows an indicator. /// - Like all notifications this is ephemeral and is **not** replayed on /// reconnect. A client that never receives the terminal frame SHOULD expire -/// the indicator after an idle timeout. -/// - Completion of reported work does not establish session readiness. -/// Observe session lifecycle state for the durable outcome. +/// the indicator after an idle timeout. public sealed record ProgressParams { /// Channel URI this notification belongs to (the root channel). @@ -269,13 +267,12 @@ public sealed record PartialSessionSummary [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? WorkingDirectories { get; init; } - /// Immutable requested source, separate from the host-resolved working directories. + /// Immutable repository intent accepted at creation. When present, this list + /// is non-empty and retained exactly, including order and omitted revisions, + /// from `creating` through `ready` or `failed` and in session summaries. + /// Entries have no one-to-one or positional mapping to `workingDirectories`. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? RepositorySource { get; init; } - - /// Immutable requested revision, not the checkout's current HEAD. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? RepositoryRevision { get; init; } + public List? Repositories { get; init; } /// Lightweight summary of this session's inline annotations channel /// (`ahp-session:/<uuid>/annotations`). Surfaced so badge UI can render diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs index c2eb15108..d5015bee3 100644 --- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs +++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs @@ -861,10 +861,6 @@ public sealed record AgentInfo /// per-capability options. public sealed record AgentCapabilities { - /// The host accepts typed repository inputs for session creation and configuration queries. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public RepositorySourceCapability? RepositorySource { get; init; } - /// The agent can host more than one concurrent chat per session. When absent, /// clients MUST NOT call `createChat` to open chats beyond the default one the /// session starts with. An empty object `{}` advertises multi-chat without @@ -944,12 +940,17 @@ public sealed record MultipleWorkingDirectoriesCapability public bool? PrimaryReplacement { get; init; } } -/// Options for repository-backed session creation. -public sealed record RepositorySourceCapability +/// Requested repository intent, independent of any host-resolved checkout. +/// The same source may appear more than once with different revisions; a source +/// URI is not a checkout identity. +public sealed record RepositorySource { - /// When true, clients may supply an explicit repositoryRevision. + /// Credential-free repository source URI. + public required string Source { get; init; } + + /// Requested branch, tag, or commit. Omit to use the host's default revision. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public bool? Revision { get; init; } + public string? Revision { get; init; } } public sealed record SessionModelInfo @@ -1559,13 +1560,12 @@ public sealed class SessionState [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? WorkingDirectories { get; set; } - /// Immutable requested source, separate from the host-resolved working directories. + /// Immutable repository intent accepted at creation. When present, this list + /// is non-empty and retained exactly, including order and omitted revisions, + /// from `creating` through `ready` or `failed` and in session summaries. + /// Entries have no one-to-one or positional mapping to `workingDirectories`. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? RepositorySource { get; set; } - - /// Immutable requested revision, not the checkout's current HEAD. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? RepositoryRevision { get; set; } + public List? Repositories { get; set; } /// Lightweight summary of this session's inline annotations channel /// (`ahp-session:/<uuid>/annotations`). Surfaced so badge UI can render @@ -1606,7 +1606,7 @@ public sealed class SessionState [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? DefaultChat { get; set; } - /// Provider-specific session configuration schema and current values. + /// Session configuration schema and current values [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public SessionConfigState? Config { get; set; } @@ -1902,13 +1902,12 @@ public sealed class SessionSummary [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? WorkingDirectories { get; set; } - /// Immutable requested source, separate from the host-resolved working directories. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? RepositorySource { get; set; } - - /// Immutable requested revision, not the checkout's current HEAD. + /// Immutable repository intent accepted at creation. When present, this list + /// is non-empty and retained exactly, including order and omitted revisions, + /// from `creating` through `ready` or `failed` and in session summaries. + /// Entries have no one-to-one or positional mapping to `workingDirectories`. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? RepositoryRevision { get; set; } + public List? Repositories { get; set; } /// Lightweight summary of this session's inline annotations channel /// (`ahp-session:/<uuid>/annotations`). Surfaced so badge UI can render diff --git a/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs b/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs index d0fa9c1d6..743d1f78f 100644 --- a/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs +++ b/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs @@ -164,6 +164,12 @@ private static (object decoded, string reencoded) DecodeAndReencode(string type, return Wrap(Ser.Deserialize(inputJson)); case "InitializeResult": return Wrap(Ser.Deserialize(inputJson)); + case "CreateSessionParams": + return Wrap(Ser.Deserialize(inputJson)); + case "ResolveSessionConfigParams": + return Wrap(Ser.Deserialize(inputJson)); + case "SessionConfigCompletionsParams": + return Wrap(Ser.Deserialize(inputJson)); case "Snapshot": return Wrap(Ser.Deserialize(inputJson)); default: diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index 915981dfc..d48d66250 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -160,6 +160,10 @@ type InitializeResult struct { Snapshots []Snapshot `json:"snapshots"` // Suggested default directory for remote filesystem browsing DefaultDirectory *URI `json:"defaultDirectory,omitempty"` + // Host-owned repository preparation for session creation and repository + // context in configuration queries. Absence means unsupported; an empty + // object supports one repository at its default revision. + RepositoryPreparation *RepositoryPreparationCapabilities `json:"repositoryPreparation,omitempty"` // Characters that, when typed in a {@link Message} input, SHOULD cause // the client to issue a `completions` request with // {@link CompletionItemKind.UserMessage}. Typically includes characters like @@ -182,6 +186,18 @@ type InitializeResult struct { Automations *AutomationCapabilities `json:"automations,omitempty"` } +// Repository preparation supported by this host, independent of the selected +// agent. Resulting working directories must still fit that agent's existing +// directory capabilities. +type RepositoryPreparationCapabilities struct { + // When true, clients may supply {@link RepositorySource.revision}. + Revision *bool `json:"revision,omitempty"` + // When true, clients may supply more than one repository. When absent or + // false, the host MUST reject lists with more than one entry with + // `InvalidParams` before preparation. + MultipleRepositories *bool `json:"multipleRepositories,omitempty"` +} + // Optional capabilities a client declares during `initialize`. // // Each field is a presence flag: an empty object `{}` means "supported", @@ -400,16 +416,15 @@ type CreateSessionParams struct { // and ignores the rest. Dispatch working-directory actions to change the set // after the session has started. // - // A non-empty list and `repositorySource` are mutually exclusive. - // A repository URI identifies the source, not a working-directory URI; one - // source may produce multiple directories. + // A non-empty list and `repositories` are mutually exclusive. WorkingDirectories []URI `json:"workingDirectories,omitempty"` - // Credential-free source to prepare; requires the agent's repositorySource capability. - RepositorySource *URI `json:"repositorySource,omitempty"` - // Requested branch, tag, or commit; requires a source and the capability's revision option. - RepositoryRevision *string `json:"repositoryRevision,omitempty"` - // Session configuration values collected via `resolveSessionConfig`. - // Keys and values follow the advertised {@link SessionConfigSchema}. + // Non-empty repository list to prepare, supported only when the host + // advertises {@link InitializeResult.repositoryPreparation}. Omit to retain + // directory/default creation. The resulting working directories MUST fit + // the selected agent's existing directory capabilities. + Repositories []RepositorySource `json:"repositories,omitempty"` + // Agent-specific configuration values collected via `resolveSessionConfig`. + // Keys and values correspond to the schema returned by the server. Config map[string]json.RawMessage `json:"config,omitempty"` // Eagerly claim an active client role for the new session. // @@ -434,9 +449,6 @@ type CreateSessionParams struct { // Disposes a session and cleans up server-side resources. // // The server broadcasts a `root/sessionRemoved` notification to all clients. -// Disposal MUST NOT erase a shared checkout or uncommitted user changes. -// Repository cleanup remains host-owned; ending a client's wait or subscription -// does not grant permission to delete repository data. type DisposeSessionParams struct { // Channel URI this command targets. Channel URI `json:"channel"` @@ -1085,8 +1097,8 @@ type DisposeTerminalParams struct { // the full current property set (not a delta). The returned `values` contain // server-resolved defaults to pass to `createSession`. // -// This command MUST NOT clone or prepare a repository. Repository context -// requires the agent's `repositorySource` capability. +// `resolveSessionConfig` and `sessionConfigCompletions` MUST NOT clone or +// prepare repositories: editing a draft should not create checkouts. type ResolveSessionConfigParams struct { // Channel URI this command targets. Channel URI `json:"channel"` @@ -1097,11 +1109,10 @@ type ResolveSessionConfigParams struct { Provider *string `json:"provider,omitempty"` // Working directory for the session WorkingDirectory *URI `json:"workingDirectory,omitempty"` - // Credential-free source context; not a working-directory URI. - RepositorySource *URI `json:"repositorySource,omitempty"` - // Requested revision; requires a source and the capability's revision option. - RepositoryRevision *string `json:"repositoryRevision,omitempty"` - // Current user-filled configuration values; see {@link SessionConfigSchema}. + // Non-empty repository context, subject to + // {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`. + Repositories []RepositorySource `json:"repositories,omitempty"` + // Current user-filled configuration values Config map[string]json.RawMessage `json:"config,omitempty"` } @@ -1128,10 +1139,9 @@ type SessionConfigCompletionsParams struct { Provider *string `json:"provider,omitempty"` // Working directory for the session WorkingDirectory *URI `json:"workingDirectory,omitempty"` - // Repository context for configuration completions; this MUST NOT prepare a checkout. - RepositorySource *URI `json:"repositorySource,omitempty"` - // Requested revision; requires a source and the capability's revision option. - RepositoryRevision *string `json:"repositoryRevision,omitempty"` + // Non-empty repository context, subject to + // {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`. + Repositories []RepositorySource `json:"repositories,omitempty"` // Current user-filled configuration values (provides context for the query) Config map[string]json.RawMessage `json:"config,omitempty"` // Property id from the schema to query values for diff --git a/clients/go/ahptypes/notifications.generated.go b/clients/go/ahptypes/notifications.generated.go index 1f31ec1ab..7a5f18021 100644 --- a/clients/go/ahptypes/notifications.generated.go +++ b/clients/go/ahptypes/notifications.generated.go @@ -115,8 +115,6 @@ type SessionSummaryChangedParams struct { // - Like all notifications this is ephemeral and is **not** replayed on // reconnect. A client that never receives the terminal frame SHOULD expire // the indicator after an idle timeout. -// - Completion of reported work does not establish session readiness. -// Observe session lifecycle state for the durable outcome. type ProgressParams struct { // Channel URI this notification belongs to (the root channel). Channel URI `json:"channel"` @@ -234,10 +232,11 @@ type PartialSessionSummary struct { // {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a // chat that sets none operates against this full set. WorkingDirectories []URI `json:"workingDirectories,omitempty"` - // Immutable requested source, separate from the host-resolved working directories. - RepositorySource *URI `json:"repositorySource,omitempty"` - // Immutable requested revision, not the checkout's current HEAD. - RepositoryRevision *string `json:"repositoryRevision,omitempty"` + // Immutable repository intent accepted at creation. When present, this list + // is non-empty and retained exactly, including order and omitted revisions, + // from `creating` through `ready` or `failed` and in session summaries. + // Entries have no one-to-one or positional mapping to `workingDirectories`. + Repositories []RepositorySource `json:"repositories,omitempty"` // Lightweight summary of this session's inline annotations channel // (`ahp-session://annotations`). Surfaced so badge UI can render // annotation / entry counts without subscribing. Absent when the session diff --git a/clients/go/ahptypes/roundtrip_fixture_test.go b/clients/go/ahptypes/roundtrip_fixture_test.go index 0f7181385..4d54bc7fc 100644 --- a/clients/go/ahptypes/roundtrip_fixture_test.go +++ b/clients/go/ahptypes/roundtrip_fixture_test.go @@ -242,6 +242,18 @@ func decodeAndReencode(t *testing.T, name, typ, inputJSON string) string { var v InitializeResult dec(&v) return enc(&v) + case "CreateSessionParams": + var v CreateSessionParams + dec(&v) + return enc(&v) + case "ResolveSessionConfigParams": + var v ResolveSessionConfigParams + dec(&v) + return enc(&v) + case "SessionConfigCompletionsParams": + var v SessionConfigCompletionsParams + dec(&v) + return enc(&v) case "ChatSource": var v ChatSource dec(&v) diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 4e195889c..ef170c14e 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -676,8 +676,6 @@ type AgentInfo struct { // corresponding client commands MUST NOT be used. Sub-fields carry // per-capability options. type AgentCapabilities struct { - // The host accepts typed repository inputs for session creation and configuration queries. - RepositorySource *RepositorySourceCapability `json:"repositorySource,omitempty"` // The agent can host more than one concurrent chat per session. When absent, // clients MUST NOT call `createChat` to open chats beyond the default one the // session starts with. An empty object `{}` advertises multi-chat without @@ -746,10 +744,14 @@ type MultipleWorkingDirectoriesCapability struct { PrimaryReplacement *bool `json:"primaryReplacement,omitempty"` } -// Options for repository-backed session creation. -type RepositorySourceCapability struct { - // When true, clients may supply an explicit repositoryRevision. - Revision *bool `json:"revision,omitempty"` +// Requested repository intent, independent of any host-resolved checkout. +// The same source may appear more than once with different revisions; a source +// URI is not a checkout identity. +type RepositorySource struct { + // Credential-free repository source URI. + Source URI `json:"source"` + // Requested branch, tag, or commit. Omit to use the host's default revision. + Revision *string `json:"revision,omitempty"` } type SessionModelInfo struct { @@ -885,10 +887,11 @@ type SessionState struct { // {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a // chat that sets none operates against this full set. WorkingDirectories []URI `json:"workingDirectories,omitempty"` - // Immutable requested source, separate from the host-resolved working directories. - RepositorySource *URI `json:"repositorySource,omitempty"` - // Immutable requested revision, not the checkout's current HEAD. - RepositoryRevision *string `json:"repositoryRevision,omitempty"` + // Immutable repository intent accepted at creation. When present, this list + // is non-empty and retained exactly, including order and omitted revisions, + // from `creating` through `ready` or `failed` and in session summaries. + // Entries have no one-to-one or positional mapping to `workingDirectories`. + Repositories []RepositorySource `json:"repositories,omitempty"` // Lightweight summary of this session's inline annotations channel // (`ahp-session://annotations`). Surfaced so badge UI can render // annotation / entry counts without subscribing. Absent when the session @@ -917,7 +920,7 @@ type SessionState struct { // marker — chats remain equal peers at the protocol level. Hosts MAY change // this over the session's lifetime. DefaultChat *URI `json:"defaultChat,omitempty"` - // Provider-specific session configuration schema and current values. + // Session configuration schema and current values Config *SessionConfigState `json:"config,omitempty"` // Top-level customizations active in this session. // @@ -1165,10 +1168,11 @@ type SessionSummary struct { // {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a // chat that sets none operates against this full set. WorkingDirectories []URI `json:"workingDirectories,omitempty"` - // Immutable requested source, separate from the host-resolved working directories. - RepositorySource *URI `json:"repositorySource,omitempty"` - // Immutable requested revision, not the checkout's current HEAD. - RepositoryRevision *string `json:"repositoryRevision,omitempty"` + // Immutable repository intent accepted at creation. When present, this list + // is non-empty and retained exactly, including order and omitted revisions, + // from `creating` through `ready` or `failed` and in session summaries. + // Entries have no one-to-one or positional mapping to `workingDirectories`. + Repositories []RepositorySource `json:"repositories,omitempty"` // Lightweight summary of this session's inline annotations channel // (`ahp-session://annotations`). Surfaced so badge UI can render // annotation / entry counts without subscribing. Absent when the session 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 65a61d25a..2c647c8da 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 @@ -363,6 +363,12 @@ data class InitializeResult( * Suggested default directory for remote filesystem browsing */ val defaultDirectory: String? = null, + /** + * Host-owned repository preparation for session creation and repository + * context in configuration queries. Absence means unsupported; an empty + * object supports one repository at its default revision. + */ + val repositoryPreparation: RepositoryPreparationCapabilities? = null, /** * Characters that, when typed in a {@link Message} input, SHOULD cause * the client to issue a `completions` request with @@ -393,6 +399,20 @@ data class InitializeResult( val automations: AutomationCapabilities? = null ) +@Serializable +data class RepositoryPreparationCapabilities( + /** + * When true, clients may supply {@link RepositorySource.revision}. + */ + val revision: Boolean? = null, + /** + * When true, clients may supply more than one repository. When absent or + * false, the host MUST reject lists with more than one entry with + * `InvalidParams` before preparation. + */ + val multipleRepositories: Boolean? = null +) + @Serializable data class ClientCapabilities( /** @@ -618,22 +638,19 @@ data class CreateSessionParams( * and ignores the rest. Dispatch working-directory actions to change the set * after the session has started. * - * A non-empty list and `repositorySource` are mutually exclusive. - * A repository URI identifies the source, not a working-directory URI; one - * source may produce multiple directories. + * A non-empty list and `repositories` are mutually exclusive. */ val workingDirectories: List? = null, /** - * Credential-free source to prepare; requires the agent's repositorySource capability. - */ - val repositorySource: String? = null, - /** - * Requested branch, tag, or commit; requires a source and the capability's revision option. + * Non-empty repository list to prepare, supported only when the host + * advertises {@link InitializeResult.repositoryPreparation}. Omit to retain + * directory/default creation. The resulting working directories MUST fit + * the selected agent's existing directory capabilities. */ - val repositoryRevision: String? = null, + val repositories: List? = null, /** - * Session configuration values collected via `resolveSessionConfig`. - * Keys and values follow the advertised {@link SessionConfigSchema}. + * Agent-specific configuration values collected via `resolveSessionConfig`. + * Keys and values correspond to the schema returned by the server. */ val config: Map? = null, /** @@ -1329,15 +1346,12 @@ data class ResolveSessionConfigParams( */ val workingDirectory: String? = null, /** - * Credential-free source context; not a working-directory URI. + * Non-empty repository context, subject to + * {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`. */ - val repositorySource: String? = null, + val repositories: List? = null, /** - * Requested revision; requires a source and the capability's revision option. - */ - val repositoryRevision: String? = null, - /** - * Current user-filled configuration values; see {@link SessionConfigSchema}. + * Current user-filled configuration values */ val config: Map? = null ) @@ -1454,13 +1468,10 @@ data class SessionConfigCompletionsParams( */ val workingDirectory: String? = null, /** - * Repository context for configuration completions; this MUST NOT prepare a checkout. - */ - val repositorySource: String? = null, - /** - * Requested revision; requires a source and the capability's revision option. + * Non-empty repository context, subject to + * {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`. */ - val repositoryRevision: String? = null, + val repositories: List? = null, /** * Current user-filled configuration values (provides context for the query) */ diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt index aff36c009..9f45c382d 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt @@ -226,13 +226,12 @@ data class PartialSessionSummary( */ val workingDirectories: List? = null, /** - * Immutable requested source, separate from the host-resolved working directories. + * Immutable repository intent accepted at creation. When present, this list + * is non-empty and retained exactly, including order and omitted revisions, + * from `creating` through `ready` or `failed` and in session summaries. + * Entries have no one-to-one or positional mapping to `workingDirectories`. */ - val repositorySource: String? = null, - /** - * Immutable requested revision, not the checkout's current HEAD. - */ - val repositoryRevision: String? = null, + val repositories: List? = null, /** * Lightweight summary of this session's inline annotations channel * (`ahp-session://annotations`). Surfaced so badge UI can render 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 f6f3b0ba0..9685baeba 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 @@ -1335,10 +1335,6 @@ data class AgentInfo( @Serializable data class AgentCapabilities( - /** - * The host accepts typed repository inputs for session creation and configuration queries. - */ - val repositorySource: RepositorySourceCapability? = null, /** * The agent can host more than one concurrent chat per session. When absent, * clients MUST NOT call `createChat` to open chats beyond the default one the @@ -1420,11 +1416,15 @@ data class MultipleWorkingDirectoriesCapability( ) @Serializable -data class RepositorySourceCapability( +data class RepositorySource( + /** + * Credential-free repository source URI. + */ + val source: String, /** - * When true, clients may supply an explicit repositoryRevision. + * Requested branch, tag, or commit. Omit to use the host's default revision. */ - val revision: Boolean? = null + val revision: String? = null ) @Serializable @@ -1772,13 +1772,12 @@ data class SessionState( */ val workingDirectories: List? = null, /** - * Immutable requested source, separate from the host-resolved working directories. + * Immutable repository intent accepted at creation. When present, this list + * is non-empty and retained exactly, including order and omitted revisions, + * from `creating` through `ready` or `failed` and in session summaries. + * Entries have no one-to-one or positional mapping to `workingDirectories`. */ - val repositorySource: String? = null, - /** - * Immutable requested revision, not the checkout's current HEAD. - */ - val repositoryRevision: String? = null, + val repositories: List? = null, /** * Lightweight summary of this session's inline annotations channel * (`ahp-session://annotations`). Surfaced so badge UI can render @@ -1822,7 +1821,7 @@ data class SessionState( */ val defaultChat: String? = null, /** - * Provider-specific session configuration schema and current values. + * Session configuration schema and current values */ val config: SessionConfigState? = null, /** @@ -2061,13 +2060,12 @@ data class SessionSummary( */ val workingDirectories: List? = null, /** - * Immutable requested source, separate from the host-resolved working directories. - */ - val repositorySource: String? = null, - /** - * Immutable requested revision, not the checkout's current HEAD. + * Immutable repository intent accepted at creation. When present, this list + * is non-empty and retained exactly, including order and omitted revisions, + * from `creating` through `ready` or `failed` and in session summaries. + * Entries have no one-to-one or positional mapping to `workingDirectories`. */ - val repositoryRevision: String? = null, + val repositories: List? = null, /** * Lightweight summary of this session's inline annotations channel * (`ahp-session://annotations`). Surfaced so badge UI can render diff --git a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt index 7af1319f9..5c90f100e 100644 --- a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt +++ b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt @@ -32,6 +32,7 @@ import com.microsoft.agenthostprotocol.generated.ActionEnvelope import com.microsoft.agenthostprotocol.generated.ChangesetOperationTarget import com.microsoft.agenthostprotocol.generated.ChatSource import com.microsoft.agenthostprotocol.generated.Customization +import com.microsoft.agenthostprotocol.generated.CreateSessionParams import com.microsoft.agenthostprotocol.generated.Implementation import com.microsoft.agenthostprotocol.generated.InitializeResult import com.microsoft.agenthostprotocol.generated.JsonRpcErrorResponse @@ -39,7 +40,9 @@ import com.microsoft.agenthostprotocol.generated.JsonRpcNotification import com.microsoft.agenthostprotocol.generated.JsonRpcRequest import com.microsoft.agenthostprotocol.generated.JsonRpcSuccessResponse import com.microsoft.agenthostprotocol.generated.PartialSessionSummary +import com.microsoft.agenthostprotocol.generated.ResolveSessionConfigParams import com.microsoft.agenthostprotocol.generated.SessionAddedParams +import com.microsoft.agenthostprotocol.generated.SessionConfigCompletionsParams import com.microsoft.agenthostprotocol.generated.ChatInputQuestion import com.microsoft.agenthostprotocol.generated.SessionStatus import com.microsoft.agenthostprotocol.generated.SessionSummary @@ -253,6 +256,9 @@ class RoundTripCorpusTest { "PartialSessionSummary" -> rt(PartialSessionSummary.serializer()) "Implementation" -> rt(Implementation.serializer()) "InitializeResult" -> rt(InitializeResult.serializer()) + "CreateSessionParams" -> rt(CreateSessionParams.serializer()) + "ResolveSessionConfigParams" -> rt(ResolveSessionConfigParams.serializer()) + "SessionConfigCompletionsParams" -> rt(SessionConfigCompletionsParams.serializer()) "ChatSource" -> rt(ChatSource.serializer()) "Snapshot" -> rt(Snapshot.serializer()) else -> fail( diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index 049601cba..78e3e1795 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -17,8 +17,9 @@ use crate::actions::{ActionEnvelope, StateAction}; use crate::state::{ AgentSelection, AutomationDefinition, AutomationSchedule, AutomationSessionTemplate, AutomationTrigger, AutomationTriggerDefinition, ContentRef, Message, MessageAttachment, - ModelSelection, SessionActiveClient, SessionConfigSchema, SessionSummary, SideChatSelection, - Snapshot, SnapshotState, TelemetryCapabilities, TerminalClaim, TextRange, Turn, + ModelSelection, RepositorySource, SessionActiveClient, SessionConfigSchema, SessionSummary, + SideChatSelection, Snapshot, SnapshotState, TelemetryCapabilities, TerminalClaim, TextRange, + Turn, }; // ─── Enums ──────────────────────────────────────────────────────────── @@ -268,6 +269,11 @@ pub struct InitializeResult { /// Suggested default directory for remote filesystem browsing #[serde(default, skip_serializing_if = "Option::is_none")] pub default_directory: Option, + /// Host-owned repository preparation for session creation and repository + /// context in configuration queries. Absence means unsupported; an empty + /// object supports one repository at its default revision. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repository_preparation: Option, /// Characters that, when typed in a {@link Message} input, SHOULD cause /// the client to issue a `completions` request with /// {@link CompletionItemKind.UserMessage}. Typically includes characters like @@ -294,6 +300,22 @@ pub struct InitializeResult { pub automations: Option, } +/// Repository preparation supported by this host, independent of the selected +/// agent. Resulting working directories must still fit that agent's existing +/// directory capabilities. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct RepositoryPreparationCapabilities { + /// When true, clients may supply {@link RepositorySource.revision}. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub revision: Option, + /// When true, clients may supply more than one repository. When absent or + /// false, the host MUST reject lists with more than one entry with + /// `InvalidParams` before preparation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub multiple_repositories: Option, +} + /// Optional capabilities a client declares during `initialize`. /// /// Each field is a presence flag: an empty object `{}` means "supported", @@ -587,19 +609,17 @@ pub struct CreateSessionParams { /// and ignores the rest. Dispatch working-directory actions to change the set /// after the session has started. /// - /// A non-empty list and `repositorySource` are mutually exclusive. - /// A repository URI identifies the source, not a working-directory URI; one - /// source may produce multiple directories. + /// A non-empty list and `repositories` are mutually exclusive. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, - /// Credential-free source to prepare; requires the agent's repositorySource capability. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub repository_source: Option, - /// Requested branch, tag, or commit; requires a source and the capability's revision option. + /// Non-empty repository list to prepare, supported only when the host + /// advertises {@link InitializeResult.repositoryPreparation}. Omit to retain + /// directory/default creation. The resulting working directories MUST fit + /// the selected agent's existing directory capabilities. #[serde(default, skip_serializing_if = "Option::is_none")] - pub repository_revision: Option, - /// Session configuration values collected via `resolveSessionConfig`. - /// Keys and values follow the advertised {@link SessionConfigSchema}. + pub repositories: Option>, + /// Agent-specific configuration values collected via `resolveSessionConfig`. + /// Keys and values correspond to the schema returned by the server. #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option, /// Eagerly claim an active client role for the new session. @@ -627,9 +647,6 @@ pub struct CreateSessionParams { /// Disposes a session and cleans up server-side resources. /// /// The server broadcasts a `root/sessionRemoved` notification to all clients. -/// Disposal MUST NOT erase a shared checkout or uncommitted user changes. -/// Repository cleanup remains host-owned; ending a client's wait or subscription -/// does not grant permission to delete repository data. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DisposeSessionParams { @@ -1391,8 +1408,8 @@ pub struct DisposeTerminalParams { /// the full current property set (not a delta). The returned `values` contain /// server-resolved defaults to pass to `createSession`. /// -/// This command MUST NOT clone or prepare a repository. Repository context -/// requires the agent's `repositorySource` capability. +/// `resolveSessionConfig` and `sessionConfigCompletions` MUST NOT clone or +/// prepare repositories: editing a draft should not create checkouts. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ResolveSessionConfigParams { @@ -1408,13 +1425,11 @@ pub struct ResolveSessionConfigParams { /// Working directory for the session #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directory: Option, - /// Credential-free source context; not a working-directory URI. + /// Non-empty repository context, subject to + /// {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`. #[serde(default, skip_serializing_if = "Option::is_none")] - pub repository_source: Option, - /// Requested revision; requires a source and the capability's revision option. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub repository_revision: Option, - /// Current user-filled configuration values; see {@link SessionConfigSchema}. + pub repositories: Option>, + /// Current user-filled configuration values #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option, } @@ -1449,12 +1464,10 @@ pub struct SessionConfigCompletionsParams { /// Working directory for the session #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directory: Option, - /// Repository context for configuration completions; this MUST NOT prepare a checkout. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub repository_source: Option, - /// Requested revision; requires a source and the capability's revision option. + /// Non-empty repository context, subject to + /// {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`. #[serde(default, skip_serializing_if = "Option::is_none")] - pub repository_revision: Option, + pub repositories: Option>, /// Current user-filled configuration values (provides context for the query) #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option, diff --git a/clients/rust/crates/ahp-types/src/notifications.rs b/clients/rust/crates/ahp-types/src/notifications.rs index 4f2fb868d..a492ad574 100644 --- a/clients/rust/crates/ahp-types/src/notifications.rs +++ b/clients/rust/crates/ahp-types/src/notifications.rs @@ -14,8 +14,8 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; #[allow(unused_imports)] use crate::state::{ AgentSelection, AnnotationsSummary, ChangesSummary, Changeset, FileEdit, ModelSelection, - ProjectInfo, ProtectedResourceMetadata, SessionChatSummary, SessionOrigin, SessionStatus, - SessionSummary, + ProjectInfo, ProtectedResourceMetadata, RepositorySource, SessionChatSummary, SessionOrigin, + SessionStatus, SessionSummary, }; // ─── Enums ──────────────────────────────────────────────────────────── @@ -154,8 +154,6 @@ pub struct SessionSummaryChangedParams { /// - Like all notifications this is ephemeral and is **not** replayed on /// reconnect. A client that never receives the terminal frame SHOULD expire /// the indicator after an idle timeout. -/// - Completion of reported work does not establish session readiness. -/// Observe session lifecycle state for the durable outcome. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ProgressParams { @@ -295,12 +293,12 @@ pub struct PartialSessionSummary { /// chat that sets none operates against this full set. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, - /// Immutable requested source, separate from the host-resolved working directories. + /// Immutable repository intent accepted at creation. When present, this list + /// is non-empty and retained exactly, including order and omitted revisions, + /// from `creating` through `ready` or `failed` and in session summaries. + /// Entries have no one-to-one or positional mapping to `workingDirectories`. #[serde(default, skip_serializing_if = "Option::is_none")] - pub repository_source: Option, - /// Immutable requested revision, not the checkout's current HEAD. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub repository_revision: Option, + pub repositories: Option>, /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index aac6d32e3..57e87c0e6 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -1623,9 +1623,6 @@ pub struct AgentInfo { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct AgentCapabilities { - /// The host accepts typed repository inputs for session creation and configuration queries. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub repository_source: Option, /// The agent can host more than one concurrent chat per session. When absent, /// clients MUST NOT call `createChat` to open chats beyond the default one the /// session starts with. An empty object `{}` advertises multi-chat without @@ -1704,13 +1701,17 @@ pub struct MultipleWorkingDirectoriesCapability { pub primary_replacement: Option, } -/// Options for repository-backed session creation. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +/// Requested repository intent, independent of any host-resolved checkout. +/// The same source may appear more than once with different revisions; a source +/// URI is not a checkout identity. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct RepositorySourceCapability { - /// When true, clients may supply an explicit repositoryRevision. +pub struct RepositorySource { + /// Credential-free repository source URI. + pub source: Uri, + /// Requested branch, tag, or commit. Omit to use the host's default revision. #[serde(default, skip_serializing_if = "Option::is_none")] - pub revision: Option, + pub revision: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -2033,12 +2034,12 @@ pub struct SessionState { /// chat that sets none operates against this full set. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, - /// Immutable requested source, separate from the host-resolved working directories. + /// Immutable repository intent accepted at creation. When present, this list + /// is non-empty and retained exactly, including order and omitted revisions, + /// from `creating` through `ready` or `failed` and in session summaries. + /// Entries have no one-to-one or positional mapping to `workingDirectories`. #[serde(default, skip_serializing_if = "Option::is_none")] - pub repository_source: Option, - /// Immutable requested revision, not the checkout's current HEAD. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub repository_revision: Option, + pub repositories: Option>, /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -2071,7 +2072,7 @@ pub struct SessionState { /// this over the session's lifetime. #[serde(default, skip_serializing_if = "Option::is_none")] pub default_chat: Option, - /// Provider-specific session configuration schema and current values. + /// Session configuration schema and current values #[serde(default, skip_serializing_if = "Option::is_none")] pub config: Option, /// Top-level customizations active in this session. @@ -2375,12 +2376,12 @@ pub struct SessionSummary { /// chat that sets none operates against this full set. #[serde(default, skip_serializing_if = "Option::is_none")] pub working_directories: Option>, - /// Immutable requested source, separate from the host-resolved working directories. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub repository_source: Option, - /// Immutable requested revision, not the checkout's current HEAD. + /// Immutable repository intent accepted at creation. When present, this list + /// is non-empty and retained exactly, including order and omitted revisions, + /// from `creating` through `ready` or `failed` and in session summaries. + /// Entries have no one-to-one or positional mapping to `workingDirectories`. #[serde(default, skip_serializing_if = "Option::is_none")] - pub repository_revision: Option, + pub repositories: Option>, /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session diff --git a/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs b/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs index 1b81a9caf..999f7097d 100644 --- a/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs +++ b/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs @@ -28,7 +28,10 @@ use ahp_types::{ actions::{ActionEnvelope, StateAction}, - commands::{ChangesetOperationTarget, ChatSource, Implementation, InitializeResult}, + commands::{ + ChangesetOperationTarget, ChatSource, CreateSessionParams, Implementation, + InitializeResult, ResolveSessionConfigParams, SessionConfigCompletionsParams, + }, common::StringOrMarkdown, messages::JsonRpcMessage, notifications::{PartialSessionSummary, SessionAddedParams}, @@ -223,6 +226,9 @@ fn decode_and_reencode(file: &str, type_name: &str, input_json: &str) -> Result< "PartialSessionSummary" => round_trip!(PartialSessionSummary), "Implementation" => round_trip!(Implementation), "InitializeResult" => round_trip!(InitializeResult), + "CreateSessionParams" => round_trip!(CreateSessionParams), + "ResolveSessionConfigParams" => round_trip!(ResolveSessionConfigParams), + "SessionConfigCompletionsParams" => round_trip!(SessionConfigCompletionsParams), "ChatSource" => round_trip!(ChatSource), "Snapshot" => round_trip!(Snapshot), other => Err(format!( diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 13293d513..75dfd108c 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -2174,8 +2174,7 @@ mod tests { origin: None, project: None, working_directories: None, - repository_source: None, - repository_revision: None, + repositories: None, annotations: None, lifecycle: SessionLifecycle::Creating, creation_error: None, diff --git a/clients/rust/crates/ahp/tests/client_roundtrip.rs b/clients/rust/crates/ahp/tests/client_roundtrip.rs index 21e58e991..907bb33bc 100644 --- a/clients/rust/crates/ahp/tests/client_roundtrip.rs +++ b/clients/rust/crates/ahp/tests/client_roundtrip.rs @@ -376,8 +376,7 @@ async fn session_config_completions_send_wrapper_targets_root_channel() { meta: None, provider: None, working_directory: None, - repository_source: None, - repository_revision: None, + repositories: None, config: None, property: "baseBranch".into(), query: Some("ma".into()), diff --git a/clients/rust/crates/ahp/tests/hosts.rs b/clients/rust/crates/ahp/tests/hosts.rs index 503a7f048..1b9c7ad31 100644 --- a/clients/rust/crates/ahp/tests/hosts.rs +++ b/clients/rust/crates/ahp/tests/hosts.rs @@ -1096,8 +1096,7 @@ fn make_summary(uri: &str, title: &str, modified_at: i64) -> ahp_types::state::S modified_at: modified, project: None, working_directories: None, - repository_source: None, - repository_revision: None, + repositories: None, changes: None, annotations: None, meta: None, diff --git a/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs b/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs index 8b91f5a86..d20c65338 100644 --- a/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs +++ b/clients/rust/crates/ahp/tests/multi_host_state_mirror.rs @@ -55,8 +55,7 @@ fn session_state(title: &str, _resource: &str) -> SessionState { origin: None, project: None, working_directories: None, - repository_source: None, - repository_revision: None, + repositories: None, annotations: None, lifecycle: SessionLifecycle::Ready, creation_error: None, @@ -478,8 +477,7 @@ fn non_action_event_is_ignored() { modified_at: "1970-01-01T00:00:00.000Z".into(), project: None, working_directories: None, - repository_source: None, - repository_revision: None, + repositories: None, changes: None, annotations: None, meta: None, diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index fd5f68304..9a1bdd7dd 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -321,6 +321,10 @@ public struct InitializeResult: Codable, Sendable { public var snapshots: [Snapshot] /// Suggested default directory for remote filesystem browsing public var defaultDirectory: String? + /// Host-owned repository preparation for session creation and repository + /// context in configuration queries. Absence means unsupported; an empty + /// object supports one repository at its default revision. + public var repositoryPreparation: RepositoryPreparationCapabilities? /// Characters that, when typed in a {@link Message} input, SHOULD cause /// the client to issue a `completions` request with /// {@link CompletionItemKind.UserMessage}. Typically includes characters like @@ -349,6 +353,7 @@ public struct InitializeResult: Codable, Sendable { case meta = "_meta" case snapshots case defaultDirectory + case repositoryPreparation case completionTriggerCharacters case terminalCommandPrefix case telemetry @@ -362,6 +367,7 @@ public struct InitializeResult: Codable, Sendable { meta: [String: AnyCodable]? = nil, snapshots: [Snapshot], defaultDirectory: String? = nil, + repositoryPreparation: RepositoryPreparationCapabilities? = nil, completionTriggerCharacters: [String]? = nil, terminalCommandPrefix: String? = nil, telemetry: TelemetryCapabilities? = nil, @@ -373,6 +379,7 @@ public struct InitializeResult: Codable, Sendable { self.meta = meta self.snapshots = snapshots self.defaultDirectory = defaultDirectory + self.repositoryPreparation = repositoryPreparation self.completionTriggerCharacters = completionTriggerCharacters self.terminalCommandPrefix = terminalCommandPrefix self.telemetry = telemetry @@ -427,6 +434,23 @@ public struct AutomationCapabilities: Codable, Sendable { } } +public struct RepositoryPreparationCapabilities: Codable, Sendable { + /// When true, clients may supply {@link RepositorySource.revision}. + public var revision: Bool? + /// When true, clients may supply more than one repository. When absent or + /// false, the host MUST reject lists with more than one entry with + /// `InvalidParams` before preparation. + public var multipleRepositories: Bool? + + public init( + revision: Bool? = nil, + multipleRepositories: Bool? = nil + ) { + self.revision = revision + self.multipleRepositories = multipleRepositories + } +} + public struct AutomationCreateCapability: Codable, Sendable { public init( @@ -652,16 +676,15 @@ public struct CreateSessionParams: Codable, Sendable { /// and ignores the rest. Dispatch working-directory actions to change the set /// after the session has started. /// - /// A non-empty list and `repositorySource` are mutually exclusive. - /// A repository URI identifies the source, not a working-directory URI; one - /// source may produce multiple directories. + /// A non-empty list and `repositories` are mutually exclusive. public var workingDirectories: [String]? - /// Credential-free source to prepare; requires the agent's repositorySource capability. - public var repositorySource: String? - /// Requested branch, tag, or commit; requires a source and the capability's revision option. - public var repositoryRevision: String? - /// Session configuration values collected via `resolveSessionConfig`. - /// Keys and values follow the advertised {@link SessionConfigSchema}. + /// Non-empty repository list to prepare, supported only when the host + /// advertises {@link InitializeResult.repositoryPreparation}. Omit to retain + /// directory/default creation. The resulting working directories MUST fit + /// the selected agent's existing directory capabilities. + public var repositories: [RepositorySource]? + /// Agent-specific configuration values collected via `resolveSessionConfig`. + /// Keys and values correspond to the schema returned by the server. public var config: [String: AnyCodable]? /// Eagerly claim an active client role for the new session. /// @@ -687,8 +710,7 @@ public struct CreateSessionParams: Codable, Sendable { case meta = "_meta" case provider case workingDirectories - case repositorySource - case repositoryRevision + case repositories case config case activeClient case progressToken @@ -699,8 +721,7 @@ public struct CreateSessionParams: Codable, Sendable { meta: [String: AnyCodable]? = nil, provider: String? = nil, workingDirectories: [String]? = nil, - repositorySource: String? = nil, - repositoryRevision: String? = nil, + repositories: [RepositorySource]? = nil, config: [String: AnyCodable]? = nil, activeClient: SessionActiveClient? = nil, progressToken: String? = nil @@ -709,8 +730,7 @@ public struct CreateSessionParams: Codable, Sendable { self.meta = meta self.provider = provider self.workingDirectories = workingDirectories - self.repositorySource = repositorySource - self.repositoryRevision = repositoryRevision + self.repositories = repositories self.config = config self.activeClient = activeClient self.progressToken = progressToken @@ -1610,11 +1630,10 @@ public struct ResolveSessionConfigParams: Codable, Sendable { public var provider: String? /// Working directory for the session public var workingDirectory: String? - /// Credential-free source context; not a working-directory URI. - public var repositorySource: String? - /// Requested revision; requires a source and the capability's revision option. - public var repositoryRevision: String? - /// Current user-filled configuration values; see {@link SessionConfigSchema}. + /// Non-empty repository context, subject to + /// {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`. + public var repositories: [RepositorySource]? + /// Current user-filled configuration values public var config: [String: AnyCodable]? enum CodingKeys: String, CodingKey { @@ -1622,8 +1641,7 @@ public struct ResolveSessionConfigParams: Codable, Sendable { case meta = "_meta" case provider case workingDirectory - case repositorySource - case repositoryRevision + case repositories case config } @@ -1632,16 +1650,14 @@ public struct ResolveSessionConfigParams: Codable, Sendable { meta: [String: AnyCodable]? = nil, provider: String? = nil, workingDirectory: String? = nil, - repositorySource: String? = nil, - repositoryRevision: String? = nil, + repositories: [RepositorySource]? = nil, config: [String: AnyCodable]? = nil ) { self.channel = channel self.meta = meta self.provider = provider self.workingDirectory = workingDirectory - self.repositorySource = repositorySource - self.repositoryRevision = repositoryRevision + self.repositories = repositories self.config = config } } @@ -1773,10 +1789,9 @@ public struct SessionConfigCompletionsParams: Codable, Sendable { public var provider: String? /// Working directory for the session public var workingDirectory: String? - /// Repository context for configuration completions; this MUST NOT prepare a checkout. - public var repositorySource: String? - /// Requested revision; requires a source and the capability's revision option. - public var repositoryRevision: String? + /// Non-empty repository context, subject to + /// {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`. + public var repositories: [RepositorySource]? /// Current user-filled configuration values (provides context for the query) public var config: [String: AnyCodable]? /// Property id from the schema to query values for @@ -1789,8 +1804,7 @@ public struct SessionConfigCompletionsParams: Codable, Sendable { case meta = "_meta" case provider case workingDirectory - case repositorySource - case repositoryRevision + case repositories case config case property case query @@ -1801,8 +1815,7 @@ public struct SessionConfigCompletionsParams: Codable, Sendable { meta: [String: AnyCodable]? = nil, provider: String? = nil, workingDirectory: String? = nil, - repositorySource: String? = nil, - repositoryRevision: String? = nil, + repositories: [RepositorySource]? = nil, config: [String: AnyCodable]? = nil, property: String, query: String? = nil @@ -1811,8 +1824,7 @@ public struct SessionConfigCompletionsParams: Codable, Sendable { self.meta = meta self.provider = provider self.workingDirectory = workingDirectory - self.repositorySource = repositorySource - self.repositoryRevision = repositoryRevision + self.repositories = repositories self.config = config self.property = property self.query = query diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift index 059944d88..3c3ff33be 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift @@ -217,10 +217,11 @@ public struct PartialSessionSummary: Codable, Sendable { /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a /// chat that sets none operates against this full set. public var workingDirectories: [String]? - /// Immutable requested source, separate from the host-resolved working directories. - public var repositorySource: String? - /// Immutable requested revision, not the checkout's current HEAD. - public var repositoryRevision: String? + /// Immutable repository intent accepted at creation. When present, this list + /// is non-empty and retained exactly, including order and omitted revisions, + /// from `creating` through `ready` or `failed` and in session summaries. + /// Entries have no one-to-one or positional mapping to `workingDirectories`. + public var repositories: [RepositorySource]? /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -259,8 +260,7 @@ public struct PartialSessionSummary: Codable, Sendable { case origin case project case workingDirectories - case repositorySource - case repositoryRevision + case repositories case annotations case resource case createdAt @@ -279,8 +279,7 @@ public struct PartialSessionSummary: Codable, Sendable { origin: SessionOrigin? = nil, project: ProjectInfo? = nil, workingDirectories: [String]? = nil, - repositorySource: String? = nil, - repositoryRevision: String? = nil, + repositories: [RepositorySource]? = nil, annotations: AnnotationsSummary? = nil, resource: String? = nil, createdAt: String? = nil, @@ -297,8 +296,7 @@ public struct PartialSessionSummary: Codable, Sendable { self.origin = origin self.project = project self.workingDirectories = workingDirectories - self.repositorySource = repositorySource - self.repositoryRevision = repositoryRevision + self.repositories = repositories self.annotations = annotations self.resource = resource self.createdAt = createdAt diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 49c623a09..6660c0e3d 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -1321,8 +1321,6 @@ public struct AgentInfo: Codable, Sendable { } public struct AgentCapabilities: Codable, Sendable { - /// The host accepts typed repository inputs for session creation and configuration queries. - public var repositorySource: RepositorySourceCapability? /// The agent can host more than one concurrent chat per session. When absent, /// clients MUST NOT call `createChat` to open chats beyond the default one the /// session starts with. An empty object `{}` advertises multi-chat without @@ -1340,11 +1338,9 @@ public struct AgentCapabilities: Codable, Sendable { public var multipleWorkingDirectories: MultipleWorkingDirectoriesCapability? public init( - repositorySource: RepositorySourceCapability? = nil, multipleChats: MultipleChatsCapability? = nil, multipleWorkingDirectories: MultipleWorkingDirectoriesCapability? = nil ) { - self.repositorySource = repositorySource self.multipleChats = multipleChats self.multipleWorkingDirectories = multipleWorkingDirectories } @@ -1415,13 +1411,17 @@ public struct MultipleWorkingDirectoriesCapability: Codable, Sendable { } } -public struct RepositorySourceCapability: Codable, Sendable { - /// When true, clients may supply an explicit repositoryRevision. - public var revision: Bool? +public struct RepositorySource: Codable, Sendable { + /// Credential-free repository source URI. + public var source: String + /// Requested branch, tag, or commit. Omit to use the host's default revision. + public var revision: String? public init( - revision: Bool? = nil + source: String, + revision: String? = nil ) { + self.source = source self.revision = revision } } @@ -1826,10 +1826,11 @@ public struct SessionState: Codable, Sendable { /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a /// chat that sets none operates against this full set. public var workingDirectories: [String]? - /// Immutable requested source, separate from the host-resolved working directories. - public var repositorySource: String? - /// Immutable requested revision, not the checkout's current HEAD. - public var repositoryRevision: String? + /// Immutable repository intent accepted at creation. When present, this list + /// is non-empty and retained exactly, including order and omitted revisions, + /// from `creating` through `ready` or `failed` and in session summaries. + /// Entries have no one-to-one or positional mapping to `workingDirectories`. + public var repositories: [RepositorySource]? /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -1858,7 +1859,7 @@ public struct SessionState: Codable, Sendable { /// marker — chats remain equal peers at the protocol level. Hosts MAY change /// this over the session's lifetime. public var defaultChat: String? - /// Provider-specific session configuration schema and current values. + /// Session configuration schema and current values public var config: SessionConfigState? /// Top-level customizations active in this session. /// @@ -1919,8 +1920,7 @@ public struct SessionState: Codable, Sendable { case origin case project case workingDirectories - case repositorySource - case repositoryRevision + case repositories case annotations case lifecycle case creationError @@ -1943,8 +1943,7 @@ public struct SessionState: Codable, Sendable { origin: SessionOrigin? = nil, project: ProjectInfo? = nil, workingDirectories: [String]? = nil, - repositorySource: String? = nil, - repositoryRevision: String? = nil, + repositories: [RepositorySource]? = nil, annotations: AnnotationsSummary? = nil, lifecycle: SessionLifecycle, creationError: ErrorInfo? = nil, @@ -1965,8 +1964,7 @@ public struct SessionState: Codable, Sendable { self.origin = origin self.project = project self.workingDirectories = workingDirectories - self.repositorySource = repositorySource - self.repositoryRevision = repositoryRevision + self.repositories = repositories self.annotations = annotations self.lifecycle = lifecycle self.creationError = creationError @@ -2164,10 +2162,11 @@ public struct SessionSummary: Codable, Sendable { /// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a /// chat that sets none operates against this full set. public var workingDirectories: [String]? - /// Immutable requested source, separate from the host-resolved working directories. - public var repositorySource: String? - /// Immutable requested revision, not the checkout's current HEAD. - public var repositoryRevision: String? + /// Immutable repository intent accepted at creation. When present, this list + /// is non-empty and retained exactly, including order and omitted revisions, + /// from `creating` through `ready` or `failed` and in session summaries. + /// Entries have no one-to-one or positional mapping to `workingDirectories`. + public var repositories: [RepositorySource]? /// Lightweight summary of this session's inline annotations channel /// (`ahp-session://annotations`). Surfaced so badge UI can render /// annotation / entry counts without subscribing. Absent when the session @@ -2206,8 +2205,7 @@ public struct SessionSummary: Codable, Sendable { case origin case project case workingDirectories - case repositorySource - case repositoryRevision + case repositories case annotations case resource case createdAt @@ -2226,8 +2224,7 @@ public struct SessionSummary: Codable, Sendable { origin: SessionOrigin? = nil, project: ProjectInfo? = nil, workingDirectories: [String]? = nil, - repositorySource: String? = nil, - repositoryRevision: String? = nil, + repositories: [RepositorySource]? = nil, annotations: AnnotationsSummary? = nil, resource: String, createdAt: String, @@ -2244,8 +2241,7 @@ public struct SessionSummary: Codable, Sendable { self.origin = origin self.project = project self.workingDirectories = workingDirectories - self.repositorySource = repositorySource - self.repositoryRevision = repositoryRevision + self.repositories = repositories self.annotations = annotations self.resource = resource self.createdAt = createdAt diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift index fc3031a3f..70cb79998 100644 --- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift +++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift @@ -188,6 +188,12 @@ final class TypesRoundTripFixtureTests: XCTestCase { return try reencode(dec.decode(Implementation.self, from: inputData)) case "InitializeResult": return try reencode(dec.decode(InitializeResult.self, from: inputData)) + case "CreateSessionParams": + return try reencode(dec.decode(CreateSessionParams.self, from: inputData)) + case "ResolveSessionConfigParams": + return try reencode(dec.decode(ResolveSessionConfigParams.self, from: inputData)) + case "SessionConfigCompletionsParams": + return try reencode(dec.decode(SessionConfigCompletionsParams.self, from: inputData)) case "ChatSource": return try reencode(dec.decode(ChatSource.self, from: inputData)) case "Snapshot": diff --git a/clients/typescript/test/client.test.ts b/clients/typescript/test/client.test.ts index 56fe0c21b..7e54b2fee 100644 --- a/clients/typescript/test/client.test.ts +++ b/clients/typescript/test/client.test.ts @@ -114,8 +114,37 @@ test('initialize round-trip', async () => { await client.shutdown(); }); -for (const withRevision of [false, true]) { - test(`typed requests round-trip repository intent ${withRevision ? 'with' : 'without'} a revision outside config`, async t => { +for (const repositoryPreparation of [ + undefined, {}, { revision: true }, { multipleRepositories: false }, + { multipleRepositories: true }, { revision: true, multipleRepositories: true }, +]) { + test(`initialize preserves host repository capability ${JSON.stringify(repositoryPreparation)}`, async t => { + const [c, s] = InMemoryTransport.pair(); + const client = new AhpClient(c); + t.after(() => client.shutdown()); + client.connect(); + + const initialization = client.initialize({ clientId: 'repository-client', protocolVersions: ['0.9.0'] }); + const request = await readRequest(s); + const result: InitializeResult = { + protocolVersion: '0.9.0', serverSeq: 0, snapshots: [], + ...(repositoryPreparation === undefined ? {} : { repositoryPreparation }), + }; + reply(s, request.id, result); + assert.deepEqual(await initialization, result); + }); +} + +const repositorySource = 'https://example.org/team/project.git'; +for (const { name, repositories } of [ + { name: 'default revision', repositories: [{ source: repositorySource }] }, + { name: 'explicit revision', repositories: [{ source: repositorySource, revision: 'refs/tags/v1.2.3' }] }, + { name: 'same source at two revisions', repositories: [ + { source: repositorySource, revision: 'main' }, + { source: repositorySource, revision: 'feature' }, + ] }, +]) { + test(`typed requests round-trip repository lists (${name}) outside config`, async t => { const [c, s] = InMemoryTransport.pair(); const client = new AhpClient(c); t.after(() => client.shutdown()); @@ -135,26 +164,23 @@ for (const withRevision of [false, true]) { const discovered = await discovery; assert.deepEqual(discovered.schema, schema); - const repository = { - repositorySource: 'https://example.org/team/project.git', - ...(withRevision ? { repositoryRevision: 'refs/tags/v1.2.3' } : {}), - }; + const context = { repositories, workingDirectory: 'file:///work/context' }; const config = discovered.values; - const resolution = client.request('resolveSessionConfig', { channel: ROOT, ...repository, config }); + const resolution = client.request('resolveSessionConfig', { channel: ROOT, ...context, config }); const resolveRequest = await readRequest(s); assert.equal(resolveRequest.method, 'resolveSessionConfig'); - assert.deepEqual(resolveRequest.params, { channel: ROOT, ...repository, config }); + assert.deepEqual(resolveRequest.params, { channel: ROOT, ...context, config }); reply(s, resolveRequest.id, { schema, values: config }); const resolved = await resolution; assert.deepEqual(resolved.values, config); - const completions = client.request('sessionConfigCompletions', { channel: ROOT, ...repository, config: resolved.values, property: 'mode' }); + const completions = client.request('sessionConfigCompletions', { channel: ROOT, ...context, config: resolved.values, property: 'mode' }); const completionRequest = await readRequest(s); - assert.deepEqual(completionRequest.params, { channel: ROOT, ...repository, config: resolved.values, property: 'mode' }); + assert.deepEqual(completionRequest.params, { channel: ROOT, ...context, config: resolved.values, property: 'mode' }); reply(s, completionRequest.id, { items: [] }); await completions; - const params = { channel: 'ahp-session:/repository-test', ...repository, config: resolved.values }; + const params = { channel: 'ahp-session:/repository-test', repositories, config: resolved.values }; const creation = client.request('createSession', params); const createRequest = await readRequest(s); assert.equal(createRequest.method, 'createSession'); @@ -174,8 +200,7 @@ for (const method of ['resolveSessionConfig', 'sessionConfigCompletions', 'creat const channel = method === 'createSession' ? 'ahp-session:/repository-test' : ROOT; const params = { channel, - repositorySource: 'https://example.org/team/project.git', - repositoryRevision: 'unsupported', + repositories: [{ source: repositorySource, revision: 'unsupported' }], ...(method === 'sessionConfigCompletions' ? { property: 'mode' } : {}), }; const request = method === 'sessionConfigCompletions' @@ -200,8 +225,11 @@ for (const failed of [false, true]) { activeClients: [], chats: [], workingDirectories: [], - repositorySource: 'https://example.org/team/project.git', - repositoryRevision: 'main', + repositories: [ + { source: repositorySource }, + { source: repositorySource, revision: 'main' }, + { source: repositorySource, revision: 'feature' }, + ], config: { schema: { type: 'object', properties: { mode: { type: 'string', title: 'Mode' } } }, values: { mode: 'review' }, @@ -225,7 +253,7 @@ for (const failed of [false, true]) { assert.ok(preparing); assert.equal(preparing.lifecycle, SessionLifecycle.Creating); assert.deepEqual(preparing.config, initial.config); - assert.deepEqual([preparing.repositorySource, preparing.repositoryRevision], [initial.repositorySource, initial.repositoryRevision]); + assert.deepEqual(preparing.repositories, initial.repositories); const joining = new AhpStateMirror(); joining.applySnapshot({ resource, state: preparing, fromSeq: 2 }); @@ -245,7 +273,7 @@ for (const failed of [false, true]) { assert.ok(completed); assert.equal(completed.lifecycle, failed ? SessionLifecycle.Failed : SessionLifecycle.Ready); assert.deepEqual(completed.config, initial.config); - assert.deepEqual([completed.repositorySource, completed.repositoryRevision], [initial.repositorySource, initial.repositoryRevision]); + assert.deepEqual(completed.repositories, initial.repositories); assert.deepEqual(completed.workingDirectories, ['file:///work/project', 'file:///work/project-worktree']); assert.deepEqual(joining.getSession(resource), completed); if (failed) { diff --git a/clients/typescript/test/types-round-trip.test.ts b/clients/typescript/test/types-round-trip.test.ts index f5736a2be..9a4d84eb8 100644 --- a/clients/typescript/test/types-round-trip.test.ts +++ b/clients/typescript/test/types-round-trip.test.ts @@ -61,6 +61,8 @@ import type { import type { SessionAddedParams } from '../src/types/channels-root/notifications.js'; import type { Implementation, InitializeResult } from '../src/types/common/commands.js'; import type { ChatSource } from '../src/types/channels-chat/commands.js'; +import type { CreateSessionParams } from '../src/types/channels-session/commands.js'; +import type { ResolveSessionConfigParams, SessionConfigCompletionsParams } from '../src/types/channels-root/commands.js'; // ─── Fixture directory ─────────────────────────────────────────────────────── @@ -243,6 +245,9 @@ function bindToType(file: string, type: string, parsed: unknown): void { case 'PartialSessionSummary': void (parsed as Partial); break; case 'Implementation': void (parsed as Implementation); break; case 'InitializeResult': void (parsed as InitializeResult); break; + case 'CreateSessionParams': void (parsed as CreateSessionParams); break; + case 'ResolveSessionConfigParams': void (parsed as ResolveSessionConfigParams); break; + case 'SessionConfigCompletionsParams': void (parsed as SessionConfigCompletionsParams); break; case 'ChatSource': void (parsed as ChatSource); break; case 'Snapshot': void (parsed as Snapshot); break; default: diff --git a/docs/.changes/20260915-repository-session-config.json b/docs/.changes/20260915-repository-session-config.json index 864da15d2..acf6cd7f5 100644 --- a/docs/.changes/20260915-repository-session-config.json +++ b/docs/.changes/20260915-repository-session-config.json @@ -1,4 +1,4 @@ { "type": "added", - "message": "Typed optional `repositorySource` and `repositoryRevision` request and session metadata fields, with an explicit agent capability for host-owned repository preparation." + "message": "Typed optional `repositories` lists on session creation, configuration queries, and immutable session metadata, with host-level `repositoryPreparation` discovery." } diff --git a/docs/specification/root-channel.md b/docs/specification/root-channel.md index c0c717285..2ccdad606 100644 --- a/docs/specification/root-channel.md +++ b/docs/specification/root-channel.md @@ -185,8 +185,6 @@ The server MAY emit `root/progress` to report incremental progress on a long-run `progress` is monotonically non-decreasing for a given `progressToken`. `total` is present only when the magnitude is known up front (e.g. a `Content-Length`); when absent, clients SHOULD show an indeterminate indicator. The operation is complete when `progress === total` — the server MUST emit a final frame satisfying this, setting `total` to the final `progress` when the total was never known, after which no further frames reference the token. An optional `message` carries a human-readable description of the work in progress; a client that tracks the token renders its own (localized) label and MAY ignore it, while a generic client MAY display `message` verbatim. The server MAY emit no progress at all (for example when the work was already done), in which case the client simply never shows an indicator. Like the catalogue events, `root/progress` is ephemeral and is **not** replayed on reconnect. -Completing reported work does not establish session readiness. For [repository-backed creation](./session-channel#repository-backed-creation), hosts MAY use this same progress notification, but clients recover the requested intent, resolved directories, and `creating` / `ready` / `failed` outcome from session state. A minimal client can ignore progress entirely. - ## Authentication Events The server MAY emit [`auth/required`](/specification/authentication#auth-expiry-notification) on the root channel when an agent's protected resource needs (re-)authentication. See [Authentication](/specification/authentication) for the full flow. diff --git a/docs/specification/session-channel.md b/docs/specification/session-channel.md index 9ca534ba8..9880a7d0e 100644 --- a/docs/specification/session-channel.md +++ b/docs/specification/session-channel.md @@ -37,38 +37,44 @@ Subscribers receive a [`SessionState`](/reference/session#sessionstate) snapshot #### Repository-backed creation -A host can offer to prepare **one repository for a new session** through typed session-creation inputs. The client collects repository intent; the host owns authorization, credentials, preparation, and cleanup. This capability does not define reusable projects, a repository catalogue, or a general-purpose clone command. +A host can offer to prepare repositories for a new session through a typed list. The client collects repository intent; the host owns authorization, credentials, and preparation. The baseline capability supports **one repository**; a separate option allows future hosts to support multiple repositories without changing the request shape. This capability does not define reusable projects, a repository catalogue, or a general-purpose clone command. ##### Capability and field constraints -The agent opts in through [`AgentCapabilities.repositorySource`](/reference/root#agentcapabilities). As with other agent capabilities, absence means unsupported and `{}` advertises source-based creation. `{ "revision": true }` additionally supports an explicit revision. +The **host**, not an individual agent, opts in through [`InitializeResult.repositoryPreparation`](/reference/common#initialize): -| Request field | Meaning | +| Capability | Meaning | |---|---| -| `repositorySource` | Credential-free repository URI string identifying the requested source. | -| `repositoryRevision` | Optional branch, tag, or commit string. | +| Absent | Repository preparation and repository context in configuration queries are unsupported. | +| `{}` | Supports one repository at its default revision. | +| `revision: true` | Also supports an explicit branch, tag, or commit. | +| `multipleRepositories: true` | Also supports a list containing more than one repository. Absent or `false` means exactly one entry when a list is present. | -Both fields are optional typed properties of [`CreateSessionParams`](/reference/session#createsessionparams), [`ResolveSessionConfigParams`](/reference/root#resolvesessionconfigparams), and [`SessionConfigCompletionsParams`](/reference/root#sessionconfigcompletionsparams). The query fields provide context for provider-specific configuration; they are not entries in `config`. +The optional typed `repositories: RepositorySource[]` field is shared by [`CreateSessionParams`](/reference/session#createsessionparams), [`ResolveSessionConfigParams`](/reference/root#resolvesessionconfigparams), and [`SessionConfigCompletionsParams`](/reference/root#sessionconfigcompletionsparams). Each [`RepositorySource`](/reference/session#repositorysource) contains: -Clients MUST check the capability rather than infer support from a provider name, protocol version, `_meta`, or configuration property. A host MUST NOT accept source input without the capability, or an explicit revision unless `revision` is `true`. Supplying either input in `config` is invalid; hosts MUST reject it rather than silently choose directory/default behavior. There are no alternative standard keys or field-name descriptors. +| Field | Meaning | +|---|---| +| `source` | Required credential-free repository URI identifying the requested source. | +| `revision` | Optional branch, tag, or commit. Omission requests the host's default revision. | -Advertising support does not make either value required. A request without repository intent retains its existing directory/default behavior. The generated request types and schemas declare the fields and their types; the host enforces capability, authorization, and cross-field constraints. Provider-specific `config` and its schema remain independent. +Clients MUST check the host capability rather than infer support from a provider name, protocol version, `_meta`, or configuration property. A host MUST NOT accept repository input without the capability, or an explicit revision unless `revision` is `true`. When `multipleRepositories` is absent or `false`, it MUST reject lists containing more than one entry **before any preparation**, rather than preparing only the first entry. -##### Values and validation +When present, `repositories` MUST be non-empty. An absent list retains existing directory/default creation; advertising support does not make the field required. The generated types and schemas describe the list and entry shape; the host enforces capability, authorization, URI validity, and cross-field constraints. Only the typed `repositories` field carries repository intent. Generic `config` and its schema remain unchanged, with no aliases or repository-specific configuration carrier. -The client supplies the same typed source and optional revision when resolving configuration, requesting configuration completions, and creating the session. Discovery and iterative configuration queries MUST NOT clone or prepare a repository. +##### Values and validation -For example, the root's agent entry can advertise: +For example, the initialization result can advertise single-repository preparation with revision selection: ```json { - "capabilities": { - "repositorySource": { "revision": true } - } + "protocolVersion": "0.9.0", + "serverSeq": 0, + "snapshots": [], + "repositoryPreparation": { "revision": true } } ``` -The client can resolve configuration with `repositorySource` and `repositoryRevision` beside `config`, without a `workingDirectory`, then pass the returned provider configuration to creation: +The client passes the same list as context when resolving configuration or requesting completions, then sends it beside the returned configuration when creating the session: ```json { @@ -77,24 +83,27 @@ The client can resolve configuration with `repositorySource` and `repositoryRevi "method": "createSession", "params": { "channel": "ahp-session:/new-session", - "repositorySource": "https://example.org/team/project.git", - "repositoryRevision": "main", + "repositories": [ + { "source": "https://example.org/team/project.git", "revision": "main" } + ], "config": { "mode": "interactive" } } } ``` -The repository URI identifies the source, not a checkout or host filesystem directory. One source can produce multiple directories, including separate checkouts or worktrees; clients MUST NOT use the source URI as a directory identity. `createSession.repositorySource` and a non-empty `createSession.workingDirectories` list are mutually exclusive. Configuration queries may also include an existing `workingDirectory` as context; they do not perform preparation. +The repository URI identifies a source, not a checkout or host filesystem directory. There is **no one-to-one or positional mapping** between `repositories` and the resolved `workingDirectories`. One source can produce multiple directories, and a multi-repository host may accept the same source at different revisions. Clients MUST NOT deduplicate entries by source URI or use it as a checkout identity. No per-repository IDs are introduced. + +`createSession.repositories` and a non-empty `createSession.workingDirectories` list are mutually exclusive. Configuration queries may include `repositories` together with an existing `workingDirectory` as context; preparation belongs to creation, not draft configuration. The resulting directories MUST fit the selected provider's existing directory capabilities. Host-level repository support does not grant an agent support for multiple working directories. -When supplied, each value MUST be a non-empty string. A revision without a source is invalid. Omit an unused source or revision instead of supplying an empty string. For creation and configuration queries, the host MUST reject invalid or unsupported intent with `InvalidParams` (`-32602`), including an unsupported source or revision, a malformed or credential-bearing source URI, or conflicting creation directories. It MUST NOT silently drop explicit input, select a default directory, or replace an unsupported revision with its default. A repository-aware client MUST surface invalid capability declarations or unsupported input instead of silently dropping the user's intent. +Each entry MUST contain a non-empty `source`, and a supplied `revision` MUST be a non-empty string. A revision without a source is invalid. For creation and configuration queries, the host MUST reject invalid or unsupported intent with `InvalidParams` (`-32602`), including an empty list, an unsupported list length or revision, a malformed or credential-bearing source URI, or conflicting creation directories. It MUST NOT silently drop entries, select a default directory, or replace an unsupported revision with its default. A repository-aware client MUST surface invalid capability declarations or unsupported input instead of silently dropping the user's intent. -Repository URIs and configuration values MUST NOT contain credentials such as passwords or access tokens. Authentication uses the existing [authentication contract](./authentication); the host MUST authorize the requesting client before repository side effects and use only credentials permitted for that request. Credentials MUST NOT appear in session state, progress messages, or logs. +Repository source URIs MUST NOT contain credentials such as passwords or access tokens. Authentication uses the existing [authentication contract](./authentication); the host MUST authorize the requesting client before repository side effects and use only credentials permitted for that request. Credentials MUST NOT appear in session state, progress messages, or logs. ##### Preparation and recoverable state Repository preparation is part of the existing `creating` lifecycle. The host MUST finish preparation before executing turns or publishing `session/ready`. No additional lifecycle state is introduced. -The host MUST publish the accepted, requested source and optional revision as `SessionState.repositorySource` and `SessionState.repositoryRevision` from the initial `creating` snapshot and preserve them through `ready` or `failed`. These immutable fields belong to [`SessionMetadata`](/reference/session#sessionmetadata), so summaries carry the same intent. Preserve requested intent even if the host resolves a branch or tag to a commit; the resolved working location is a separate fact. No configuration action changes these fields. +The host MUST publish the accepted `repositories` list in the initial `creating` snapshot and retain it **exactly**, including entry order and omitted revisions, through `ready` or `failed`. This immutable field belongs to [`SessionMetadata`](/reference/session#sessionmetadata), so summaries carry the same list. Preserve requested intent even if the host resolves a branch or tag to a commit; the resolved working location is a separate fact. Configuration and working-directory actions do not change this list. Before dispatching `session/ready` or `session/creationFailed`, the host MUST publish the actual resolved `workingDirectories` in session state, using the existing snapshot and working-directory actions. While no directory has been resolved, `workingDirectories` MAY be absent or empty; do not claim a checkout was prepared when preparation failed. On failure, the existing `session/creationFailed` action records `lifecycle: "failed"` and `creationError`. Both outcomes retain the requested intent and any resolved directories so clients can recover them from a snapshot or replay. @@ -102,13 +111,13 @@ The host MAY report preparation through the existing `createSession.progressToke ##### Reattachment, retry, and cleanup -`createSession` is not an idempotent preparation command. A duplicate URI still returns `SessionAlreadyExists` (`-32003`), including while preparation is running or after creation has failed; it MUST NOT start another preparation for that session. After a lost response, the client should reattach to the same session URI through subscription or [reconnection](./lifecycle#reconnection) and inspect its state. Before treating the recovered session as the requested creation, it MUST verify that its typed `repositorySource` and `repositoryRevision` match the requested intent and inspect the lifecycle. A mismatch is a conflict, not successful recovery. It MUST NOT treat a duplicate creation error as successful recovery. After a failure is addressed, a user can explicitly retry with a new session URI rather than overwrite the failed session. +`createSession` is not an idempotent preparation command. A duplicate URI still returns `SessionAlreadyExists` (`-32003`), including while preparation is running or after creation has failed; it MUST NOT start another preparation for that session. After a lost response, the client should reattach to the same session URI through subscription or [reconnection](./lifecycle#reconnection) and inspect its state. Before treating the recovered session as the requested creation, it MUST verify that its complete `repositories` list matches the requested intent, including order and revision omission, and inspect the lifecycle. A mismatch is a conflict, not successful recovery. A duplicate creation error alone is not successful recovery. After a failure is addressed, a user can explicitly retry with a new session URI rather than overwrite the failed session; no per-repository retry is introduced. -Cancelling a local wait, disconnecting, or unsubscribing does not grant permission to delete repository data. When the user intends to dispose the session, use the existing `disposeSession` command; this capability adds no cancellation RPC. The host MUST NOT erase a shared checkout or uncommitted user changes during cancellation or disposal. Cleanup of exclusively owned temporary preparation resources remains a host responsibility. +The host owns the lifetime of preparation resources it creates; a source URI does not establish ownership of an existing or shared checkout. This capability adds no cancellation RPC or new disposal rules. ##### Minimal-client behavior -A client supporting this capability collects the source and optional revision separately from provider configuration and sends them as typed request fields. It needs no Git implementation, clone RPC, or progress implementation. Minimal clients can omit the optional capability and continue using directory/default creation. Joining or reconnecting clients read the source, revision, lifecycle and working directories from authoritative session state without repeating preparation. +A client supporting this capability collects a repository list separately from configuration and respects the host's single- or multi-repository limit. It needs no Git implementation, clone RPC, or progress implementation. Minimal clients can omit `repositories` and continue using directory/default creation. Joining or reconnecting clients read the list, lifecycle, and working directories from authoritative session state without repeating preparation. ### Active session diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 91da25776..6a2ad4920 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -2943,10 +2943,6 @@ "type": "object", "description": "Static capabilities an {@link AgentInfo} advertises. Modelled after MCP\ncapabilities: each field is opt-in and its presence (an empty object `{}`)\nsignals support, while absence means the feature is unsupported and the\ncorresponding client commands MUST NOT be used. Sub-fields carry\nper-capability options.", "properties": { - "repositorySource": { - "$ref": "#/$defs/RepositorySourceCapability", - "description": "The host accepts typed repository inputs for session creation and configuration queries." - }, "multipleChats": { "$ref": "#/$defs/MultipleChatsCapability", "description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nsource-based creation; set {@link MultipleChatsCapability.fork} or\n{@link MultipleChatsCapability.sideChat} to allow the corresponding mode." @@ -2957,16 +2953,6 @@ } } }, - "RepositorySourceCapability": { - "type": "object", - "description": "Options for repository-backed session creation.", - "properties": { - "revision": { - "type": "boolean", - "description": "When true, clients may supply an explicit repositoryRevision." - } - } - }, "MultipleChatsCapability": { "type": "object", "description": "Options for the {@link AgentCapabilities.multipleChats} capability.", @@ -3107,6 +3093,23 @@ "run" ] }, + "RepositorySource": { + "type": "object", + "description": "Requested repository intent, independent of any host-resolved checkout.\nThe same source may appear more than once with different revisions; a source\nURI is not a checkout identity.", + "properties": { + "source": { + "$ref": "#/$defs/URI", + "description": "Credential-free repository source URI." + }, + "revision": { + "type": "string", + "description": "Requested branch, tag, or commit. Omit to use the host's default revision." + } + }, + "required": [ + "source" + ] + }, "SessionMetadata": { "type": "object", "description": "Metadata shared between the full {@link SessionState} (delivered when a\nclient subscribes to a session's URI) and the lightweight\n{@link SessionSummary} (carried in the root-channel session catalog).\n\nThese fields describe the session at a glance and appear in both places.\n`SessionState` owns the authoritative values for a subscribed session;\n`SessionSummary` mirrors them into the catalog so clients that only render a\nsession list don't have to subscribe to every session URI. The host keeps\nthe catalog in sync via `root/sessionSummaryChanged`.", @@ -3142,13 +3145,13 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, - "repositorySource": { - "$ref": "#/$defs/URI", - "description": "Immutable requested source, separate from the host-resolved working directories." - }, - "repositoryRevision": { - "type": "string", - "description": "Immutable requested revision, not the checkout's current HEAD." + "repositories": { + "type": "array", + "items": { + "$ref": "#/$defs/RepositorySource" + }, + "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.", + "minItems": 1 }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -3196,13 +3199,13 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, - "repositorySource": { - "$ref": "#/$defs/URI", - "description": "Immutable requested source, separate from the host-resolved working directories." - }, - "repositoryRevision": { - "type": "string", - "description": "Immutable requested revision, not the checkout's current HEAD." + "repositories": { + "type": "array", + "items": { + "$ref": "#/$defs/RepositorySource" + }, + "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.", + "minItems": 1 }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -3243,7 +3246,7 @@ }, "config": { "$ref": "#/$defs/SessionConfigState", - "description": "Provider-specific session configuration schema and current values." + "description": "Session configuration schema and current values" }, "customizations": { "type": "array", @@ -3512,13 +3515,13 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, - "repositorySource": { - "$ref": "#/$defs/URI", - "description": "Immutable requested source, separate from the host-resolved working directories." - }, - "repositoryRevision": { - "type": "string", - "description": "Immutable requested revision, not the checkout's current HEAD." + "repositories": { + "type": "array", + "items": { + "$ref": "#/$defs/RepositorySource" + }, + "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.", + "minItems": 1 }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", diff --git a/schema/commands.schema.json b/schema/commands.schema.json index e3766a977..dccf1a486 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -163,6 +163,10 @@ "$ref": "#/$defs/URI", "description": "Suggested default directory for remote filesystem browsing" }, + "repositoryPreparation": { + "$ref": "#/$defs/RepositoryPreparationCapabilities", + "description": "Host-owned repository preparation for session creation and repository\ncontext in configuration queries. Absence means unsupported; an empty\nobject supports one repository at its default revision." + }, "completionTriggerCharacters": { "type": "array", "items": { @@ -189,6 +193,20 @@ "snapshots" ] }, + "RepositoryPreparationCapabilities": { + "type": "object", + "description": "Repository preparation supported by this host, independent of the selected\nagent. Resulting working directories must still fit that agent's existing\ndirectory capabilities.", + "properties": { + "revision": { + "type": "boolean", + "description": "When true, clients may supply {@link RepositorySource.revision}." + }, + "multipleRepositories": { + "type": "boolean", + "description": "When true, clients may supply more than one repository. When absent or\nfalse, the host MUST reject lists with more than one entry with\n`InvalidParams` before preparation." + } + } + }, "AutomationCapabilities": { "type": "object", "description": "Automation features supported by this host authority.\n\nThe presence of this object advertises the baseline `ahp-automations://`\ncatalogue. Optional fields describe additional host features and\nrestrictions.\n\nCapabilities describe implementation support.\n{@link AutomationEntry.operations} remains authoritative for which\ndefinition mutations are currently allowed on a particular automation.", @@ -949,7 +967,7 @@ }, "ResolveSessionConfigParams": { "type": "object", - "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.\n\nThis command MUST NOT clone or prepare a repository. Repository context\nrequires the agent's `repositorySource` capability.", + "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.\n\n`resolveSessionConfig` and `sessionConfigCompletions` MUST NOT clone or\nprepare repositories: editing a draft should not create checkouts.", "properties": { "channel": { "type": "string", @@ -970,18 +988,18 @@ "$ref": "#/$defs/URI", "description": "Working directory for the session" }, - "repositorySource": { - "$ref": "#/$defs/URI", - "description": "Credential-free source context; not a working-directory URI." - }, - "repositoryRevision": { - "type": "string", - "description": "Requested revision; requires a source and the capability's revision option." + "repositories": { + "type": "array", + "items": { + "$ref": "#/$defs/RepositorySource" + }, + "description": "Non-empty repository context, subject to\n{@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`.", + "minItems": 1 }, "config": { "type": "object", "additionalProperties": {}, - "description": "Current user-filled configuration values; see {@link SessionConfigSchema}." + "description": "Current user-filled configuration values" } }, "required": [ @@ -1052,13 +1070,13 @@ "$ref": "#/$defs/URI", "description": "Working directory for the session" }, - "repositorySource": { - "$ref": "#/$defs/URI", - "description": "Repository context for configuration completions; this MUST NOT prepare a checkout." - }, - "repositoryRevision": { - "type": "string", - "description": "Requested revision; requires a source and the capability's revision option." + "repositories": { + "type": "array", + "items": { + "$ref": "#/$defs/RepositorySource" + }, + "description": "Non-empty repository context, subject to\n{@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`.", + "minItems": 1 }, "config": { "type": "object", @@ -1117,20 +1135,20 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises a protected-primary capability. An\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary | immutable\nprimary} is fixed, while a\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement | replaceable\nprimary} is changed only with `session/workingDirectoryReplaced`.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch working-directory actions to change the set\nafter the session has started.\n\nA non-empty list and `repositorySource` are mutually exclusive.\nA repository URI identifies the source, not a working-directory URI; one\nsource may produce multiple directories." - }, - "repositorySource": { - "$ref": "#/$defs/URI", - "description": "Credential-free source to prepare; requires the agent's repositorySource capability." + "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises a protected-primary capability. An\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary | immutable\nprimary} is fixed, while a\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement | replaceable\nprimary} is changed only with `session/workingDirectoryReplaced`.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch working-directory actions to change the set\nafter the session has started.\n\nA non-empty list and `repositories` are mutually exclusive." }, - "repositoryRevision": { - "type": "string", - "description": "Requested branch, tag, or commit; requires a source and the capability's revision option." + "repositories": { + "type": "array", + "items": { + "$ref": "#/$defs/RepositorySource" + }, + "description": "Non-empty repository list to prepare, supported only when the host\nadvertises {@link InitializeResult.repositoryPreparation}. Omit to retain\ndirectory/default creation. The resulting working directories MUST fit\nthe selected agent's existing directory capabilities.", + "minItems": 1 }, "config": { "type": "object", "additionalProperties": {}, - "description": "Session configuration values collected via `resolveSessionConfig`.\nKeys and values follow the advertised {@link SessionConfigSchema}." + "description": "Agent-specific configuration values collected via `resolveSessionConfig`.\nKeys and values correspond to the schema returned by the server." }, "activeClient": { "$ref": "#/$defs/SessionActiveClient", @@ -1147,7 +1165,7 @@ }, "DisposeSessionParams": { "type": "object", - "description": "Disposes a session and cleans up server-side resources.\n\nThe server broadcasts a `root/sessionRemoved` notification to all clients.\nDisposal MUST NOT erase a shared checkout or uncommitted user changes.\nRepository cleanup remains host-owned; ending a client's wait or subscription\ndoes not grant permission to delete repository data.", + "description": "Disposes a session and cleans up server-side resources.\n\nThe server broadcasts a `root/sessionRemoved` notification to all clients.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -2211,10 +2229,6 @@ "type": "object", "description": "Static capabilities an {@link AgentInfo} advertises. Modelled after MCP\ncapabilities: each field is opt-in and its presence (an empty object `{}`)\nsignals support, while absence means the feature is unsupported and the\ncorresponding client commands MUST NOT be used. Sub-fields carry\nper-capability options.", "properties": { - "repositorySource": { - "$ref": "#/$defs/RepositorySourceCapability", - "description": "The host accepts typed repository inputs for session creation and configuration queries." - }, "multipleChats": { "$ref": "#/$defs/MultipleChatsCapability", "description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nsource-based creation; set {@link MultipleChatsCapability.fork} or\n{@link MultipleChatsCapability.sideChat} to allow the corresponding mode." @@ -2225,16 +2239,6 @@ } } }, - "RepositorySourceCapability": { - "type": "object", - "description": "Options for repository-backed session creation.", - "properties": { - "revision": { - "type": "boolean", - "description": "When true, clients may supply an explicit repositoryRevision." - } - } - }, "MultipleChatsCapability": { "type": "object", "description": "Options for the {@link AgentCapabilities.multipleChats} capability.", @@ -2375,6 +2379,23 @@ "run" ] }, + "RepositorySource": { + "type": "object", + "description": "Requested repository intent, independent of any host-resolved checkout.\nThe same source may appear more than once with different revisions; a source\nURI is not a checkout identity.", + "properties": { + "source": { + "$ref": "#/$defs/URI", + "description": "Credential-free repository source URI." + }, + "revision": { + "type": "string", + "description": "Requested branch, tag, or commit. Omit to use the host's default revision." + } + }, + "required": [ + "source" + ] + }, "SessionMetadata": { "type": "object", "description": "Metadata shared between the full {@link SessionState} (delivered when a\nclient subscribes to a session's URI) and the lightweight\n{@link SessionSummary} (carried in the root-channel session catalog).\n\nThese fields describe the session at a glance and appear in both places.\n`SessionState` owns the authoritative values for a subscribed session;\n`SessionSummary` mirrors them into the catalog so clients that only render a\nsession list don't have to subscribe to every session URI. The host keeps\nthe catalog in sync via `root/sessionSummaryChanged`.", @@ -2410,13 +2431,13 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, - "repositorySource": { - "$ref": "#/$defs/URI", - "description": "Immutable requested source, separate from the host-resolved working directories." - }, - "repositoryRevision": { - "type": "string", - "description": "Immutable requested revision, not the checkout's current HEAD." + "repositories": { + "type": "array", + "items": { + "$ref": "#/$defs/RepositorySource" + }, + "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.", + "minItems": 1 }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2464,13 +2485,13 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, - "repositorySource": { - "$ref": "#/$defs/URI", - "description": "Immutable requested source, separate from the host-resolved working directories." - }, - "repositoryRevision": { - "type": "string", - "description": "Immutable requested revision, not the checkout's current HEAD." + "repositories": { + "type": "array", + "items": { + "$ref": "#/$defs/RepositorySource" + }, + "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.", + "minItems": 1 }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -2511,7 +2532,7 @@ }, "config": { "$ref": "#/$defs/SessionConfigState", - "description": "Provider-specific session configuration schema and current values." + "description": "Session configuration schema and current values" }, "customizations": { "type": "array", @@ -2780,13 +2801,13 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, - "repositorySource": { - "$ref": "#/$defs/URI", - "description": "Immutable requested source, separate from the host-resolved working directories." - }, - "repositoryRevision": { - "type": "string", - "description": "Immutable requested revision, not the checkout's current HEAD." + "repositories": { + "type": "array", + "items": { + "$ref": "#/$defs/RepositorySource" + }, + "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.", + "minItems": 1 }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 3cbd78d6a..7a47919a8 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -616,10 +616,6 @@ "type": "object", "description": "Static capabilities an {@link AgentInfo} advertises. Modelled after MCP\ncapabilities: each field is opt-in and its presence (an empty object `{}`)\nsignals support, while absence means the feature is unsupported and the\ncorresponding client commands MUST NOT be used. Sub-fields carry\nper-capability options.", "properties": { - "repositorySource": { - "$ref": "#/$defs/RepositorySourceCapability", - "description": "The host accepts typed repository inputs for session creation and configuration queries." - }, "multipleChats": { "$ref": "#/$defs/MultipleChatsCapability", "description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nsource-based creation; set {@link MultipleChatsCapability.fork} or\n{@link MultipleChatsCapability.sideChat} to allow the corresponding mode." @@ -630,16 +626,6 @@ } } }, - "RepositorySourceCapability": { - "type": "object", - "description": "Options for repository-backed session creation.", - "properties": { - "revision": { - "type": "boolean", - "description": "When true, clients may supply an explicit repositoryRevision." - } - } - }, "MultipleChatsCapability": { "type": "object", "description": "Options for the {@link AgentCapabilities.multipleChats} capability.", @@ -780,6 +766,23 @@ "run" ] }, + "RepositorySource": { + "type": "object", + "description": "Requested repository intent, independent of any host-resolved checkout.\nThe same source may appear more than once with different revisions; a source\nURI is not a checkout identity.", + "properties": { + "source": { + "$ref": "#/$defs/URI", + "description": "Credential-free repository source URI." + }, + "revision": { + "type": "string", + "description": "Requested branch, tag, or commit. Omit to use the host's default revision." + } + }, + "required": [ + "source" + ] + }, "SessionMetadata": { "type": "object", "description": "Metadata shared between the full {@link SessionState} (delivered when a\nclient subscribes to a session's URI) and the lightweight\n{@link SessionSummary} (carried in the root-channel session catalog).\n\nThese fields describe the session at a glance and appear in both places.\n`SessionState` owns the authoritative values for a subscribed session;\n`SessionSummary` mirrors them into the catalog so clients that only render a\nsession list don't have to subscribe to every session URI. The host keeps\nthe catalog in sync via `root/sessionSummaryChanged`.", @@ -815,13 +818,13 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, - "repositorySource": { - "$ref": "#/$defs/URI", - "description": "Immutable requested source, separate from the host-resolved working directories." - }, - "repositoryRevision": { - "type": "string", - "description": "Immutable requested revision, not the checkout's current HEAD." + "repositories": { + "type": "array", + "items": { + "$ref": "#/$defs/RepositorySource" + }, + "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.", + "minItems": 1 }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -869,13 +872,13 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, - "repositorySource": { - "$ref": "#/$defs/URI", - "description": "Immutable requested source, separate from the host-resolved working directories." - }, - "repositoryRevision": { - "type": "string", - "description": "Immutable requested revision, not the checkout's current HEAD." + "repositories": { + "type": "array", + "items": { + "$ref": "#/$defs/RepositorySource" + }, + "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.", + "minItems": 1 }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -916,7 +919,7 @@ }, "config": { "$ref": "#/$defs/SessionConfigState", - "description": "Provider-specific session configuration schema and current values." + "description": "Session configuration schema and current values" }, "customizations": { "type": "array", @@ -1185,13 +1188,13 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, - "repositorySource": { - "$ref": "#/$defs/URI", - "description": "Immutable requested source, separate from the host-resolved working directories." - }, - "repositoryRevision": { - "type": "string", - "description": "Immutable requested revision, not the checkout's current HEAD." + "repositories": { + "type": "array", + "items": { + "$ref": "#/$defs/RepositorySource" + }, + "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.", + "minItems": 1 }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -5890,6 +5893,10 @@ "$ref": "#/$defs/URI", "description": "Suggested default directory for remote filesystem browsing" }, + "repositoryPreparation": { + "$ref": "#/$defs/RepositoryPreparationCapabilities", + "description": "Host-owned repository preparation for session creation and repository\ncontext in configuration queries. Absence means unsupported; an empty\nobject supports one repository at its default revision." + }, "completionTriggerCharacters": { "type": "array", "items": { @@ -5916,6 +5923,20 @@ "snapshots" ] }, + "RepositoryPreparationCapabilities": { + "type": "object", + "description": "Repository preparation supported by this host, independent of the selected\nagent. Resulting working directories must still fit that agent's existing\ndirectory capabilities.", + "properties": { + "revision": { + "type": "boolean", + "description": "When true, clients may supply {@link RepositorySource.revision}." + }, + "multipleRepositories": { + "type": "boolean", + "description": "When true, clients may supply more than one repository. When absent or\nfalse, the host MUST reject lists with more than one entry with\n`InvalidParams` before preparation." + } + } + }, "AutomationCapabilities": { "type": "object", "description": "Automation features supported by this host authority.\n\nThe presence of this object advertises the baseline `ahp-automations://`\ncatalogue. Optional fields describe additional host features and\nrestrictions.\n\nCapabilities describe implementation support.\n{@link AutomationEntry.operations} remains authoritative for which\ndefinition mutations are currently allowed on a particular automation.", @@ -6676,7 +6697,7 @@ }, "ResolveSessionConfigParams": { "type": "object", - "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.\n\nThis command MUST NOT clone or prepare a repository. Repository context\nrequires the agent's `repositorySource` capability.", + "description": "Iteratively resolves the session configuration schema. The client sends the\ncurrent partial session config and any user-filled metadata values. The server\nreturns a property schema describing what additional metadata is needed,\ncontextual to the current selections.\n\nThe client calls this command whenever the user changes a significant input\n(e.g. picks a working directory, toggles a property). Each response returns\nthe full current property set (not a delta). The returned `values` contain\nserver-resolved defaults to pass to `createSession`.\n\n`resolveSessionConfig` and `sessionConfigCompletions` MUST NOT clone or\nprepare repositories: editing a draft should not create checkouts.", "properties": { "channel": { "type": "string", @@ -6697,18 +6718,18 @@ "$ref": "#/$defs/URI", "description": "Working directory for the session" }, - "repositorySource": { - "$ref": "#/$defs/URI", - "description": "Credential-free source context; not a working-directory URI." - }, - "repositoryRevision": { - "type": "string", - "description": "Requested revision; requires a source and the capability's revision option." + "repositories": { + "type": "array", + "items": { + "$ref": "#/$defs/RepositorySource" + }, + "description": "Non-empty repository context, subject to\n{@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`.", + "minItems": 1 }, "config": { "type": "object", "additionalProperties": {}, - "description": "Current user-filled configuration values; see {@link SessionConfigSchema}." + "description": "Current user-filled configuration values" } }, "required": [ @@ -6779,13 +6800,13 @@ "$ref": "#/$defs/URI", "description": "Working directory for the session" }, - "repositorySource": { - "$ref": "#/$defs/URI", - "description": "Repository context for configuration completions; this MUST NOT prepare a checkout." - }, - "repositoryRevision": { - "type": "string", - "description": "Requested revision; requires a source and the capability's revision option." + "repositories": { + "type": "array", + "items": { + "$ref": "#/$defs/RepositorySource" + }, + "description": "Non-empty repository context, subject to\n{@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`.", + "minItems": 1 }, "config": { "type": "object", @@ -6844,20 +6865,20 @@ "items": { "$ref": "#/$defs/URI" }, - "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises a protected-primary capability. An\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary | immutable\nprimary} is fixed, while a\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement | replaceable\nprimary} is changed only with `session/workingDirectoryReplaced`.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch working-directory actions to change the set\nafter the session has started.\n\nA non-empty list and `repositorySource` are mutually exclusive.\nA repository URI identifies the source, not a working-directory URI; one\nsource may produce multiple directories." - }, - "repositorySource": { - "$ref": "#/$defs/URI", - "description": "Credential-free source to prepare; requires the agent's repositorySource capability." + "description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises a protected-primary capability. An\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary | immutable\nprimary} is fixed, while a\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement | replaceable\nprimary} is changed only with `session/workingDirectoryReplaced`.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch working-directory actions to change the set\nafter the session has started.\n\nA non-empty list and `repositories` are mutually exclusive." }, - "repositoryRevision": { - "type": "string", - "description": "Requested branch, tag, or commit; requires a source and the capability's revision option." + "repositories": { + "type": "array", + "items": { + "$ref": "#/$defs/RepositorySource" + }, + "description": "Non-empty repository list to prepare, supported only when the host\nadvertises {@link InitializeResult.repositoryPreparation}. Omit to retain\ndirectory/default creation. The resulting working directories MUST fit\nthe selected agent's existing directory capabilities.", + "minItems": 1 }, "config": { "type": "object", "additionalProperties": {}, - "description": "Session configuration values collected via `resolveSessionConfig`.\nKeys and values follow the advertised {@link SessionConfigSchema}." + "description": "Agent-specific configuration values collected via `resolveSessionConfig`.\nKeys and values correspond to the schema returned by the server." }, "activeClient": { "$ref": "#/$defs/SessionActiveClient", @@ -6874,7 +6895,7 @@ }, "DisposeSessionParams": { "type": "object", - "description": "Disposes a session and cleans up server-side resources.\n\nThe server broadcasts a `root/sessionRemoved` notification to all clients.\nDisposal MUST NOT erase a shared checkout or uncommitted user changes.\nRepository cleanup remains host-owned; ending a client's wait or subscription\ndoes not grant permission to delete repository data.", + "description": "Disposes a session and cleans up server-side resources.\n\nThe server broadcasts a `root/sessionRemoved` notification to all clients.", "properties": { "channel": { "$ref": "#/$defs/URI", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 4dace51c2..86838bdf7 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -109,13 +109,13 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, - "repositorySource": { - "$ref": "#/$defs/URI", - "description": "Immutable requested source, separate from the host-resolved working directories." - }, - "repositoryRevision": { - "type": "string", - "description": "Immutable requested revision, not the checkout's current HEAD." + "repositories": { + "type": "array", + "items": { + "$ref": "#/$defs/RepositorySource" + }, + "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.", + "minItems": 1 }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -165,7 +165,7 @@ }, "ProgressParams": { "type": "object", - "description": "Generic progress notification for a long-running operation.\n\nA client opts in to progress for a request by including a `progressToken` in\nthat request (today: the `progressToken` field on `createSession`). If the\nserver does long-running work to service the request — e.g. lazily\ndownloading an agent's native SDK the first time a session of that provider\nis materialized — it emits `progress` notifications carrying the same token.\n\nThe notification is operation-agnostic: it says nothing about *what* is\nprogressing. The client correlates `progressToken` back to the request it\noriginated from (and thus the UI surface awaiting it) and renders its own\nlocalized indicator. The same channel serves any future long-running\noperation without a new method.\n\nSemantics:\n\n- `progress` is monotonically non-decreasing for a given `progressToken`.\n- `total` is present only when the server knows the magnitude up front\n (e.g. a `Content-Length`); when absent the client SHOULD show an\n indeterminate indicator.\n- The operation is complete when `progress === total`. The server MUST emit a\n final frame satisfying `progress === total`; when the total was never\n known, it sets `total` to the final `progress` on that frame. No further\n frames reference the token afterwards.\n- The server MAY emit no progress at all (e.g. the work was already done);\n the client then never shows an indicator.\n- Like all notifications this is ephemeral and is **not** replayed on\n reconnect. A client that never receives the terminal frame SHOULD expire\n the indicator after an idle timeout.\n- Completion of reported work does not establish session readiness.\n Observe session lifecycle state for the durable outcome.", + "description": "Generic progress notification for a long-running operation.\n\nA client opts in to progress for a request by including a `progressToken` in\nthat request (today: the `progressToken` field on `createSession`). If the\nserver does long-running work to service the request — e.g. lazily\ndownloading an agent's native SDK the first time a session of that provider\nis materialized — it emits `progress` notifications carrying the same token.\n\nThe notification is operation-agnostic: it says nothing about *what* is\nprogressing. The client correlates `progressToken` back to the request it\noriginated from (and thus the UI surface awaiting it) and renders its own\nlocalized indicator. The same channel serves any future long-running\noperation without a new method.\n\nSemantics:\n\n- `progress` is monotonically non-decreasing for a given `progressToken`.\n- `total` is present only when the server knows the magnitude up front\n (e.g. a `Content-Length`); when absent the client SHOULD show an\n indeterminate indicator.\n- The operation is complete when `progress === total`. The server MUST emit a\n final frame satisfying `progress === total`; when the total was never\n known, it sets `total` to the final `progress` on that frame. No further\n frames reference the token afterwards.\n- The server MAY emit no progress at all (e.g. the work was already done);\n the client then never shows an indicator.\n- Like all notifications this is ephemeral and is **not** replayed on\n reconnect. A client that never receives the terminal frame SHOULD expire\n the indicator after an idle timeout.", "properties": { "channel": { "$ref": "#/$defs/URI", @@ -802,10 +802,6 @@ "type": "object", "description": "Static capabilities an {@link AgentInfo} advertises. Modelled after MCP\ncapabilities: each field is opt-in and its presence (an empty object `{}`)\nsignals support, while absence means the feature is unsupported and the\ncorresponding client commands MUST NOT be used. Sub-fields carry\nper-capability options.", "properties": { - "repositorySource": { - "$ref": "#/$defs/RepositorySourceCapability", - "description": "The host accepts typed repository inputs for session creation and configuration queries." - }, "multipleChats": { "$ref": "#/$defs/MultipleChatsCapability", "description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nsource-based creation; set {@link MultipleChatsCapability.fork} or\n{@link MultipleChatsCapability.sideChat} to allow the corresponding mode." @@ -816,16 +812,6 @@ } } }, - "RepositorySourceCapability": { - "type": "object", - "description": "Options for repository-backed session creation.", - "properties": { - "revision": { - "type": "boolean", - "description": "When true, clients may supply an explicit repositoryRevision." - } - } - }, "MultipleChatsCapability": { "type": "object", "description": "Options for the {@link AgentCapabilities.multipleChats} capability.", @@ -966,6 +952,23 @@ "run" ] }, + "RepositorySource": { + "type": "object", + "description": "Requested repository intent, independent of any host-resolved checkout.\nThe same source may appear more than once with different revisions; a source\nURI is not a checkout identity.", + "properties": { + "source": { + "$ref": "#/$defs/URI", + "description": "Credential-free repository source URI." + }, + "revision": { + "type": "string", + "description": "Requested branch, tag, or commit. Omit to use the host's default revision." + } + }, + "required": [ + "source" + ] + }, "SessionMetadata": { "type": "object", "description": "Metadata shared between the full {@link SessionState} (delivered when a\nclient subscribes to a session's URI) and the lightweight\n{@link SessionSummary} (carried in the root-channel session catalog).\n\nThese fields describe the session at a glance and appear in both places.\n`SessionState` owns the authoritative values for a subscribed session;\n`SessionSummary` mirrors them into the catalog so clients that only render a\nsession list don't have to subscribe to every session URI. The host keeps\nthe catalog in sync via `root/sessionSummaryChanged`.", @@ -1001,13 +1004,13 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, - "repositorySource": { - "$ref": "#/$defs/URI", - "description": "Immutable requested source, separate from the host-resolved working directories." - }, - "repositoryRevision": { - "type": "string", - "description": "Immutable requested revision, not the checkout's current HEAD." + "repositories": { + "type": "array", + "items": { + "$ref": "#/$defs/RepositorySource" + }, + "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.", + "minItems": 1 }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -1055,13 +1058,13 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, - "repositorySource": { - "$ref": "#/$defs/URI", - "description": "Immutable requested source, separate from the host-resolved working directories." - }, - "repositoryRevision": { - "type": "string", - "description": "Immutable requested revision, not the checkout's current HEAD." + "repositories": { + "type": "array", + "items": { + "$ref": "#/$defs/RepositorySource" + }, + "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.", + "minItems": 1 }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -1102,7 +1105,7 @@ }, "config": { "$ref": "#/$defs/SessionConfigState", - "description": "Provider-specific session configuration schema and current values." + "description": "Session configuration schema and current values" }, "customizations": { "type": "array", @@ -1371,13 +1374,13 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, - "repositorySource": { - "$ref": "#/$defs/URI", - "description": "Immutable requested source, separate from the host-resolved working directories." - }, - "repositoryRevision": { - "type": "string", - "description": "Immutable requested revision, not the checkout's current HEAD." + "repositories": { + "type": "array", + "items": { + "$ref": "#/$defs/RepositorySource" + }, + "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.", + "minItems": 1 }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", diff --git a/schema/state.schema.json b/schema/state.schema.json index 9cd3e62b1..cff008d2b 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -527,10 +527,6 @@ "type": "object", "description": "Static capabilities an {@link AgentInfo} advertises. Modelled after MCP\ncapabilities: each field is opt-in and its presence (an empty object `{}`)\nsignals support, while absence means the feature is unsupported and the\ncorresponding client commands MUST NOT be used. Sub-fields carry\nper-capability options.", "properties": { - "repositorySource": { - "$ref": "#/$defs/RepositorySourceCapability", - "description": "The host accepts typed repository inputs for session creation and configuration queries." - }, "multipleChats": { "$ref": "#/$defs/MultipleChatsCapability", "description": "The agent can host more than one concurrent chat per session. When absent,\nclients MUST NOT call `createChat` to open chats beyond the default one the\nsession starts with. An empty object `{}` advertises multi-chat without\nsource-based creation; set {@link MultipleChatsCapability.fork} or\n{@link MultipleChatsCapability.sideChat} to allow the corresponding mode." @@ -541,16 +537,6 @@ } } }, - "RepositorySourceCapability": { - "type": "object", - "description": "Options for repository-backed session creation.", - "properties": { - "revision": { - "type": "boolean", - "description": "When true, clients may supply an explicit repositoryRevision." - } - } - }, "MultipleChatsCapability": { "type": "object", "description": "Options for the {@link AgentCapabilities.multipleChats} capability.", @@ -691,6 +677,23 @@ "run" ] }, + "RepositorySource": { + "type": "object", + "description": "Requested repository intent, independent of any host-resolved checkout.\nThe same source may appear more than once with different revisions; a source\nURI is not a checkout identity.", + "properties": { + "source": { + "$ref": "#/$defs/URI", + "description": "Credential-free repository source URI." + }, + "revision": { + "type": "string", + "description": "Requested branch, tag, or commit. Omit to use the host's default revision." + } + }, + "required": [ + "source" + ] + }, "SessionMetadata": { "type": "object", "description": "Metadata shared between the full {@link SessionState} (delivered when a\nclient subscribes to a session's URI) and the lightweight\n{@link SessionSummary} (carried in the root-channel session catalog).\n\nThese fields describe the session at a glance and appear in both places.\n`SessionState` owns the authoritative values for a subscribed session;\n`SessionSummary` mirrors them into the catalog so clients that only render a\nsession list don't have to subscribe to every session URI. The host keeps\nthe catalog in sync via `root/sessionSummaryChanged`.", @@ -726,13 +729,13 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, - "repositorySource": { - "$ref": "#/$defs/URI", - "description": "Immutable requested source, separate from the host-resolved working directories." - }, - "repositoryRevision": { - "type": "string", - "description": "Immutable requested revision, not the checkout's current HEAD." + "repositories": { + "type": "array", + "items": { + "$ref": "#/$defs/RepositorySource" + }, + "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.", + "minItems": 1 }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -780,13 +783,13 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, - "repositorySource": { - "$ref": "#/$defs/URI", - "description": "Immutable requested source, separate from the host-resolved working directories." - }, - "repositoryRevision": { - "type": "string", - "description": "Immutable requested revision, not the checkout's current HEAD." + "repositories": { + "type": "array", + "items": { + "$ref": "#/$defs/RepositorySource" + }, + "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.", + "minItems": 1 }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", @@ -827,7 +830,7 @@ }, "config": { "$ref": "#/$defs/SessionConfigState", - "description": "Provider-specific session configuration schema and current values." + "description": "Session configuration schema and current values" }, "customizations": { "type": "array", @@ -1096,13 +1099,13 @@ }, "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set." }, - "repositorySource": { - "$ref": "#/$defs/URI", - "description": "Immutable requested source, separate from the host-resolved working directories." - }, - "repositoryRevision": { - "type": "string", - "description": "Immutable requested revision, not the checkout's current HEAD." + "repositories": { + "type": "array", + "items": { + "$ref": "#/$defs/RepositorySource" + }, + "description": "Immutable repository intent accepted at creation. When present, this list\nis non-empty and retained exactly, including order and omitted revisions,\nfrom `creating` through `ready` or `failed` and in session summaries.\nEntries have no one-to-one or positional mapping to `workingDirectories`.", + "minItems": 1 }, "annotations": { "$ref": "#/$defs/AnnotationsSummary", diff --git a/scripts/generate-csharp.ts b/scripts/generate-csharp.ts index f10ad38a1..ea2ad96cf 100644 --- a/scripts/generate-csharp.ts +++ b/scripts/generate-csharp.ts @@ -659,7 +659,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; csName?: strin { name: 'AgentCapabilities' }, { name: 'MultipleChatsCapability' }, { name: 'MultipleWorkingDirectoriesCapability' }, - { name: 'RepositorySourceCapability' }, + { name: 'RepositorySource' }, { name: 'SessionModelInfo' }, { name: 'ModelSelection' }, { name: 'AgentSelection' }, @@ -2082,6 +2082,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; csName?: str // record or those fields reference a non-existent type (CS0246). { name: 'Implementation' }, { name: 'ClientCapabilities' }, + { name: 'RepositoryPreparationCapabilities' }, { name: 'AutomationCapabilities' }, { name: 'AutomationCreateCapability' }, { name: 'AutomationScheduleCapabilities' }, diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 41202ffd8..638e3cba3 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -739,7 +739,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin { name: 'AgentCapabilities' }, { name: 'MultipleChatsCapability' }, { name: 'MultipleWorkingDirectoriesCapability' }, - { name: 'RepositorySourceCapability' }, + { name: 'RepositorySource' }, { name: 'SessionModelInfo' }, { name: 'ModelSelection' }, { name: 'AgentSelection' }, @@ -1717,6 +1717,7 @@ const COMMAND_ENUMS = ['ReconnectResultType', 'ChatSourceKind', 'ContentEncoding const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: string }[] = [ { name: 'InitializeParams' }, { name: 'InitializeResult' }, + { name: 'RepositoryPreparationCapabilities' }, { name: 'ClientCapabilities' }, { name: 'AutomationCapabilities' }, { name: 'AutomationCreateCapability' }, { name: 'AutomationScheduleCapabilities' }, diff --git a/scripts/generate-json-schema.test.ts b/scripts/generate-json-schema.test.ts index c104f4c83..bfd980124 100644 --- a/scripts/generate-json-schema.test.ts +++ b/scripts/generate-json-schema.test.ts @@ -142,7 +142,9 @@ function schemaAccepts( case 'null': return value === null; case 'array': - return Array.isArray(value); + return Array.isArray(value) + && (typeof schema.minItems !== 'number' || value.length >= schema.minItems) + && value.every(item => schemaAccepts(root, schema.items as JsonNode, item)); } return true; @@ -224,7 +226,7 @@ describe('generated JSON schemas', () => { assert.equal(schemaAccepts(schema, configSchema, legacy), true); }); - it('declares optional typed repository inputs beside generic config', () => { + it('declares optional non-empty repository lists beside generic config', () => { if (file !== 'commands.schema.json') { return; } @@ -237,40 +239,81 @@ describe('generated JSON schemas', () => { const properties = defs[definition].properties as Record>; assert.equal(properties.config.type, 'object'); assert.deepEqual( - Object.keys(properties).filter(name => ['repository', 'repositorySource', 'repositoryRevision'].includes(name)), - ['repositorySource', 'repositoryRevision'], + Object.keys(properties).filter(name => name.startsWith('repositor')), + ['repositories'], ); + assert.equal(properties.repositories.minItems, 1); const base = { channel, ...(definition === 'SessionConfigCompletionsParams' ? { property: 'mode' } : {}) }; assert.equal(schemaAccepts(schema, defs[definition], base), true); - for (const source of [ - {}, - { repositorySource: 'https://example.org/team/project.git' }, - { - repositorySource: 'https://example.org/team/project.git', - repositoryRevision: 'refs/tags/v1.2.3', - }, + const source = 'https://example.org/team/project.git'; + for (const repositories of [ + [{ source }], + [{ source, revision: 'refs/tags/v1.2.3' }], + [{ source, revision: 'main' }, { source, revision: 'feature' }], ]) { - assert.equal(schemaAccepts(schema, defs[definition], { ...base, ...source, config: { mode: 'review' } }), true); + assert.equal(schemaAccepts(schema, defs[definition], { ...base, repositories, config: { mode: 'review' } }), true); } - for (const invalid of [{ repositorySource: 42 }, { repositorySource: null }, { repositoryRevision: 42 }]) { - assert.equal(schemaAccepts(schema, defs[definition], { ...base, ...invalid }), false); + for (const repositories of [ + [], null, {}, source, [null], [source], [{}], + [{ revision: 'main' }], [{ source: 42 }], [{ source: null }], + [{ source, revision: 42 }], + ]) { + assert.equal(schemaAccepts(schema, defs[definition], { ...base, repositories }), false); } } }); - it('declares immutable source metadata and an opt-in repository capability', () => { + it('declares immutable repository lists with a required source per entry', () => { const defs = schema.$defs as Record>; - for (const name of ['SessionState', 'SessionSummary']) { + for (const name of ['SessionMetadata', 'SessionState', 'SessionSummary']) { const properties = defs[name].properties as Record>; - assert.equal(dereferenceSchema(schema, properties.repositorySource).type, 'string'); - assert.equal(dereferenceSchema(schema, properties.repositoryRevision).type, 'string'); + const { type, items, minItems } = properties.repositories; + assert.deepEqual({ type, items, minItems }, { + type: 'array', items: { $ref: '#/$defs/RepositorySource' }, minItems: 1, + }); + assert.equal((defs[name].required as string[]).includes('repositories'), false); } + const source = defs.RepositorySource; + const properties = source.properties as Record>; + assert.deepEqual({ + fields: Object.keys(properties), + required: source.required, + sourceType: dereferenceSchema(schema, properties.source).type, + revisionType: properties.revision.type, + }, { + fields: ['source', 'revision'], required: ['source'], sourceType: 'string', revisionType: 'string', + }); + }); + + it('advertises repository preparation on the host, not the agent', () => { + const defs = schema.$defs as Record>; const capabilities = defs.AgentCapabilities.properties as Record>; - assert.deepEqual(capabilities.repositorySource.$ref, '#/$defs/RepositorySourceCapability'); - for (const value of [{}, { repositorySource: {} }, { repositorySource: { revision: true } }]) { - assert.equal(schemaAccepts(schema, defs.AgentCapabilities, value), true); + assert.deepEqual({ + agentSource: capabilities.repositorySource, + agentPreparation: capabilities.repositoryPreparation, + oldCapability: defs.RepositorySourceCapability, + }, { + agentSource: undefined, agentPreparation: undefined, oldCapability: undefined, + }); + if (file !== 'commands.schema.json') { + return; + } + const properties = defs.InitializeResult.properties as Record>; + assert.equal(properties.repositoryPreparation.$ref, '#/$defs/RepositoryPreparationCapabilities'); + const hostCapabilities = defs.RepositoryPreparationCapabilities; + assert.equal(hostCapabilities.required, undefined); + const base = { protocolVersion: '0.9.0', serverSeq: 0, snapshots: [] }; + assert.equal(schemaAccepts(schema, defs.InitializeResult, base), true); + for (const repositoryPreparation of [ + {}, { revision: true }, { revision: false }, + { multipleRepositories: false }, { multipleRepositories: true }, + { revision: true, multipleRepositories: true }, + ]) { + assert.equal(schemaAccepts(schema, defs.InitializeResult, { ...base, repositoryPreparation }), true); + } + for (const repositoryPreparation of [true, null, [], { revision: 'true' }, { multipleRepositories: 2 }]) { + assert.equal(schemaAccepts(schema, defs.InitializeResult, { ...base, repositoryPreparation }), false); } - assert.equal(schemaAccepts(schema, defs.AgentCapabilities, { repositorySource: true }), false); }); it('constrains every ChatOrigin branch to a distinct kind', () => { diff --git a/scripts/generate-json-schema.ts b/scripts/generate-json-schema.ts index 0ac0fb4ff..bfcbab796 100644 --- a/scripts/generate-json-schema.ts +++ b/scripts/generate-json-schema.ts @@ -33,6 +33,7 @@ interface JsonSchema { enum?: Array; const?: string | number | boolean; minimum?: number; + minItems?: number; oneOf?: JsonSchema[]; allOf?: JsonSchema[]; anyOf?: JsonSchema[]; @@ -376,6 +377,15 @@ function interfaceToSchema(iface: InterfaceDeclaration, project: Project): JsonS } propSchema.minimum = minimum; } + const minItems = getNumericPropertyTag(prop, 'minItems'); + if (minItems !== undefined) { + if (propSchema.type !== 'array' || !Number.isInteger(minItems) || minItems < 0) { + throw new Error( + `${prop.getSourceFile().getFilePath()}: ${name} uses invalid @minItems on ${typeText}`, + ); + } + propSchema.minItems = minItems; + } schema.properties![name] = propSchema; if (!prop.hasQuestionToken() && !typeAdmitsUndefined(typeText)) { if (!schema.required!.includes(name)) { diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 0837bcfec..b35ef7773 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -987,7 +987,7 @@ const STATE_STRUCTS = [ 'AgentCapabilities', 'MultipleChatsCapability', 'MultipleWorkingDirectoriesCapability', - 'RepositorySourceCapability', + 'RepositorySource', 'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema', 'PendingMessage', 'ChatState', 'ChatSummary', 'SideChatSelection', 'SessionState', 'SessionActiveClient', 'SessionChatInputRequest', 'SessionToolConfirmationRequest', 'SessionToolClientExecutionRequest', @@ -1722,6 +1722,7 @@ const COMMAND_ENUMS = ['ReconnectResultType', 'ChatSourceKind', 'ContentEncoding const COMMAND_STRUCTS = [ 'InitializeParams', 'InitializeResult', + 'RepositoryPreparationCapabilities', 'ClientCapabilities', 'AutomationCapabilities', 'AutomationCreateCapability', 'AutomationScheduleCapabilities', diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 2f663eede..3518ad451 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -800,7 +800,7 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str { name: 'AgentCapabilities' }, { name: 'MultipleChatsCapability' }, { name: 'MultipleWorkingDirectoriesCapability' }, - { name: 'RepositorySourceCapability' }, + { name: 'RepositorySource' }, { name: 'SessionModelInfo' }, { name: 'ModelSelection' }, { name: 'AgentSelection' }, @@ -1700,6 +1700,7 @@ const COMMAND_ENUMS = ['ReconnectResultType', 'ChatSourceKind', 'ContentEncoding const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: string }[] = [ { name: 'InitializeParams' }, { name: 'InitializeResult' }, + { name: 'RepositoryPreparationCapabilities' }, { name: 'ClientCapabilities' }, { name: 'AutomationCapabilities' }, { name: 'AutomationCreateCapability' }, { name: 'AutomationScheduleCapabilities' }, @@ -1765,7 +1766,7 @@ function generateCommandsFile(project: Project): string { lines.push('#[allow(unused_imports)]'); lines.push('use crate::actions::{ActionEnvelope, StateAction};'); lines.push('#[allow(unused_imports)]'); - lines.push('use crate::state::{AgentSelection, AutomationDefinition, AutomationSchedule, AutomationSessionTemplate, AutomationTrigger, AutomationTriggerDefinition, ContentRef, Message, MessageAttachment, ModelSelection, SessionActiveClient, SessionConfigSchema, SessionSummary, SideChatSelection, Snapshot, SnapshotState, TelemetryCapabilities, TerminalClaim, TextRange, Turn};'); + lines.push('use crate::state::{AgentSelection, AutomationDefinition, AutomationSchedule, AutomationSessionTemplate, AutomationTrigger, AutomationTriggerDefinition, ContentRef, Message, MessageAttachment, ModelSelection, RepositorySource, SessionActiveClient, SessionConfigSchema, SessionSummary, SideChatSelection, Snapshot, SnapshotState, TelemetryCapabilities, TerminalClaim, TextRange, Turn};'); lines.push(''); lines.push('// ─── Enums ────────────────────────────────────────────────────────────\n'); @@ -1920,7 +1921,7 @@ const NOTIFICATION_STRUCTS = [ function generateNotificationsFile(project: Project): string { const lines: string[] = [GENERATED_HEADER]; lines.push('#[allow(unused_imports)]'); - lines.push('use crate::state::{AgentSelection, AnnotationsSummary, ChangesSummary, Changeset, FileEdit, ModelSelection, ProjectInfo, ProtectedResourceMetadata, SessionChatSummary, SessionOrigin, SessionStatus, SessionSummary};'); + lines.push('use crate::state::{AgentSelection, AnnotationsSummary, ChangesSummary, Changeset, FileEdit, ModelSelection, ProjectInfo, ProtectedResourceMetadata, RepositorySource, SessionChatSummary, SessionOrigin, SessionStatus, SessionSummary};'); lines.push(''); lines.push('// ─── Enums ────────────────────────────────────────────────────────────\n'); diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 6335aff9d..a12fca80d 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -692,7 +692,7 @@ const STATE_STRUCTS = [ 'AgentCapabilities', 'MultipleChatsCapability', 'MultipleWorkingDirectoriesCapability', - 'RepositorySourceCapability', + 'RepositorySource', 'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema', 'PendingMessage', 'ChatState', 'ChatSummary', 'SideChatSelection', 'SessionState', 'SessionActiveClient', 'SessionChatInputRequest', 'SessionToolConfirmationRequest', 'SessionToolClientExecutionRequest', @@ -1629,6 +1629,7 @@ const COMMAND_ENUMS = ['ReconnectResultType', 'ChatSourceKind', 'ContentEncoding const COMMAND_STRUCTS = [ 'InitializeParams', 'InitializeResult', 'ClientCapabilities', 'AutomationCapabilities', + 'RepositoryPreparationCapabilities', 'AutomationCreateCapability', 'AutomationScheduleCapabilities', 'AutomationRunCancellationCapability', diff --git a/types/channels-root/commands.ts b/types/channels-root/commands.ts index 00635e1ca..e7cfc8fbf 100644 --- a/types/channels-root/commands.ts +++ b/types/channels-root/commands.ts @@ -8,7 +8,7 @@ import type { URI } from '../common/state.js'; import type { BaseParams, PaginatedParams, PaginatedResult } from '../common/commands.js'; -import type { SessionSummary, SessionConfigSchema } from '../channels-session/state.js'; +import type { RepositorySource, SessionSummary, SessionConfigSchema } from '../channels-session/state.js'; // Re-export schema types so the legacy `commands.ts` aggregator continues to // expose them from the same import path. @@ -79,8 +79,8 @@ export interface ListSessionsResult extends PaginatedResult { * the full current property set (not a delta). The returned `values` contain * server-resolved defaults to pass to `createSession`. * - * This command MUST NOT clone or prepare a repository. Repository context - * requires the agent's `repositorySource` capability. + * `resolveSessionConfig` and `sessionConfigCompletions` MUST NOT clone or + * prepare repositories: editing a draft should not create checkouts. * * @category Commands * @method resolveSessionConfig @@ -133,11 +133,14 @@ export interface ResolveSessionConfigParams extends BaseParams { provider?: string; /** Working directory for the session */ workingDirectory?: URI; - /** Credential-free source context; not a working-directory URI. */ - repositorySource?: URI; - /** Requested revision; requires a source and the capability's revision option. */ - repositoryRevision?: string; - /** Current user-filled configuration values; see {@link SessionConfigSchema}. */ + /** + * Non-empty repository context, subject to + * {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`. + * + * @minItems 1 + */ + repositories?: RepositorySource[]; + /** Current user-filled configuration values */ config?: Record; } @@ -202,10 +205,13 @@ export interface SessionConfigCompletionsParams extends BaseParams { provider?: string; /** Working directory for the session */ workingDirectory?: URI; - /** Repository context for configuration completions; this MUST NOT prepare a checkout. */ - repositorySource?: URI; - /** Requested revision; requires a source and the capability's revision option. */ - repositoryRevision?: string; + /** + * Non-empty repository context, subject to + * {@link InitializeResult.repositoryPreparation}. May accompany `workingDirectory`. + * + * @minItems 1 + */ + repositories?: RepositorySource[]; /** Current user-filled configuration values (provides context for the query) */ config?: Record; /** Property id from the schema to query values for */ diff --git a/types/channels-root/notifications.ts b/types/channels-root/notifications.ts index 8ae4aebd0..eb74fedc5 100644 --- a/types/channels-root/notifications.ts +++ b/types/channels-root/notifications.ts @@ -175,8 +175,6 @@ export interface SessionSummaryChangedParams { * - Like all notifications this is ephemeral and is **not** replayed on * reconnect. A client that never receives the terminal frame SHOULD expire * the indicator after an idle timeout. - * - Completion of reported work does not establish session readiness. - * Observe session lifecycle state for the durable outcome. * * @category Protocol Notifications * @method root/progress diff --git a/types/channels-root/state.ts b/types/channels-root/state.ts index 4c2700251..b6c123810 100644 --- a/types/channels-root/state.ts +++ b/types/channels-root/state.ts @@ -106,8 +106,6 @@ export interface AgentInfo { * @category Root State */ export interface AgentCapabilities { - /** The host accepts typed repository inputs for session creation and configuration queries. */ - repositorySource?: RepositorySourceCapability; /** * The agent can host more than one concurrent chat per session. When absent, * clients MUST NOT call `createChat` to open chats beyond the default one the @@ -129,15 +127,6 @@ export interface AgentCapabilities { multipleWorkingDirectories?: MultipleWorkingDirectoriesCapability; } -/** - * Options for repository-backed session creation. - * @category Root State - */ -export interface RepositorySourceCapability { - /** When true, clients may supply an explicit repositoryRevision. */ - revision?: boolean; -} - /** * Options for the {@link AgentCapabilities.multipleChats} capability. * diff --git a/types/channels-session/commands.ts b/types/channels-session/commands.ts index b58e53549..0c0425e23 100644 --- a/types/channels-session/commands.ts +++ b/types/channels-session/commands.ts @@ -8,6 +8,7 @@ import type { URI } from '../common/state.js'; import type { BaseParams } from '../common/commands.js'; import type { + RepositorySource, SessionActiveClient, } from './state.js'; import type { @@ -70,18 +71,21 @@ export interface CreateSessionParams extends BaseParams { * and ignores the rest. Dispatch working-directory actions to change the set * after the session has started. * - * A non-empty list and `repositorySource` are mutually exclusive. - * A repository URI identifies the source, not a working-directory URI; one - * source may produce multiple directories. + * A non-empty list and `repositories` are mutually exclusive. */ workingDirectories?: URI[]; - /** Credential-free source to prepare; requires the agent's repositorySource capability. */ - repositorySource?: URI; - /** Requested branch, tag, or commit; requires a source and the capability's revision option. */ - repositoryRevision?: string; /** - * Session configuration values collected via `resolveSessionConfig`. - * Keys and values follow the advertised {@link SessionConfigSchema}. + * Non-empty repository list to prepare, supported only when the host + * advertises {@link InitializeResult.repositoryPreparation}. Omit to retain + * directory/default creation. The resulting working directories MUST fit + * the selected agent's existing directory capabilities. + * + * @minItems 1 + */ + repositories?: RepositorySource[]; + /** + * Agent-specific configuration values collected via `resolveSessionConfig`. + * Keys and values correspond to the schema returned by the server. */ config?: Record; /** @@ -114,9 +118,6 @@ export interface CreateSessionParams extends BaseParams { * Disposes a session and cleans up server-side resources. * * The server broadcasts a `root/sessionRemoved` notification to all clients. - * Disposal MUST NOT erase a shared checkout or uncommitted user changes. - * Repository cleanup remains host-owned; ending a client's wait or subscription - * does not grant permission to delete repository data. * * @category Commands * @method disposeSession diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index 5bcc00931..de173769c 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -102,6 +102,20 @@ export interface AutomationSessionOrigin { */ export type SessionOrigin = AutomationSessionOrigin; +/** + * Requested repository intent, independent of any host-resolved checkout. + * The same source may appear more than once with different revisions; a source + * URI is not a checkout identity. + * + * @category Session State + */ +export interface RepositorySource { + /** Credential-free repository source URI. */ + source: URI; + /** Requested branch, tag, or commit. Omit to use the host's default revision. */ + revision?: string; +} + /** * Metadata shared between the full {@link SessionState} (delivered when a * client subscribes to a session's URI) and the lightweight @@ -141,10 +155,15 @@ export interface SessionMetadata { * chat that sets none operates against this full set. */ workingDirectories?: URI[]; - /** Immutable requested source, separate from the host-resolved working directories. */ - repositorySource?: URI; - /** Immutable requested revision, not the checkout's current HEAD. */ - repositoryRevision?: string; + /** + * Immutable repository intent accepted at creation. When present, this list + * is non-empty and retained exactly, including order and omitted revisions, + * from `creating` through `ready` or `failed` and in session summaries. + * Entries have no one-to-one or positional mapping to `workingDirectories`. + * + * @minItems 1 + */ + repositories?: RepositorySource[]; /** * Lightweight summary of this session's inline annotations channel * (`ahp-session://annotations`). Surfaced so badge UI can render @@ -193,7 +212,7 @@ export interface SessionState extends SessionMetadata { * this over the session's lifetime. */ defaultChat?: URI; - /** Provider-specific session configuration schema and current values. */ + /** Session configuration schema and current values */ config?: SessionConfigState; /** * Top-level customizations active in this session. diff --git a/types/common/commands.ts b/types/common/commands.ts index d2bff1e86..40fd1efd7 100644 --- a/types/common/commands.ts +++ b/types/common/commands.ts @@ -261,6 +261,12 @@ export interface InitializeResult { snapshots: Snapshot[]; /** Suggested default directory for remote filesystem browsing */ defaultDirectory?: URI; + /** + * Host-owned repository preparation for session creation and repository + * context in configuration queries. Absence means unsupported; an empty + * object supports one repository at its default revision. + */ + repositoryPreparation?: RepositoryPreparationCapabilities; /** * Characters that, when typed in a {@link Message} input, SHOULD cause * the client to issue a `completions` request with @@ -295,6 +301,24 @@ export interface InitializeResult { automations?: AutomationCapabilities; } +/** + * Repository preparation supported by this host, independent of the selected + * agent. Resulting working directories must still fit that agent's existing + * directory capabilities. + * + * @category Commands + */ +export interface RepositoryPreparationCapabilities { + /** When true, clients may supply {@link RepositorySource.revision}. */ + revision?: boolean; + /** + * When true, clients may supply more than one repository. When absent or + * false, the host MUST reject lists with more than one entry with + * `InvalidParams` before preparation. + */ + multipleRepositories?: boolean; +} + /** * Automation features supported by this host authority. * diff --git a/types/test-cases/reducers/272-repositories-survive-ready-config-and-directories.json b/types/test-cases/reducers/272-repositories-survive-ready-config-and-directories.json new file mode 100644 index 000000000..5600c247e --- /dev/null +++ b/types/test-cases/reducers/272-repositories-survive-ready-config-and-directories.json @@ -0,0 +1,46 @@ +{ + "description": "Repository intent survives readiness, config changes, and directory changes without deduplication or revision resolution", + "reducer": "session", + "initial": { + "provider": "example", + "title": "Repository session", + "status": 1, + "lifecycle": "creating", + "activeClients": [], + "chats": [], + "repositories": [ + { "source": "https://example.org/team/project.git" }, + { "source": "https://example.org/team/project.git", "revision": "feature" }, + { "source": "https://example.org/team/project.git", "revision": "main" } + ], + "config": { + "schema": { "type": "object", "properties": { "mode": { "type": "string", "title": "Mode" } } }, + "values": { "mode": "review" } + } + }, + "actions": [ + { "type": "session/workingDirectorySet", "directory": "file:///work/project" }, + { "type": "session/ready" }, + { "type": "session/configChanged", "config": { "mode": "interactive" }, "replace": true }, + { "type": "session/workingDirectorySet", "directory": "file:///work/project-worktree" }, + { "type": "session/workingDirectoryRemoved", "directory": "file:///work/project" } + ], + "expected": { + "provider": "example", + "title": "Repository session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "repositories": [ + { "source": "https://example.org/team/project.git" }, + { "source": "https://example.org/team/project.git", "revision": "feature" }, + { "source": "https://example.org/team/project.git", "revision": "main" } + ], + "workingDirectories": ["file:///work/project-worktree"], + "config": { + "schema": { "type": "object", "properties": { "mode": { "type": "string", "title": "Mode" } } }, + "values": { "mode": "interactive" } + } + } +} diff --git a/types/test-cases/reducers/273-repositories-survive-creation-failure.json b/types/test-cases/reducers/273-repositories-survive-creation-failure.json new file mode 100644 index 000000000..654baee4d --- /dev/null +++ b/types/test-cases/reducers/273-repositories-survive-creation-failure.json @@ -0,0 +1,34 @@ +{ + "description": "Failed preparation retains the exact repository list without claiming any checkout was prepared", + "reducer": "session", + "initial": { + "provider": "example", + "title": "Repository session", + "status": 1, + "lifecycle": "creating", + "activeClients": [], + "chats": [], + "repositories": [ + { "source": "https://example.org/team/project.git", "revision": "main" }, + { "source": "https://example.org/team/project.git", "revision": "feature" }, + { "source": "https://example.org/team/other.git" } + ] + }, + "actions": [ + { "type": "session/creationFailed", "error": { "errorType": "preparationFailed", "message": "Preparation failed" } } + ], + "expected": { + "provider": "example", + "title": "Repository session", + "status": 1, + "lifecycle": "failed", + "creationError": { "errorType": "preparationFailed", "message": "Preparation failed" }, + "activeClients": [], + "chats": [], + "repositories": [ + { "source": "https://example.org/team/project.git", "revision": "main" }, + { "source": "https://example.org/team/project.git", "revision": "feature" }, + { "source": "https://example.org/team/other.git" } + ] + } +} diff --git a/types/test-cases/round-trips/046-repository-session-source-only.json b/types/test-cases/round-trips/046-repository-session-source-only.json index 591ba53a6..5cf298750 100644 --- a/types/test-cases/round-trips/046-repository-session-source-only.json +++ b/types/test-cases/round-trips/046-repository-session-source-only.json @@ -12,7 +12,7 @@ "lifecycle": "ready", "activeClients": [], "chats": [], - "repositorySource": "https://example.org/team/project.git", + "repositories": [{ "source": "https://example.org/team/project.git" }], "workingDirectories": ["file:///work/project", "file:///work/project-worktree"] }, "fromSeq": 2 @@ -26,7 +26,7 @@ "lifecycle": "ready", "activeClients": [], "chats": [], - "repositorySource": "https://example.org/team/project.git", + "repositories": [{ "source": "https://example.org/team/project.git" }], "workingDirectories": ["file:///work/project", "file:///work/project-worktree"] }, "fromSeq": 2 diff --git a/types/test-cases/round-trips/047-repository-session-revision.json b/types/test-cases/round-trips/047-repository-session-revision.json index 46570667c..d2f269068 100644 --- a/types/test-cases/round-trips/047-repository-session-revision.json +++ b/types/test-cases/round-trips/047-repository-session-revision.json @@ -12,8 +12,7 @@ "lifecycle": "creating", "activeClients": [], "chats": [], - "repositorySource": "https://example.org/team/project.git", - "repositoryRevision": "refs/tags/v1.2.3" + "repositories": [{ "source": "https://example.org/team/project.git", "revision": "refs/tags/v1.2.3" }] }, "fromSeq": 0 }, @@ -26,8 +25,7 @@ "lifecycle": "creating", "activeClients": [], "chats": [], - "repositorySource": "https://example.org/team/project.git", - "repositoryRevision": "refs/tags/v1.2.3" + "repositories": [{ "source": "https://example.org/team/project.git", "revision": "refs/tags/v1.2.3" }] }, "fromSeq": 0 }] diff --git a/types/test-cases/round-trips/048-repository-session-failed.json b/types/test-cases/round-trips/048-repository-session-failed.json index 36487a383..b2282eeaf 100644 --- a/types/test-cases/round-trips/048-repository-session-failed.json +++ b/types/test-cases/round-trips/048-repository-session-failed.json @@ -13,8 +13,7 @@ "creationError": { "errorType": "preparationFailed", "message": "Preparation failed" }, "activeClients": [], "chats": [], - "repositorySource": "https://example.org/team/project.git", - "repositoryRevision": "main" + "repositories": [{ "source": "https://example.org/team/project.git", "revision": "main" }] }, "fromSeq": 1 }, @@ -28,8 +27,7 @@ "creationError": { "errorType": "preparationFailed", "message": "Preparation failed" }, "activeClients": [], "chats": [], - "repositorySource": "https://example.org/team/project.git", - "repositoryRevision": "main" + "repositories": [{ "source": "https://example.org/team/project.git", "revision": "main" }] }, "fromSeq": 1 }] diff --git a/types/test-cases/round-trips/049-repository-preparation-capability.json b/types/test-cases/round-trips/049-repository-preparation-capability.json new file mode 100644 index 000000000..235182054 --- /dev/null +++ b/types/test-cases/round-trips/049-repository-preparation-capability.json @@ -0,0 +1,18 @@ +{ + "name": "repository-preparation-capability", + "group": "A", + "description": "An empty host capability supports one default-revision repository without an agent capability.", + "type": "InitializeResult", + "input": { + "protocolVersion": "0.9.0", + "serverSeq": 0, + "snapshots": [], + "repositoryPreparation": {} + }, + "acceptableOutputs": [{ + "protocolVersion": "0.9.0", + "serverSeq": 0, + "snapshots": [], + "repositoryPreparation": {} + }] +} diff --git a/types/test-cases/round-trips/049-repository-source-capability.json b/types/test-cases/round-trips/049-repository-source-capability.json deleted file mode 100644 index 89ecefbf0..000000000 --- a/types/test-cases/round-trips/049-repository-source-capability.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "name": "repository-source-capability", - "group": "A", - "description": "Agents advertise source-only or source-with-revision preparation independently of config schemas.", - "type": "InitializeResult", - "input": { - "protocolVersion": "0.9.0", - "serverSeq": 0, - "snapshots": [{ - "resource": "ahp-root://", - "state": { - "agents": [ - { - "provider": "source-only", - "displayName": "Source only", - "description": "Repository preparation", - "models": [], - "capabilities": { "repositorySource": {} } - }, - { - "provider": "source-with-revision", - "displayName": "Source with revision", - "description": "Repository preparation", - "models": [], - "capabilities": { "repositorySource": { "revision": true } } - } - ] - }, - "fromSeq": 0 - }] - }, - "acceptableOutputs": [{ - "protocolVersion": "0.9.0", - "serverSeq": 0, - "snapshots": [{ - "resource": "ahp-root://", - "state": { - "agents": [ - { - "provider": "source-only", - "displayName": "Source only", - "description": "Repository preparation", - "models": [], - "capabilities": { "repositorySource": {} } - }, - { - "provider": "source-with-revision", - "displayName": "Source with revision", - "description": "Repository preparation", - "models": [], - "capabilities": { "repositorySource": { "revision": true } } - } - ] - }, - "fromSeq": 0 - }] - }] -} diff --git a/types/test-cases/round-trips/050-repository-preparation-revisions.json b/types/test-cases/round-trips/050-repository-preparation-revisions.json new file mode 100644 index 000000000..a17335aad --- /dev/null +++ b/types/test-cases/round-trips/050-repository-preparation-revisions.json @@ -0,0 +1,18 @@ +{ + "name": "repository-preparation-revisions", + "group": "A", + "description": "Host revision support does not imply multiple-repository support.", + "type": "InitializeResult", + "input": { + "protocolVersion": "0.9.0", + "serverSeq": 0, + "snapshots": [], + "repositoryPreparation": { "revision": true } + }, + "acceptableOutputs": [{ + "protocolVersion": "0.9.0", + "serverSeq": 0, + "snapshots": [], + "repositoryPreparation": { "revision": true } + }] +} diff --git a/types/test-cases/round-trips/051-repository-preparation-single.json b/types/test-cases/round-trips/051-repository-preparation-single.json new file mode 100644 index 000000000..89b785dbf --- /dev/null +++ b/types/test-cases/round-trips/051-repository-preparation-single.json @@ -0,0 +1,18 @@ +{ + "name": "repository-preparation-single", + "group": "A", + "description": "Explicit false capability options round-trip without being confused with absence of host support.", + "type": "InitializeResult", + "input": { + "protocolVersion": "0.9.0", + "serverSeq": 0, + "snapshots": [], + "repositoryPreparation": { "revision": false, "multipleRepositories": false } + }, + "acceptableOutputs": [{ + "protocolVersion": "0.9.0", + "serverSeq": 0, + "snapshots": [], + "repositoryPreparation": { "revision": false, "multipleRepositories": false } + }] +} diff --git a/types/test-cases/round-trips/052-repository-preparation-multiple.json b/types/test-cases/round-trips/052-repository-preparation-multiple.json new file mode 100644 index 000000000..226d1faf8 --- /dev/null +++ b/types/test-cases/round-trips/052-repository-preparation-multiple.json @@ -0,0 +1,18 @@ +{ + "name": "repository-preparation-multiple", + "group": "A", + "description": "Future hosts can advertise multiple repositories and explicit revisions independently of agent capabilities.", + "type": "InitializeResult", + "input": { + "protocolVersion": "0.9.0", + "serverSeq": 0, + "snapshots": [], + "repositoryPreparation": { "revision": true, "multipleRepositories": true } + }, + "acceptableOutputs": [{ + "protocolVersion": "0.9.0", + "serverSeq": 0, + "snapshots": [], + "repositoryPreparation": { "revision": true, "multipleRepositories": true } + }] +} diff --git a/types/test-cases/round-trips/053-repository-session-request-default-revision.json b/types/test-cases/round-trips/053-repository-session-request-default-revision.json new file mode 100644 index 000000000..eef807c9f --- /dev/null +++ b/types/test-cases/round-trips/053-repository-session-request-default-revision.json @@ -0,0 +1,16 @@ +{ + "name": "repository-session-request-default-revision", + "group": "A", + "description": "Creation uses a typed repository list and preserves omission of the requested revision.", + "type": "CreateSessionParams", + "input": { + "channel": "ahp-session:/repository-session", + "repositories": [{ "source": "https://example.org/team/project.git" }], + "config": { "mode": "review" } + }, + "acceptableOutputs": [{ + "channel": "ahp-session:/repository-session", + "repositories": [{ "source": "https://example.org/team/project.git" }], + "config": { "mode": "review" } + }] +} diff --git a/types/test-cases/round-trips/054-repository-session-request-multiple.json b/types/test-cases/round-trips/054-repository-session-request-multiple.json new file mode 100644 index 000000000..8503e6a26 --- /dev/null +++ b/types/test-cases/round-trips/054-repository-session-request-multiple.json @@ -0,0 +1,20 @@ +{ + "name": "repository-session-request-multiple", + "group": "A", + "description": "A future multi-repository request preserves two revisions of the same source in their requested order.", + "type": "CreateSessionParams", + "input": { + "channel": "ahp-session:/repository-session", + "repositories": [ + { "source": "https://example.org/team/project.git", "revision": "feature" }, + { "source": "https://example.org/team/project.git", "revision": "main" } + ] + }, + "acceptableOutputs": [{ + "channel": "ahp-session:/repository-session", + "repositories": [ + { "source": "https://example.org/team/project.git", "revision": "feature" }, + { "source": "https://example.org/team/project.git", "revision": "main" } + ] + }] +} diff --git a/types/test-cases/round-trips/055-repository-config-query-context.json b/types/test-cases/round-trips/055-repository-config-query-context.json new file mode 100644 index 000000000..6874f6df4 --- /dev/null +++ b/types/test-cases/round-trips/055-repository-config-query-context.json @@ -0,0 +1,18 @@ +{ + "name": "repository-config-query-context", + "group": "A", + "description": "Repository lists and directory context coexist in configuration queries, outside generic config.", + "type": "ResolveSessionConfigParams", + "input": { + "channel": "ahp-root://", + "workingDirectory": "file:///work/context", + "repositories": [{ "source": "https://example.org/team/project.git", "revision": "main" }], + "config": { "mode": "review" } + }, + "acceptableOutputs": [{ + "channel": "ahp-root://", + "workingDirectory": "file:///work/context", + "repositories": [{ "source": "https://example.org/team/project.git", "revision": "main" }], + "config": { "mode": "review" } + }] +} diff --git a/types/test-cases/round-trips/056-repository-config-completions-context.json b/types/test-cases/round-trips/056-repository-config-completions-context.json new file mode 100644 index 000000000..7c9dddd3e --- /dev/null +++ b/types/test-cases/round-trips/056-repository-config-completions-context.json @@ -0,0 +1,22 @@ +{ + "name": "repository-config-completions-context", + "group": "A", + "description": "Configuration completions preserve the repository list and optional existing directory context.", + "type": "SessionConfigCompletionsParams", + "input": { + "channel": "ahp-root://", + "workingDirectory": "file:///work/context", + "repositories": [{ "source": "https://example.org/team/project.git" }], + "config": { "mode": "review" }, + "property": "mode", + "query": "re" + }, + "acceptableOutputs": [{ + "channel": "ahp-root://", + "workingDirectory": "file:///work/context", + "repositories": [{ "source": "https://example.org/team/project.git" }], + "config": { "mode": "review" }, + "property": "mode", + "query": "re" + }] +} diff --git a/types/test-cases/round-trips/057-repository-session-summary.json b/types/test-cases/round-trips/057-repository-session-summary.json new file mode 100644 index 000000000..9222a39d7 --- /dev/null +++ b/types/test-cases/round-trips/057-repository-session-summary.json @@ -0,0 +1,34 @@ +{ + "name": "repository-session-summary", + "group": "A", + "description": "Session summaries retain ordered repository intent, repeated sources, and omitted revisions.", + "type": "SessionSummary", + "input": { + "resource": "ahp-session:/repository-session", + "provider": "example", + "title": "Repository session", + "status": 1, + "createdAt": "2026-09-20T12:00:00.000Z", + "modifiedAt": "2026-09-20T12:00:00.000Z", + "repositories": [ + { "source": "https://example.org/team/project.git" }, + { "source": "https://example.org/team/project.git", "revision": "main" }, + { "source": "https://example.org/team/project.git", "revision": "feature" } + ], + "workingDirectories": ["file:///work/project", "file:///work/project-worktree"] + }, + "acceptableOutputs": [{ + "resource": "ahp-session:/repository-session", + "provider": "example", + "title": "Repository session", + "status": 1, + "createdAt": "2026-09-20T12:00:00.000Z", + "modifiedAt": "2026-09-20T12:00:00.000Z", + "repositories": [ + { "source": "https://example.org/team/project.git" }, + { "source": "https://example.org/team/project.git", "revision": "main" }, + { "source": "https://example.org/team/project.git", "revision": "feature" } + ], + "workingDirectories": ["file:///work/project", "file:///work/project-worktree"] + }] +} diff --git a/types/test-cases/round-trips/058-repository-session-multiple-ready.json b/types/test-cases/round-trips/058-repository-session-multiple-ready.json new file mode 100644 index 000000000..ffcedc449 --- /dev/null +++ b/types/test-cases/round-trips/058-repository-session-multiple-ready.json @@ -0,0 +1,40 @@ +{ + "name": "repository-session-multiple-ready", + "group": "A", + "description": "Ready state preserves repeated sources at different revisions without a positional directory mapping.", + "type": "Snapshot", + "input": { + "resource": "ahp-session:/repository-session", + "state": { + "provider": "example", + "title": "Repository session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "repositories": [ + { "source": "https://example.org/team/project.git", "revision": "feature" }, + { "source": "https://example.org/team/project.git", "revision": "main" } + ], + "workingDirectories": ["file:///work/session-workspace"] + }, + "fromSeq": 2 + }, + "acceptableOutputs": [{ + "resource": "ahp-session:/repository-session", + "state": { + "provider": "example", + "title": "Repository session", + "status": 1, + "lifecycle": "ready", + "activeClients": [], + "chats": [], + "repositories": [ + { "source": "https://example.org/team/project.git", "revision": "feature" }, + { "source": "https://example.org/team/project.git", "revision": "main" } + ], + "workingDirectories": ["file:///work/session-workspace"] + }, + "fromSeq": 2 + }] +}