Skip to content

Commit e051528

Browse files
zenshclaude
andcommitted
feat(mcp): add delegation tools to the local connector
The local connector spec predates Agent Delegation, so its 27 tools had no way to resolve a principal or check a credential even though the connector is the only component holding a signing key. Adds five tools and ports them to the Rust and TypeScript connectors. - agent_protocols_principal_resolve runs Section 5.3 resolution and reports `alias` only for a URL the resolved document acknowledges. - agent_protocols_delegation_check resolves the principal and queries the delegation_query_url that document names, defaulting `subject` to the active Agent ID. Reads stay anchored on the principal rather than on a service URL the caller was handed. - agent_protocols_delegations_list enumerates only the active Agent ID's own delegations, authorized by an Agent Identity request JWT. - agent_protocols_delegation_grant/_revoke resolve the principal, substitute the canonical id, and refuse to sign unless the active Agent ID is listed in controllers, so an unacceptable envelope is never signed or sent. DelegationClient gains query_delegations_at for the principal-named endpoint, and query_delegations now takes a request JWT instead of an enumeration flag: authorization and enumeration are the same condition, so a public query that omits either key cannot leave the client. Versions bumped to 0.7.0 for the client signature change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 3d06909 commit e051528

19 files changed

Lines changed: 711 additions & 44 deletions

File tree

‎crates/agent-protocols/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "agent-protocols"
3-
version = "0.6.0"
3+
version = "0.7.0"
44
edition.workspace = true
55
license.workspace = true
66
repository.workspace = true

‎crates/agent-protocols/src/http_client.rs‎

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -556,23 +556,33 @@ impl DelegationClient {
556556
}
557557

558558
/// Public queries are existence checks and carry both `subject` and
559-
/// `principal_id`. Pass `allow_enumeration` only for a request the service
560-
/// has authorized to enumerate one side.
559+
/// `principal_id`. Passing a request JWT authorizes an enumeration query,
560+
/// which a service must otherwise refuse.
561561
pub async fn query_delegations(
562562
&self,
563563
request: &DelegationQueryRequest,
564-
allow_enumeration: bool,
564+
jwt: Option<&str>,
565565
) -> Result<DelegationQueryResponse> {
566-
crate::delegation::validate_delegation_query_request(request, allow_enumeration)?;
567-
Ok(self
568-
.inner
569-
.post(self.url("/v1/delegations/query"))
570-
.json(request)
571-
.send()
572-
.await?
573-
.error_for_status()?
574-
.json()
575-
.await?)
566+
self.query_delegations_at(&self.url("/v1/delegations/query"), request, jwt)
567+
.await
568+
}
569+
570+
/// Queries the endpoint a principal document names in its
571+
/// `delegation_query_url`. That is how a relying party reaches the
572+
/// authoritative service for a principal without trusting a URL supplied
573+
/// by whoever presented the credential.
574+
pub async fn query_delegations_at(
575+
&self,
576+
query_url: &str,
577+
request: &DelegationQueryRequest,
578+
jwt: Option<&str>,
579+
) -> Result<DelegationQueryResponse> {
580+
crate::delegation::validate_delegation_query_request(request, jwt.is_some())?;
581+
let mut builder = self.inner.post(query_url).json(request);
582+
if let Some(jwt) = jwt {
583+
builder = builder.bearer_auth(jwt);
584+
}
585+
Ok(builder.send().await?.error_for_status()?.json().await?)
576586
}
577587

