From 5eadbe33f6048f748fe42a0a125b96e91414bfdc Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Tue, 15 Sep 2026 18:17:33 -0700 Subject: [PATCH 1/4] 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/4] 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/4] 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/4] 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 + }] + }] +}