diff --git a/agent-client-protocol-schema/src/v2/client.rs b/agent-client-protocol-schema/src/v2/client.rs index 2619d71fa..175729e0f 100644 --- a/agent-client-protocol-schema/src/v2/client.rs +++ b/agent-client-protocol-schema/src/v2/client.rs @@ -1259,7 +1259,7 @@ impl TextCommandInput { // Permission -/// Request for user permission to execute a tool call. +/// Request for user permission to proceed with an operation. /// /// Sent when the agent needs authorization before performing a sensitive operation. /// @@ -1273,8 +1273,27 @@ impl TextCommandInput { pub struct RequestPermissionRequest { /// The session ID for this request. pub session_id: SessionId, - /// Details about the tool call requiring permission. - pub tool_call: ToolCallUpdate, + /// Human-readable title for the permission prompt. + /// + /// This title is specific to the permission prompt and does not update any + /// subject's displayed title. + pub title: String, + /// Optional human-readable explanation of why permission is needed. + /// + /// This text is specific to the permission prompt and does not update any + /// subject's displayed content. Omitted or `null` both mean no separate + /// permission description was provided. + #[serde_as(deserialize_as = "DefaultOnError")] + #[schemars(extend("x-deserialize-default-on-error" = true))] + #[serde(default)] + pub description: Option, + /// Optional structured context about the operation requiring permission. + /// + /// Omitted or `null` both mean no structured subject was provided. + #[serde_as(deserialize_as = "DefaultOnError")] + #[schemars(extend("x-deserialize-default-on-error" = true))] + #[serde(default)] + pub subject: Option, /// Available permission options for the user to choose from. #[serde_as(deserialize_as = "DefaultOnError>")] #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))] @@ -1296,17 +1315,33 @@ impl RequestPermissionRequest { #[must_use] pub fn new( session_id: impl Into, - tool_call: ToolCallUpdate, + title: impl Into, options: Vec, ) -> Self { Self { session_id: session_id.into(), - tool_call, + title: title.into(), + description: None, + subject: None, options, meta: None, } } + /// Sets or clears the optional `description` field. + #[must_use] + pub fn description(mut self, description: impl IntoOption) -> Self { + self.description = description.into_option(); + self + } + + /// Sets or clears the optional `subject` field. + #[must_use] + pub fn subject(mut self, subject: impl IntoOption) -> Self { + self.subject = subject.into_option(); + self + } + /// The _meta property is reserved by ACP to allow clients and agents to attach additional /// metadata to their interactions. Implementations MUST NOT make assumptions about values at /// these keys. @@ -1319,6 +1354,119 @@ impl RequestPermissionRequest { } } +/// The operation requiring permission. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(tag = "type", rename_all = "snake_case")] +#[schemars(extend("discriminator" = {"propertyName": "type"}))] +#[non_exhaustive] +pub enum RequestPermissionSubject { + /// Permission is requested before executing a tool call. + ToolCall(Box), + /// Custom or future permission subject. + /// + /// Values beginning with `_` are reserved for implementation-specific + /// extensions. Unknown values that do not begin with `_` are reserved for + /// future ACP variants. + /// + /// Clients that do not understand this subject type should preserve the raw + /// payload when storing, replaying, proxying, or forwarding permission + /// requests, and otherwise display a generic permission prompt or decline it + /// according to policy. + #[serde(untagged)] + Other(OtherRequestPermissionSubject), +} + +impl From for RequestPermissionSubject { + fn from(subject: ToolCallPermissionSubject) -> Self { + Self::ToolCall(Box::new(subject)) + } +} + +impl From for RequestPermissionSubject { + fn from(tool_call: ToolCallUpdate) -> Self { + ToolCallPermissionSubject::new(tool_call).into() + } +} + +/// Permission request details for a tool call. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct ToolCallPermissionSubject { + /// Details about the tool call requiring permission. + pub tool_call: ToolCallUpdate, +} + +impl ToolCallPermissionSubject { + /// Builds [`ToolCallPermissionSubject`] with the required fields set. + #[must_use] + pub fn new(tool_call: ToolCallUpdate) -> Self { + Self { tool_call } + } +} + +/// Custom or future permission subject payload. +#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq)] +#[schemars(inline)] +#[schemars(transform = other_request_permission_subject_schema)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct OtherRequestPermissionSubject { + /// Custom or future permission subject type. + /// + /// Values beginning with `_` are reserved for implementation-specific + /// extensions. Unknown values that do not begin with `_` are reserved for + /// future ACP variants. + #[serde(rename = "type")] + pub type_: String, + /// Additional fields from the unknown permission subject payload. + #[serde(flatten)] + pub fields: BTreeMap, +} + +impl OtherRequestPermissionSubject { + /// Builds [`OtherRequestPermissionSubject`] from an unknown discriminator and preserves the remaining extension fields. + #[must_use] + pub fn new(type_: impl Into, mut fields: BTreeMap) -> Self { + fields.remove("type"); + Self { + type_: type_.into(), + fields, + } + } +} + +impl<'de> Deserialize<'de> for OtherRequestPermissionSubject { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let mut fields = BTreeMap::::deserialize(deserializer)?; + let type_ = fields + .remove("type") + .ok_or_else(|| serde::de::Error::missing_field("type"))?; + let serde_json::Value::String(type_) = type_ else { + return Err(serde::de::Error::custom("`type` must be a string")); + }; + + if is_known_request_permission_subject_type(&type_) { + return Err(serde::de::Error::custom(format!( + "known request permission subject `{type_}` did not match its schema" + ))); + } + + Ok(Self { type_, fields }) + } +} + +fn is_known_request_permission_subject_type(type_: &str) -> bool { + matches!(type_, "tool_call") +} + +fn other_request_permission_subject_schema(schema: &mut Schema) { + super::schema_util::reject_known_string_discriminators(schema, "type", &["tool_call"]); +} + /// An option presented to the user when requesting permission. #[serde_as] #[skip_serializing_none] @@ -1924,7 +2072,7 @@ pub(crate) const ELICITATION_COMPLETE_NOTIFICATION: &str = "elicitation/complete #[schemars(inline)] #[non_exhaustive] pub enum AgentRequest { - /// Requests permission from the user for a tool call operation. + /// Requests permission from the user for an operation. /// /// Called by the agent when it needs user authorization before executing /// a potentially sensitive operation. The client should present the options @@ -2641,6 +2789,149 @@ mod tests { assert!(matches!(roundtripped, AvailableCommandInput::Text(_))); } + #[test] + fn request_permission_subject_tool_call_uses_type_discriminator() { + use serde_json::json; + + let subject = RequestPermissionSubject::from(ToolCallUpdate::new("call_001")); + + let json = serde_json::to_value(&subject).unwrap(); + assert_eq!( + json, + json!({ + "type": "tool_call", + "toolCall": { + "toolCallId": "call_001" + } + }) + ); + + let roundtripped: RequestPermissionSubject = serde_json::from_value(json).unwrap(); + assert!(matches!( + roundtripped, + RequestPermissionSubject::ToolCall(_) + )); + } + + #[test] + fn request_permission_subject_preserves_unknown_variant() { + use serde_json::json; + + let subject: RequestPermissionSubject = serde_json::from_value(json!({ + "type": "_review", + "reason": "needs-review", + "retryAfterSeconds": 30 + })) + .unwrap(); + + let RequestPermissionSubject::Other(unknown) = subject else { + panic!("expected unknown permission subject"); + }; + + assert_eq!(unknown.type_, "_review"); + assert_eq!(unknown.fields.get("reason"), Some(&json!("needs-review"))); + assert_eq!(unknown.fields.get("retryAfterSeconds"), Some(&json!(30))); + assert_eq!( + serde_json::to_value(RequestPermissionSubject::Other(unknown)).unwrap(), + json!({ + "type": "_review", + "reason": "needs-review", + "retryAfterSeconds": 30 + }) + ); + } + + #[test] + fn request_permission_subject_unknown_does_not_hide_malformed_known_variant() { + use serde_json::json; + + assert!( + serde_json::from_value::(json!({ + "type": "tool_call" + })) + .is_err() + ); + assert!( + serde_json::from_value::(json!({ + "type": 1 + })) + .is_err() + ); + } + + #[test] + fn request_permission_title_and_description_are_separate_from_tool_call_content() { + use serde_json::json; + + let request = + RequestPermissionRequest::new("sess_abc123def456", "Approve file edit?", Vec::new()) + .description("Allow this tool to edit src/main.rs?") + .subject(RequestPermissionSubject::from(ToolCallUpdate::new( + "call_001", + ))); + + assert_eq!( + serde_json::to_value(request).unwrap(), + json!({ + "sessionId": "sess_abc123def456", + "title": "Approve file edit?", + "description": "Allow this tool to edit src/main.rs?", + "subject": { + "type": "tool_call", + "toolCall": { + "toolCallId": "call_001" + } + }, + "options": [] + }) + ); + } + + #[test] + fn request_permission_requires_title_and_allows_missing_subject() { + use serde_json::json; + + let request = RequestPermissionRequest::new( + "sess_abc123def456", + "Approve elevated permissions?", + Vec::new(), + ); + + assert_eq!( + serde_json::to_value(request).unwrap(), + json!({ + "sessionId": "sess_abc123def456", + "title": "Approve elevated permissions?", + "options": [] + }) + ); + + let missing_subject: RequestPermissionRequest = serde_json::from_value(json!({ + "sessionId": "sess_abc123def456", + "title": "Approve elevated permissions?", + "options": [] + })) + .unwrap(); + assert!(missing_subject.subject.is_none()); + + let null_subject: RequestPermissionRequest = serde_json::from_value(json!({ + "sessionId": "sess_abc123def456", + "title": "Approve elevated permissions?", + "subject": null, + "options": [] + })) + .unwrap(); + assert!(null_subject.subject.is_none()); + + assert!( + serde_json::from_value::(json!({ + "sessionId": "sess_abc123def456", + "options": [] + })) + .is_err() + ); + } + #[test] fn request_permission_outcome_preserves_unknown_variant() { use serde_json::json; diff --git a/agent-client-protocol-schema/src/v2/conversion.rs b/agent-client-protocol-schema/src/v2/conversion.rs index 60bd11255..ee3445058 100644 --- a/agent-client-protocol-schema/src/v2/conversion.rs +++ b/agent-client-protocol-schema/src/v2/conversion.rs @@ -1493,10 +1493,29 @@ impl IntoV1 for super::RequestPermissionRequest { fn into_v1(self) -> Result { let Self { session_id, - tool_call, + title: _, + description: _, + subject, options, meta, } = self; + let Some(subject) = subject else { + return Err(ProtocolConversionError::new( + "v2 RequestPermissionRequest without `subject` cannot be represented in v1", + )); + }; + let tool_call = match subject { + super::RequestPermissionSubject::ToolCall(subject) => { + let super::ToolCallPermissionSubject { tool_call } = *subject; + tool_call + } + super::RequestPermissionSubject::Other(subject) => { + return Err(unknown_v2_enum_variant( + "RequestPermissionSubject", + &subject.type_, + )); + } + }; Ok(crate::v1::RequestPermissionRequest { session_id: session_id.into_v1()?, tool_call: tool_call.into_v1()?, @@ -1516,9 +1535,17 @@ impl IntoV2 for crate::v1::RequestPermissionRequest { options, meta, } = self; + let title = tool_call + .fields + .title + .clone() + .filter(|title| !title.is_empty()) + .unwrap_or_else(|| "Permission requested".to_string()); Ok(super::RequestPermissionRequest { session_id: session_id.into_v2()?, - tool_call: tool_call.into_v2()?, + title, + description: None, + subject: Some(super::RequestPermissionSubject::from(tool_call.into_v2()?)), options: options.into_v2()?, meta: meta.into_v2()?, }) @@ -10473,6 +10500,20 @@ mod tests { )), "v2 AvailableCommandInput variant `_choices` cannot be represented in v1", ); + assert_v2_to_v1_error( + v2::RequestPermissionRequest::new("session-id", "Permission requested", Vec::new()) + .subject(v2::RequestPermissionSubject::Other( + v2::OtherRequestPermissionSubject::new( + "_review", + std::collections::BTreeMap::new(), + ), + )), + "v2 RequestPermissionSubject variant `_review` cannot be represented in v1", + ); + assert_v2_to_v1_error( + v2::RequestPermissionRequest::new("session-id", "Permission requested", Vec::new()), + "v2 RequestPermissionRequest without `subject` cannot be represented in v1", + ); assert_v2_to_v1_error( v2::RequestPermissionOutcome::Other(v2::OtherRequestPermissionOutcome::new( "_defer", @@ -10532,6 +10573,35 @@ mod tests { ); } + #[test] + fn converts_v1_request_permission_request_with_required_v2_title() { + let titled = v1::RequestPermissionRequest::new( + "session-id", + v1::ToolCallUpdate::new("call_1", v1::ToolCallUpdateFields::new().title("Read file")), + Vec::new(), + ); + + let converted: v2::RequestPermissionRequest = v1_to_v2(titled).unwrap(); + assert_eq!(converted.title, "Read file"); + let Some(v2::RequestPermissionSubject::ToolCall(subject)) = converted.subject else { + panic!("expected tool-call permission subject"); + }; + assert_eq!(subject.tool_call.tool_call_id.to_string(), "call_1"); + + let fallback = v1::RequestPermissionRequest::new( + "session-id", + v1::ToolCallUpdate::new("call_2", v1::ToolCallUpdateFields::new()), + Vec::new(), + ); + + let converted: v2::RequestPermissionRequest = v1_to_v2(fallback).unwrap(); + assert_eq!(converted.title, "Permission requested"); + let Some(v2::RequestPermissionSubject::ToolCall(subject)) = converted.subject else { + panic!("expected tool-call permission subject"); + }; + assert_eq!(subject.tool_call.tool_call_id.to_string(), "call_2"); + } + #[test] fn round_trips_error_with_data_payload() { let err = v1::Error::invalid_params().data(serde_json::json!({ diff --git a/docs/docs.json b/docs/docs.json index fb4f5b9e2..bed6252c2 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -203,6 +203,7 @@ "rfds/v2/client-filesystem-terminal-capabilities", "rfds/v2/plan-variants", "rfds/v2/tool-call-updates", + "rfds/v2/permission-requests", "rfds/v2/message-updates" ] } diff --git a/docs/protocol/v2/draft/overview.mdx b/docs/protocol/v2/draft/overview.mdx index 3fb274581..683e06b4c 100644 --- a/docs/protocol/v2/draft/overview.mdx +++ b/docs/protocol/v2/draft/overview.mdx @@ -123,8 +123,8 @@ Clients provide the interface between users and agents. They are typically code ]} > [Request user - authorization](/protocol/v2/draft/tool-calls#requesting-permission) for tool - calls. + authorization](/protocol/v2/draft/tool-calls#requesting-permission) for + operations such as tool calls. ### Notifications diff --git a/docs/protocol/v2/draft/schema.mdx b/docs/protocol/v2/draft/schema.mdx index fcc3add74..1b421e0ca 100644 --- a/docs/protocol/v2/draft/schema.mdx +++ b/docs/protocol/v2/draft/schema.mdx @@ -1998,7 +1998,7 @@ This is the inner MCP response result payload. Any JSON value is valid. ### session/request_permission -Requests permission from the user for a tool call operation. +Requests permission from the user for an operation. Called by the agent when it needs user authorization before executing a potentially sensitive operation. The client should present the options @@ -2011,7 +2011,7 @@ See protocol docs: [Requesting Permission](https://agentclientprotocol.com/proto #### RequestPermissionRequest -Request for user permission to execute a tool call. +Request for user permission to proceed with an operation. Sent when the agent needs authorization before performing a sensitive operation. @@ -2028,6 +2028,14 @@ these keys. See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility) + + + Optional human-readable explanation of why permission is needed. + +This text is specific to the permission prompt and does not update any +subject's displayed content. Omitted or `null` both mean no separate +permission description was provided. + PermissionOption[]} required> Available permission options for the user to choose from. @@ -2035,8 +2043,18 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/d SessionId} required> The session ID for this request. -ToolCallUpdate} required> - Details about the tool call requiring permission. +RequestPermissionSubject | null} > + Optional structured context about the operation requiring permission. + +Omitted or `null` both mean no structured subject was provided. + + + + Human-readable title for the permission prompt. + +This title is specific to the permission prompt and does not update any +subject's displayed title. + #### RequestPermissionResponse @@ -6652,6 +6670,57 @@ future ACP variants. +## RequestPermissionSubject + +The operation requiring permission. + +**Type:** Union + + +Permission is requested before executing a tool call. + + + +ToolCallUpdate} + required +> + Details about the tool call requiring permission. + + + The discriminator value. Must be `"tool_call"`. + + + + + + +Custom or future permission subject. + +Values beginning with `_` are reserved for implementation-specific +extensions. Unknown values that do not begin with `_` are reserved for +future ACP variants. + +Clients that do not understand this subject type should preserve the raw +payload when storing, replaying, proxying, or forwarding permission +requests, and otherwise display a generic permission prompt or decline it +according to policy. + + + + + Custom or future permission subject type. + +Values beginning with `_` are reserved for implementation-specific +extensions. Unknown values that do not begin with `_` are reserved for +future ACP variants. + + + + + + ## RequiresActionStateUpdate The agent is waiting on user action before it can continue. @@ -8370,6 +8439,22 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/d The absolute file path being accessed or modified. +## ToolCallPermissionSubject + +Permission request details for a tool call. + +**Type:** Object + +**Properties:** + +ToolCallUpdate} + required +> + Details about the tool call requiring permission. + + ## ToolCallStatus Execution status of a tool call. diff --git a/docs/protocol/v2/draft/tool-calls.mdx b/docs/protocol/v2/draft/tool-calls.mdx index baec689f5..5a01b3397 100644 --- a/docs/protocol/v2/draft/tool-calls.mdx +++ b/docs/protocol/v2/draft/tool-calls.mdx @@ -164,7 +164,7 @@ with `content: []` or `content: null` clears the tool-call content. A ## Requesting Permission -The Agent **MAY** request permission from the user before executing a tool call by calling the `session/request_permission` method: +The Agent **MAY** request permission from the user before proceeding with an operation, such as executing a tool call, by calling the `session/request_permission` method: ```json { @@ -173,8 +173,13 @@ The Agent **MAY** request permission from the user before executing a tool call "method": "session/request_permission", "params": { "sessionId": "sess_abc123def456", - "toolCall": { - "toolCallId": "call_001" + "title": "Approve file edit?", + "description": "Allow the agent to edit src/main.rs?", + "subject": { + "type": "tool_call", + "toolCall": { + "toolCallId": "call_001" + } }, "options": [ { @@ -196,8 +201,24 @@ The Agent **MAY** request permission from the user before executing a tool call The session ID for this request - - The tool call update containing details about the operation + + Title shown with the permission prompt. This text is separate from any + `toolCall` update and does not replace the tool-call title. + + + + Optional explanation shown with the permission prompt. This text is separate + from any `toolCall` update and does not replace tool-call content. Omitted or + `null` means no separate permission description was provided. + + + + Optional structured context about the operation requiring permission. For tool + calls, use `type: "tool_call"` with a `toolCall` update containing details + about the operation. Omitted or `null` means no structured subject was + provided. Custom or future subject types may appear. Clients that do not + understand a subject should preserve it when proxying and use the common + prompt fields or decline according to policy. diff --git a/docs/protocol/v2/overview.mdx b/docs/protocol/v2/overview.mdx index 202ddb845..0d728de90 100644 --- a/docs/protocol/v2/overview.mdx +++ b/docs/protocol/v2/overview.mdx @@ -120,7 +120,7 @@ Clients provide the interface between users and agents. They are typically code post={[Schema]} > [Request user authorization](/protocol/v2/tool-calls#requesting-permission) - for tool calls. + for operations such as tool calls. ### Notifications diff --git a/docs/protocol/v2/schema.mdx b/docs/protocol/v2/schema.mdx index ea75ff15f..dccf1c3e6 100644 --- a/docs/protocol/v2/schema.mdx +++ b/docs/protocol/v2/schema.mdx @@ -759,7 +759,7 @@ and control access to resources. ### session/request_permission -Requests permission from the user for a tool call operation. +Requests permission from the user for an operation. Called by the agent when it needs user authorization before executing a potentially sensitive operation. The client should present the options @@ -772,7 +772,7 @@ See protocol docs: [Requesting Permission](https://agentclientprotocol.com/proto #### RequestPermissionRequest -Request for user permission to execute a tool call. +Request for user permission to proceed with an operation. Sent when the agent needs authorization before performing a sensitive operation. @@ -789,6 +789,14 @@ these keys. See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility) + + + Optional human-readable explanation of why permission is needed. + +This text is specific to the permission prompt and does not update any +subject's displayed content. Omitted or `null` both mean no separate +permission description was provided. + PermissionOption[]} required> Available permission options for the user to choose from. @@ -796,8 +804,18 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/e SessionId} required> The session ID for this request. -ToolCallUpdate} required> - Details about the tool call requiring permission. +RequestPermissionSubject | null} > + Optional structured context about the operation requiring permission. + +Omitted or `null` both mean no structured subject was provided. + + + + Human-readable title for the permission prompt. + +This title is specific to the permission prompt and does not update any +subject's displayed title. + #### RequestPermissionResponse @@ -2730,6 +2748,57 @@ future ACP variants. +## RequestPermissionSubject + +The operation requiring permission. + +**Type:** Union + + +Permission is requested before executing a tool call. + + + +ToolCallUpdate} + required +> + Details about the tool call requiring permission. + + + The discriminator value. Must be `"tool_call"`. + + + + + + +Custom or future permission subject. + +Values beginning with `_` are reserved for implementation-specific +extensions. Unknown values that do not begin with `_` are reserved for +future ACP variants. + +Clients that do not understand this subject type should preserve the raw +payload when storing, replaying, proxying, or forwarding permission +requests, and otherwise display a generic permission prompt or decline it +according to policy. + + + + + Custom or future permission subject type. + +Values beginning with `_` are reserved for implementation-specific +extensions. Unknown values that do not begin with `_` are reserved for +future ACP variants. + + + + + + ## RequiresActionStateUpdate The agent is waiting on user action before it can continue. @@ -4155,6 +4224,22 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/e The absolute file path being accessed or modified. +## ToolCallPermissionSubject + +Permission request details for a tool call. + +**Type:** Object + +**Properties:** + +ToolCallUpdate} + required +> + Details about the tool call requiring permission. + + ## ToolCallStatus Execution status of a tool call. diff --git a/docs/protocol/v2/tool-calls.mdx b/docs/protocol/v2/tool-calls.mdx index 48ea80a50..6c18533e1 100644 --- a/docs/protocol/v2/tool-calls.mdx +++ b/docs/protocol/v2/tool-calls.mdx @@ -164,7 +164,7 @@ with `content: []` or `content: null` clears the tool-call content. A ## Requesting Permission -The Agent **MAY** request permission from the user before executing a tool call by calling the `session/request_permission` method: +The Agent **MAY** request permission from the user before proceeding with an operation, such as executing a tool call, by calling the `session/request_permission` method: ```json { @@ -173,8 +173,13 @@ The Agent **MAY** request permission from the user before executing a tool call "method": "session/request_permission", "params": { "sessionId": "sess_abc123def456", - "toolCall": { - "toolCallId": "call_001" + "title": "Approve file edit?", + "description": "Allow the agent to edit src/main.rs?", + "subject": { + "type": "tool_call", + "toolCall": { + "toolCallId": "call_001" + } }, "options": [ { @@ -196,8 +201,24 @@ The Agent **MAY** request permission from the user before executing a tool call The session ID for this request - - The tool call update containing details about the operation + + Title shown with the permission prompt. This text is separate from any + `toolCall` update and does not replace the tool-call title. + + + + Optional explanation shown with the permission prompt. This text is separate + from any `toolCall` update and does not replace tool-call content. Omitted or + `null` means no separate permission description was provided. + + + + Optional structured context about the operation requiring permission. For tool + calls, use `type: "tool_call"` with a `toolCall` update containing details + about the operation. Omitted or `null` means no structured subject was + provided. Custom or future subject types may appear. Clients that do not + understand a subject should preserve it when proxying and use the common + prompt fields or decline according to policy. diff --git a/docs/rfds/v2/overview.mdx b/docs/rfds/v2/overview.mdx index 3eb54666f..64f831bec 100644 --- a/docs/rfds/v2/overview.mdx +++ b/docs/rfds/v2/overview.mdx @@ -33,6 +33,7 @@ Current RFDs accepted as Drafts that are targeting v2 release - [Client Filesystem and Terminal Surface](./client-filesystem-terminal-capabilities.mdx) - [Plan Variants](./plan-variants.mdx) - [Tool Call Updates](./tool-call-updates.mdx) +- [Permission Requests](./permission-requests.mdx) - [Message Updates and Chunks](./message-updates.mdx) - [Remote Transports](../streamable-http-websocket-transport.mdx) @@ -46,6 +47,7 @@ Other RFDs will progress separately and are not dependent on breaking changes (s - Make [plan variants](./plan-variants.mdx) the default v2 plan shape by replacing the old `plan` session update with item-based `plan_update`. - Replace the v1 split between `tool_call` and `tool_call_update` with a single [tool-call update](./tool-call-updates.mdx) upsert shape keyed by `toolCallId`. - Add [tool-call content chunks](./tool-call-updates.mdx) so Agents can stream individual `ToolCallContent` items that append to a tool call. +- Make [permission requests](./permission-requests.mdx) carry a required prompt `title` and optional extensible `subject` tagged union. Tool-call permissions use `subject.type: "tool_call"` with the same `ToolCallUpdate` payload shape as session updates, while subject-less permissions rely on the common prompt fields. - Add [whole-message updates](./message-updates.mdx) for `user_message`, `agent_message`, and `agent_thought` alongside streamed chunks. Message updates are upserts keyed by `messageId`; their `content` arrays replace current message content, while chunks append to the current content. - Require [message IDs](../message-id.mdx) on streamed message chunks. - Follow JSON-RPC 2.0 batch request and notification behavior. @@ -109,6 +111,7 @@ With the needed breaking changes, as much as possible I am targeting having a co ## Revision history +- 2026-07-02: Added the v2 Permission Requests RFD for required permission titles, optional structured subjects, and tool-call permission subjects. - 2026-06-30: Recorded the v2 ID unification principle: prefer domain-specific ID field names. - 2026-06-25: Recorded the v2 initialize information cleanup: required role-agnostic `info` in both params and results, replacing `clientInfo` and `agentInfo`. - 2026-06-25: Recorded the v2 authentication method cleanup: grouped `auth/login` and required `auth/logout` method names replace v1's top-level `authenticate` and capability-gated `logout`, with matching generated type names for login and logout auth requests and responses. diff --git a/docs/rfds/v2/permission-requests.mdx b/docs/rfds/v2/permission-requests.mdx new file mode 100644 index 000000000..0c12ed131 --- /dev/null +++ b/docs/rfds/v2/permission-requests.mdx @@ -0,0 +1,101 @@ +--- +title: "v2 Permission Requests" +--- + +Author(s): [@benbrandt](https://github.com/benbrandt) + +## Elevator pitch + +> What are you proposing to change? + +ACP v2 should make `session/request_permission` extensible beyond tool calls by giving every permission prompt a required `title` and optional structured `subject` context. Tool-call permissions use `subject.type: "tool_call"`, while permission requests that are not tied to a structured subject omit `subject`. + +Permission prompt copy should live in common `title` and `description` fields rather than in subject-specific data. + +## Status quo + +> How do things work today and what problems does this cause? Why would we change things? + +In v1, permission requests are tied directly to a `toolCall` field. This works for tool execution approvals, but it makes the method harder to extend to other approval-like decisions. + +It also tempts agents to put permission prompt text into `ToolCallUpdate.title` or `ToolCallUpdate.content`. In v2, those fields are patch state for the displayed tool call. Using them for permission-only UI copy can accidentally change the tool call shown elsewhere. + +## What we propose to do about it + +> What are you proposing to improve the situation? + +`RequestPermissionRequest` should have these common fields: + +- `sessionId` +- `title` +- `description` +- `options` +- `subject` +- `_meta` + +`title` is required and is the minimum user-visible prompt text. `description` is optional explanatory text. Omitted and `null` descriptions are equivalent and mean no extra detail was provided. + +`subject` is optional structured context and uses a tagged union when present: + +```json +{ + "title": "Approve file edit?", + "description": "Allow the agent to edit src/main.rs?", + "subject": { + "type": "tool_call", + "toolCall": { + "toolCallId": "call_001" + } + } +} +``` + +```json +{ + "title": "Continue with elevated permissions?" +} +``` + +Omitted and `null` subjects are equivalent and mean no structured subject was provided. A missing subject is not a separate subject variant. + +Unknown future subject types should be preserved by clients that proxy, store, replay, or forward permission requests. Clients that cannot understand the subject should show a generic permission prompt or decline according to policy. + +## Shiny future + +> How will things play out once this feature exists? + +Agents can request permission for tool calls without conflating permission UI text with tool-call state. They can also request permission for operations that have no structured subject, and future structured subjects can be added without changing the common permission request fields. + +## Implementation details and plan + +> Tell me more about your implementation. What is your detailed implementation plan? + +1. Replace the v1-style top-level `toolCall` request field in v2 with an optional `subject` tagged union. +2. Add the `tool_call` subject variant. +3. Add common required `title` and optional `description` fields. +4. Keep `options` as the required list of choices the user can select. +5. Preserve unknown subject variants while keeping known subject payloads distinct from unknown fallback variants. + +## Frequently asked questions + +> What questions have arisen over the course of authoring this document or during subsequent discussions? + +### Why require `title`? + +The title is the minimum text a client can display when asking the user to choose an option. Requiring it keeps subject-less permission requests usable without requiring clients to synthesize prompt copy. + +### Why make `subject` optional? + +Some permission requests are valid without a structured target. In those cases, `title`, `description`, and `options` are the permission request, while `subject` is additional context for clients that can render a richer prompt. + +### Why not flatten subject fields into the request? + +`title`, `description`, and `options` describe the permission prompt. `subject` describes what the permission is about. Keeping the subject nested avoids collisions between common request fields and future subject-specific fields. + +### Why not use `ToolCallUpdate.title` or `ToolCallUpdate.content` for permission copy? + +Those fields patch the displayed tool call. Permission-only prompt copy belongs in the common `title` and `description` fields so a permission request does not accidentally replace tool-call title or content. It's quite common to have a reason you want to display for the permission request itself without updating the persisted tool call for the rest of the session. + +## Revision history + +- 2026-07-02: Initial draft diff --git a/docs/rfds/v2/tool-call-updates.mdx b/docs/rfds/v2/tool-call-updates.mdx index a08412178..74b4ce6c1 100644 --- a/docs/rfds/v2/tool-call-updates.mdx +++ b/docs/rfds/v2/tool-call-updates.mdx @@ -57,7 +57,9 @@ The v2 `ToolCallUpdate` payload has the following semantics: - For a new `toolCallId`, omitted fields use Client defaults. - Agents **SHOULD** include `title` the first time they report a `toolCallId`. -The `session/request_permission` method should also carry this same `ToolCallUpdate` shape in its `toolCall` field, so the permission UI receives the same patch/upsert payload shape as session updates. +The `session/request_permission` method should also carry this same `ToolCallUpdate` shape for tool-call permissions. In v2, [permission requests](./permission-requests.mdx) use an optional `subject` tagged union; tool-call permissions set `subject.type: "tool_call"` and carry the update in the subject's `toolCall` field, so the permission UI receives the same patch/upsert payload shape as session updates while leaving room for future permission subjects. + +Permission-specific prompt text should be carried by the request's common required `title` and optional `description` fields instead of `ToolCallUpdate.title` or `ToolCallUpdate.content`. That keeps a permission prompt from accidentally replacing displayed tool-call state. ACP v2 should define a matching streaming update: @@ -114,5 +116,6 @@ Tool-call updates need both operations. Omission means "I am not changing this f ## Revision history +- 2026-07-02: Updated permission requests to carry tool-call updates through an optional `subject` tagged union and to use separate `title` and `description` fields for permission-specific prompt text. - 2026-06-09: Added `tool_call_content_chunk` for streaming tool-call content. - 2026-06-08: Initial draft diff --git a/schema/v2/schema.json b/schema/v2/schema.json index e7186c4ba..97b9d3420 100644 --- a/schema/v2/schema.json +++ b/schema/v2/schema.json @@ -485,7 +485,7 @@ "anyOf": [ { "title": "RequestPermissionRequest", - "description": "Requests permission from the user for a tool call operation.\n\nCalled by the agent when it needs user authorization before executing\na potentially sensitive operation. The client should present the options\nto the user and return their decision.\n\nIf the client cancels active session work via `session/cancel`, it MUST\nrespond to this request with `RequestPermissionOutcome::Cancelled`.\n\nSee protocol docs: [Requesting Permission](https://agentclientprotocol.com/protocol/v2/tool-calls#requesting-permission)", + "description": "Requests permission from the user for an operation.\n\nCalled by the agent when it needs user authorization before executing\na potentially sensitive operation. The client should present the options\nto the user and return their decision.\n\nIf the client cancels active session work via `session/cancel`, it MUST\nrespond to this request with `RequestPermissionOutcome::Cancelled`.\n\nSee protocol docs: [Requesting Permission](https://agentclientprotocol.com/protocol/v2/tool-calls#requesting-permission)", "allOf": [ { "$ref": "#/$defs/RequestPermissionRequest" @@ -534,7 +534,7 @@ ] }, "RequestPermissionRequest": { - "description": "Request for user permission to execute a tool call.\n\nSent when the agent needs authorization before performing a sensitive operation.\n\nSee protocol docs: [Requesting Permission](https://agentclientprotocol.com/protocol/v2/tool-calls#requesting-permission)", + "description": "Request for user permission to proceed with an operation.\n\nSent when the agent needs authorization before performing a sensitive operation.\n\nSee protocol docs: [Requesting Permission](https://agentclientprotocol.com/protocol/v2/tool-calls#requesting-permission)", "type": "object", "properties": { "sessionId": { @@ -545,13 +545,26 @@ } ] }, - "toolCall": { - "description": "Details about the tool call requiring permission.", - "allOf": [ + "title": { + "description": "Human-readable title for the permission prompt.\n\nThis title is specific to the permission prompt and does not update any\nsubject's displayed title.", + "type": "string" + }, + "description": { + "description": "Optional human-readable explanation of why permission is needed.\n\nThis text is specific to the permission prompt and does not update any\nsubject's displayed content. Omitted or `null` both mean no separate\npermission description was provided.", + "type": ["string", "null"], + "x-deserialize-default-on-error": true + }, + "subject": { + "description": "Optional structured context about the operation requiring permission.\n\nOmitted or `null` both mean no structured subject was provided.", + "anyOf": [ { - "$ref": "#/$defs/ToolCallUpdate" + "$ref": "#/$defs/RequestPermissionSubject" + }, + { + "type": "null" } - ] + ], + "x-deserialize-default-on-error": true }, "options": { "description": "Available permission options for the user to choose from.", @@ -569,7 +582,7 @@ "additionalProperties": true } }, - "required": ["sessionId", "toolCall", "options"], + "required": ["sessionId", "title", "options"], "x-side": "client", "x-method": "session/request_permission" }, @@ -577,6 +590,57 @@ "description": "A unique identifier for a conversation session between a client and agent.\n\nSessions maintain their own context, conversation history, and state,\nallowing multiple independent interactions with the same agent.\n\nSee protocol docs: [Session ID](https://agentclientprotocol.com/protocol/v2/session-setup#session-id)", "type": "string" }, + "RequestPermissionSubject": { + "description": "The operation requiring permission.", + "anyOf": [ + { + "description": "Permission is requested before executing a tool call.", + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "tool_call" + } + }, + "required": ["type"], + "allOf": [ + { + "$ref": "#/$defs/ToolCallPermissionSubject" + } + ] + }, + { + "title": "other", + "description": "Custom or future permission subject.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.\n\nClients that do not understand this subject type should preserve the raw\npayload when storing, replaying, proxying, or forwarding permission\nrequests, and otherwise display a generic permission prompt or decline it\naccording to policy.", + "type": "object", + "properties": { + "type": { + "description": "Custom or future permission subject type.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.", + "type": "string" + } + }, + "required": ["type"], + "not": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "tool_call" + } + }, + "required": ["type"] + } + ] + }, + "additionalProperties": true + } + ], + "discriminator": { + "propertyName": "type" + } + }, "ToolCallUpdate": { "description": "Represents an upsert for a tool call that the language model has requested.\n\nTool calls are actions that the agent executes on behalf of the language model,\nsuch as reading files, executing code, or fetching data from external sources.\n\nOnly [`ToolCallUpdate::tool_call_id`] is required. Other fields have patch semantics:\nomitted fields leave the existing tool call value unchanged, `null` clears or\nunsets the value, and concrete values replace the previous value. For\ncollection fields, concrete arrays replace the previous collection, and both\n`null` and `[]` clear the collection. When a client receives a tool call ID it\nhas not seen before, omitted fields use client defaults.\n\nSee protocol docs: [Tool Calls](https://agentclientprotocol.com/protocol/v2/tool-calls)", "type": "object", @@ -1364,6 +1428,21 @@ }, "required": ["path"] }, + "ToolCallPermissionSubject": { + "description": "Permission request details for a tool call.", + "type": "object", + "properties": { + "toolCall": { + "description": "Details about the tool call requiring permission.", + "allOf": [ + { + "$ref": "#/$defs/ToolCallUpdate" + } + ] + } + }, + "required": ["toolCall"] + }, "PermissionOption": { "description": "An option presented to the user when requesting permission.", "type": "object", diff --git a/schema/v2/schema.unstable.json b/schema/v2/schema.unstable.json index 00cc31ccf..8aa288ea2 100644 --- a/schema/v2/schema.unstable.json +++ b/schema/v2/schema.unstable.json @@ -593,7 +593,7 @@ "anyOf": [ { "title": "RequestPermissionRequest", - "description": "Requests permission from the user for a tool call operation.\n\nCalled by the agent when it needs user authorization before executing\na potentially sensitive operation. The client should present the options\nto the user and return their decision.\n\nIf the client cancels active session work via `session/cancel`, it MUST\nrespond to this request with `RequestPermissionOutcome::Cancelled`.\n\nSee protocol docs: [Requesting Permission](https://agentclientprotocol.com/protocol/v2/draft/tool-calls#requesting-permission)", + "description": "Requests permission from the user for an operation.\n\nCalled by the agent when it needs user authorization before executing\na potentially sensitive operation. The client should present the options\nto the user and return their decision.\n\nIf the client cancels active session work via `session/cancel`, it MUST\nrespond to this request with `RequestPermissionOutcome::Cancelled`.\n\nSee protocol docs: [Requesting Permission](https://agentclientprotocol.com/protocol/v2/draft/tool-calls#requesting-permission)", "allOf": [ { "$ref": "#/$defs/RequestPermissionRequest" @@ -678,7 +678,7 @@ ] }, "RequestPermissionRequest": { - "description": "Request for user permission to execute a tool call.\n\nSent when the agent needs authorization before performing a sensitive operation.\n\nSee protocol docs: [Requesting Permission](https://agentclientprotocol.com/protocol/v2/draft/tool-calls#requesting-permission)", + "description": "Request for user permission to proceed with an operation.\n\nSent when the agent needs authorization before performing a sensitive operation.\n\nSee protocol docs: [Requesting Permission](https://agentclientprotocol.com/protocol/v2/draft/tool-calls#requesting-permission)", "type": "object", "properties": { "sessionId": { @@ -689,13 +689,26 @@ } ] }, - "toolCall": { - "description": "Details about the tool call requiring permission.", - "allOf": [ + "title": { + "description": "Human-readable title for the permission prompt.\n\nThis title is specific to the permission prompt and does not update any\nsubject's displayed title.", + "type": "string" + }, + "description": { + "description": "Optional human-readable explanation of why permission is needed.\n\nThis text is specific to the permission prompt and does not update any\nsubject's displayed content. Omitted or `null` both mean no separate\npermission description was provided.", + "type": ["string", "null"], + "x-deserialize-default-on-error": true + }, + "subject": { + "description": "Optional structured context about the operation requiring permission.\n\nOmitted or `null` both mean no structured subject was provided.", + "anyOf": [ { - "$ref": "#/$defs/ToolCallUpdate" + "$ref": "#/$defs/RequestPermissionSubject" + }, + { + "type": "null" } - ] + ], + "x-deserialize-default-on-error": true }, "options": { "description": "Available permission options for the user to choose from.", @@ -713,7 +726,7 @@ "additionalProperties": true } }, - "required": ["sessionId", "toolCall", "options"], + "required": ["sessionId", "title", "options"], "x-side": "client", "x-method": "session/request_permission" }, @@ -721,6 +734,57 @@ "description": "A unique identifier for a conversation session between a client and agent.\n\nSessions maintain their own context, conversation history, and state,\nallowing multiple independent interactions with the same agent.\n\nSee protocol docs: [Session ID](https://agentclientprotocol.com/protocol/v2/draft/session-setup#session-id)", "type": "string" }, + "RequestPermissionSubject": { + "description": "The operation requiring permission.", + "anyOf": [ + { + "description": "Permission is requested before executing a tool call.", + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "tool_call" + } + }, + "required": ["type"], + "allOf": [ + { + "$ref": "#/$defs/ToolCallPermissionSubject" + } + ] + }, + { + "title": "other", + "description": "Custom or future permission subject.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.\n\nClients that do not understand this subject type should preserve the raw\npayload when storing, replaying, proxying, or forwarding permission\nrequests, and otherwise display a generic permission prompt or decline it\naccording to policy.", + "type": "object", + "properties": { + "type": { + "description": "Custom or future permission subject type.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.", + "type": "string" + } + }, + "required": ["type"], + "not": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "tool_call" + } + }, + "required": ["type"] + } + ] + }, + "additionalProperties": true + } + ], + "discriminator": { + "propertyName": "type" + } + }, "ToolCallUpdate": { "description": "Represents an upsert for a tool call that the language model has requested.\n\nTool calls are actions that the agent executes on behalf of the language model,\nsuch as reading files, executing code, or fetching data from external sources.\n\nOnly [`ToolCallUpdate::tool_call_id`] is required. Other fields have patch semantics:\nomitted fields leave the existing tool call value unchanged, `null` clears or\nunsets the value, and concrete values replace the previous value. For\ncollection fields, concrete arrays replace the previous collection, and both\n`null` and `[]` clear the collection. When a client receives a tool call ID it\nhas not seen before, omitted fields use client defaults.\n\nSee protocol docs: [Tool Calls](https://agentclientprotocol.com/protocol/v2/draft/tool-calls)", "type": "object", @@ -1508,6 +1572,21 @@ }, "required": ["path"] }, + "ToolCallPermissionSubject": { + "description": "Permission request details for a tool call.", + "type": "object", + "properties": { + "toolCall": { + "description": "Details about the tool call requiring permission.", + "allOf": [ + { + "$ref": "#/$defs/ToolCallUpdate" + } + ] + } + }, + "required": ["toolCall"] + }, "PermissionOption": { "description": "An option presented to the user when requesting permission.", "type": "object",