578588
fn url(&self, path: &str) -> String {

‎crates/agent-protocols/src/local_connector.rs‎

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,21 @@ use serde::Serialize;
4444
use serde_json::{json, Value};
4545
use std::collections::BTreeMap;
4646

47+
use crate::delegation::{
48+
delegation_grant_event, delegation_revoke_event, is_principal_alias,
49+
validate_delegation_grant_payload, DelegationGrantPayload, DelegationQueryRequest,
50+
DelegationRevokePayload, PrincipalDescriptor, PrincipalDocument,
51+
};
4752
use crate::discourse::{
4853
discourse_event, event_type, room_create_event, validate_discourse_envelope,
4954
validate_room_path, verify_server_record, AgentStatusInput, JoinRequestStatus,
5055
MessageCreatePayload, ReasonPayload, Role, RoomCreatePayload, RoomJoinPayload,
5156
RoomJoinRequestInput, RoomJoinReviewPayload, RoomResponse, ServerRecord, Visibility,
5257
};
5358
use crate::error::{Result, SdkError};
54-
use crate::http_client::{DiscourseClient, ProfileClient, PublicRoomsOptions, RoomEventsOptions};
59+
use crate::http_client::{
60+
DelegationClient, DiscourseClient, ProfileClient, PublicRoomsOptions, RoomEventsOptions,
61+
};
5562
use crate::identity::{
5663
unix_ms, unix_secs, AgentId, AgentSigner, ClientNonceManager, Envelope, RequestBinding,
5764
RequestJwtClaims, DEFAULT_REQUEST_JWT_TTL_SECS,
@@ -220,6 +227,11 @@ impl LocalConnector {
220227
match name {
221228
TOOL_IDENTITY_CURRENT => self.identity_current(),
222229
TOOL_HOSTS_LIST => self.hosts_list(),
230+
TOOL_PRINCIPAL_RESOLVE => self.principal_resolve(parse_input(input)?).await,
231+
TOOL_DELEGATION_CHECK => self.delegation_check(parse_input(input)?).await,
232+
TOOL_DELEGATIONS_LIST => self.delegations_list(parse_input(input)?).await,
233+
TOOL_DELEGATION_GRANT => self.delegation_grant(parse_input(input)?).await,
234+
TOOL_DELEGATION_REVOKE => self.delegation_revoke(parse_input(input)?).await,
223235
TOOL_ROOMS_SEARCH => self.rooms_search(parse_input(input)?).await,
224236
TOOL_ROOMS_LIST => self.rooms_list(parse_input(input)?),
225237
TOOL_ROOM_OPEN => self.room_open(parse_input(input)?).await,
@@ -875,6 +887,132 @@ impl LocalConnector {
875887
}))
876888
}
877889

890+
/// Resolves a principal per Agent Delegation Section 5.3 and reports
891+
/// whether the requested URL is an alias the principal acknowledges. Any
892+
/// origin can redirect to any principal, so an unlisted URL is never
893+
/// presented as a name for it.
894+
async fn resolve_principal(&self, url: &str) -> Result<(PrincipalDocument, bool)> {
895+
let document = DelegationClient::new(url).principal(Some(url)).await?;
896+
let alias = document.id != url && is_principal_alias(&document, url);
897+
Ok((document, alias))
898+
}
899+
900+
/// Resolves the principal and refuses when the active Agent ID is not one
901+
/// of its controller keys, so a grant that could never be accepted is not
902+
/// signed or transmitted.
903+
async fn controller_principal(&self, principal_id: &str) -> Result<PrincipalDocument> {
904+
let (document, _) = self.resolve_principal(principal_id).await?;
905+
let active = self.agent_id();
906+
if !document.controllers.contains(&active) {
907+
return Err(SdkError::InvalidPayload(format!(
908+
"active agent {active} is not a controller key of {}",
909+
document.id
910+
)));
911+
}
912+
Ok(document)
913+
}
914+
915+
async fn principal_resolve(&self, input: PrincipalResolveInput) -> Result<Value> {
916+
let (document, alias) = self.resolve_principal(&input.url).await?;
917+
json_result(json!({
918+
"canonical_id": document.id,
919+
"requested_url": input.url,
920+
"alias": alias,
921+
"principal": document,
922+
}))
923+
}
924+
925+
async fn delegation_check(&self, input: DelegationCheckInput) -> Result<Value> {
926+
let (document, _) = self.resolve_principal(&input.principal_id).await?;
927+
// The authoritative service is the one the principal names, never one
928+
// supplied by whoever presented a credential.
929+
let query_url = document.delegation_query_url.clone().ok_or_else(|| {
930+
SdkError::InvalidPayload(format!(
931+
"principal {} publishes no delegation_query_url",
932+
document.id
933+
))
934+
})?;
935+
let request = DelegationQueryRequest {
936+
subject: Some(input.subject.unwrap_or_else(|| self.agent_id())),
937+
principal_id: Some(document.id.clone()),
938+
id: input.id,
939+
status: input.status,
940+
limit: None,
941+
};
942+
let response = DelegationClient::new(&query_url)
943+
.query_delegations_at(&query_url, &request, None)
944+
.await?;
945+
json_result(json!({
946+
"canonical_id": document.id,
947+
"query_url": query_url,
948+
"delegations": response.result,
949+
}))
950+
}
951+
952+
async fn delegations_list(&self, input: DelegationsListInput) -> Result<Value> {
953+
// Enumerating one subject requires authorization; the connector proves
954+
// the active identity and never enumerates anyone else.
955+
let jwt = self.request_jwt(&input.delegation_service)?;
956+
let request = DelegationQueryRequest {
957+
subject: Some(self.agent_id()),
958+
principal_id: None,
959+
id: None,
960+
status: input.status,
961+
limit: input.limit,
962+
};
963+
let response = DelegationClient::new(&input.delegation_service)
964+
.query_delegations(&request, Some(&jwt))
965+
.await?;
966+
json_result(json!({ "delegations": response.result }))
967+
}
968+
969+
async fn delegation_grant(&mut self, input: DelegationGrantInput) -> Result<Value> {
970+
let principal = self.controller_principal(&input.principal_id).await?;
971+
let mut payload = DelegationGrantPayload::new(
972+
input.id,
973+
PrincipalDescriptor::new(principal.id),
974+
input.subject,
975+
input.scopes,
976+
);
977+
payload.relationship = input.relationship;
978+
payload.constraints = input.constraints;
979+
payload.not_before = input.not_before;
980+
payload.expires_at = input.expires_at;
981+
let created_at = unix_ms();
982+
validate_delegation_grant_payload(&payload, Some(created_at))?;
983+
let event = delegation_grant_event(
984+
self.agent_id(),
985+
created_at,
986+
self.nonce_manager.next_nonce()?,
987+
payload,
988+
);
989+
let envelope = self.signer.sign_event(event)?;
990+
let credential = DelegationClient::new(&input.delegation_service)
991+
.submit_delegation_event(&envelope)
992+
.await?;
993+
json_result(json!({ "credential": credential, "envelope": envelope }))
994+
}
995+
996+
async fn delegation_revoke(&mut self, input: DelegationRevokeInput) -> Result<Value> {
997+
let principal = self.controller_principal(&input.principal_id).await?;
998+
let payload = DelegationRevokePayload {
999+
id: input.id,
1000+
principal_id: principal.id,
1001+
reason: input.reason,
1002+
};
1003+
let event = delegation_revoke_event(
1004+
self.agent_id(),
1005+
unix_ms(),
1006+
self.nonce_manager.next_nonce()?,
1007+
payload,
1008+
);
1009+
let envelope = self.signer.sign_event(event)?;
1010+
let result = DelegationClient::new(&input.delegation_service)
1011+
.submit_delegation_event(&envelope)
1012+
.await?;
1013+
json_result(json!({ "result": result, "envelope": envelope }))
1014+
}
1015+
8781016
async fn profile_update(&mut self, input: ProfileUpdateInput) -> Result<Value> {
8791017
let mut profile = input.profile;
8801018
let object = profile

‎crates/agent-protocols/src/local_connector/catalog.rs‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ use serde_json::{json, Value};
88

99
pub const TOOL_IDENTITY_CURRENT: &str = "agent_protocols_identity_current";
1010
pub const TOOL_HOSTS_LIST: &str = "agent_protocols_hosts_list";
11+
pub const TOOL_PRINCIPAL_RESOLVE: &str = "agent_protocols_principal_resolve";
12+
pub const TOOL_DELEGATION_CHECK: &str = "agent_protocols_delegation_check";
13+
pub const TOOL_DELEGATIONS_LIST: &str = "agent_protocols_delegations_list";
14+
pub const TOOL_DELEGATION_GRANT: &str = "agent_protocols_delegation_grant";
15+
pub const TOOL_DELEGATION_REVOKE: &str = "agent_protocols_delegation_revoke";
1116
pub const TOOL_ROOMS_SEARCH: &str = "agent_protocols_rooms_search";
1217
pub const TOOL_ROOMS_LIST: &str = "agent_protocols_rooms_list";
1318
pub const TOOL_ROOM_OPEN: &str = "agent_protocols_room_open";
@@ -79,6 +84,41 @@ pub fn standard_tool_definitions() -> Vec<LocalConnectorToolDefinition> {
7984
true,
8085
false,
8186
),
87+
(
88+
TOOL_PRINCIPAL_RESOLVE,
89+
"Resolve a principal URL to its canonical identifier and controller keys.",
90+
true,
91+
true,
92+
true,
93+
),
94+
(
95+
TOOL_DELEGATION_CHECK,
96+
"Check whether an agent holds a delegation from a principal.",
97+
true,
98+
true,
99+
true,
100+
),
101+
(
102+
TOOL_DELEGATIONS_LIST,
103+
"List the active local agent's own delegation credentials.",
104+
true,
105+
true,
106+
true,
107+
),
108+
(
109+
TOOL_DELEGATION_GRANT,
110+
"Sign and submit delegation.grant as a controller key of the principal.",
111+
false,
112+
false,
113+
true,
114+
),
115+
(
116+
TOOL_DELEGATION_REVOKE,
117+
"Sign and submit delegation.revoke as a controller key of the principal.",
118+
false,
119+
false,
120+
true,
121+
),
82122
(
83123
TOOL_ROOMS_SEARCH,
84124
"Search public rooms on an allowed host.",

‎crates/agent-protocols/src/local_connector/inputs.rs‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,63 @@ use serde::Deserialize;
66
use serde_json::Value;
77
use std::collections::BTreeMap;
88

9+
use crate::delegation::DelegationStatus;
910
use crate::discourse::{JoinDecision, Role, TypeDeclaration, Visibility};
1011
use crate::identity::AgentId;
1112

1213
use super::views::{DraftAction, HeadMismatchPolicy, RoomMemberStatus};
1314

15+
#[derive(Deserialize)]
16+
pub(crate) struct PrincipalResolveInput {
17+
pub url: String,
18+
}
19+
20+
#[derive(Deserialize)]
21+
pub(crate) struct DelegationCheckInput {
22+
pub principal_id: String,
23+
#[serde(default)]
24+
pub subject: Option<AgentId>,
25+
#[serde(default)]
26+
pub id: Option<String>,
27+
#[serde(default)]
28+
pub status: Option<DelegationStatus>,
29+
}
30+
31+
#[derive(Deserialize)]
32+
pub(crate) struct DelegationsListInput {
33+
pub delegation_service: String,
34+
#[serde(default)]
35+
pub status: Option<DelegationStatus>,
36+
#[serde(default)]
37+
pub limit: Option<u32>,
38+
}
39+
40+
#[derive(Deserialize)]
41+
pub(crate) struct DelegationGrantInput {
42+
pub delegation_service: String,
43+
pub id: String,
44+
pub principal_id: String,
45+
pub subject: AgentId,
46+
#[serde(default)]
47+
pub relationship: Option<String>,
48+
pub scopes: Vec<String>,
49+
#[serde(default)]
50+
pub constraints: BTreeMap<String, Value>,
51+
#[serde(default)]
52+
pub not_before: Option<i64>,
53+
#[serde(default)]
54+
pub expires_at: Option<i64>,
55+
}
56+
57+
#[derive(Deserialize)]
58+
pub(crate) struct DelegationRevokeInput {
59+
pub delegation_service: String,
60+
pub id: String,
61+
pub principal_id: String,
62+
#[serde(default)]
63+
pub reason: Option<String>,
64+
}
65+
1466
#[derive(Deserialize)]
1567
pub(crate) struct RoomsSearchInput {
1668
pub host: String,

0 commit comments

Comments
 (0)