From 17bd3a54251b4e23da82bb25d490e11ee89130b3 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Wed, 14 Oct 2026 08:26:02 +0000 Subject: [PATCH 1/7] router: keep the directory's records itself, and let a member withdraw them The router kept members' records in kad-dht's provider store and ran lookups as kad queries. That store cannot forget a record before its TTL, merges every announced address into the peerstore for the TTL, and cannot be replaced (kad-dht does not take a custom ProviderStore), so a service a node stopped stayed discoverable for 15 minutes and a circuit through a router the node left lingered as long. The router now keeps records in a store of its own, by key and provider, each with the addresses of the last announcement and an expiry at the mesh's record TTL; announcing again replaces the addresses, a sweep drops what expired. The DHT is left with what the directory needs from it: the routers' table, from which the routers closest to a key are read. A lookup from a member merges this router's records with the answers of those routers, asked at once over the directory protocol, each within five seconds; a lookup from a peer router is answered from this router's records alone, so a lookup is one hop. A provider the router holds is answered at the addresses it sees for it. DirectoryWithdraw (api/agentmesh.proto) removes a record the same way a provide stores one: from the router asked and from the routers closest to the key, with the provider named only between routers. A node withdraws a service's records when it unregisters it (the record by type stays while another service of the type is registered) and every record, its own included, when it tears down. Metrics: agentmesh_router_directory_records, _records_expired_total, _lookups_total{result}; op labels withdraw and lookup. Bindings regenerated; go-cid is no longer imported directly. --- api/agentmesh.pb.go | 779 ++++++++++-------- api/agentmesh.proto | 26 +- go.mod | 2 +- internal/node/directory_client.go | 17 +- internal/node/directory_client_test.go | 2 + internal/node/node.go | 17 +- internal/node/service_registry.go | 60 +- internal/node/service_registry_test.go | 69 +- internal/router/dht.go | 25 +- internal/router/directory.go | 320 +++++-- internal/router/directory_store.go | 152 ++++ internal/router/directory_test.go | 116 ++- internal/router/metrics.go | 8 +- internal/router/metrics_test.go | 3 + internal/router/router.go | 12 +- sdk/README.md | 6 +- sdk/js/src/gen/agentmesh_pb.ts | 169 ++-- .../src/agent_mesh/_proto/agentmesh_pb2.py | 266 +++--- .../src/agent_mesh/_proto/agentmesh_pb2.pyi | 14 +- site/content/docs/concepts/networking.md | 22 +- site/content/docs/guides/scalability.md | 26 +- site/content/docs/reference/router.md | 17 +- tests/integration/minimal_helpers_test.go | 2 + tests/integration/router_attachment_test.go | 18 +- 24 files changed, 1438 insertions(+), 710 deletions(-) create mode 100644 internal/router/directory_store.go diff --git a/api/agentmesh.pb.go b/api/agentmesh.pb.go index 0a42d421..91ddad18 100644 --- a/api/agentmesh.pb.go +++ b/api/agentmesh.pb.go @@ -384,7 +384,7 @@ func (x ExtProcProcessingMode_HeaderMode) Number() protoreflect.EnumNumber { // Deprecated: Use ExtProcProcessingMode_HeaderMode.Descriptor instead. func (ExtProcProcessingMode_HeaderMode) EnumDescriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{30, 0} + return file_api_agentmesh_proto_rawDescGZIP(), []int{31, 0} } type ExtProcProcessingMode_BodyMode int32 @@ -439,7 +439,7 @@ func (x ExtProcProcessingMode_BodyMode) Number() protoreflect.EnumNumber { // Deprecated: Use ExtProcProcessingMode_BodyMode.Descriptor instead. func (ExtProcProcessingMode_BodyMode) EnumDescriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{30, 1} + return file_api_agentmesh_proto_rawDescGZIP(), []int{31, 1} } type AuthFrame struct { @@ -1520,9 +1520,11 @@ func (x *DirectoryParameters) GetRecordTtl() *durationpb.Duration { // DirectoryRequest is what a member asks a router it is attached to over // /mesh/directory/1.0.0: one request per stream, answered with one -// DirectoryResponse. The member keeps no DHT of its own; the router runs -// the lookup across the routers' DHT and answers from it. A router sends -// the same message to another router to place a member's record on it. +// DirectoryResponse. The member keeps no DHT of its own; the router keeps +// its records, places them on the routers closest to each key, and answers +// a lookup from its own records and those routers'. A router sends the +// same message to another router to place a member's record, or its +// withdrawal, on it. type DirectoryRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Request: @@ -1530,6 +1532,7 @@ type DirectoryRequest struct { // *DirectoryRequest_Provide // *DirectoryRequest_FindProviders // *DirectoryRequest_FindPeer + // *DirectoryRequest_Withdraw Request isDirectoryRequest_Request `protobuf_oneof:"request"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1599,6 +1602,15 @@ func (x *DirectoryRequest) GetFindPeer() *DirectoryFindPeer { return nil } +func (x *DirectoryRequest) GetWithdraw() *DirectoryWithdraw { + if x != nil { + if x, ok := x.Request.(*DirectoryRequest_Withdraw); ok { + return x.Withdraw + } + } + return nil +} + type isDirectoryRequest_Request interface { isDirectoryRequest_Request() } @@ -1615,17 +1627,25 @@ type DirectoryRequest_FindPeer struct { FindPeer *DirectoryFindPeer `protobuf:"bytes,3,opt,name=find_peer,json=findPeer,proto3,oneof"` } +type DirectoryRequest_Withdraw struct { + Withdraw *DirectoryWithdraw `protobuf:"bytes,4,opt,name=withdraw,proto3,oneof"` +} + func (*DirectoryRequest_Provide) isDirectoryRequest_Request() {} func (*DirectoryRequest_FindProviders) isDirectoryRequest_Request() {} func (*DirectoryRequest_FindPeer) isDirectoryRequest_Request() {} +func (*DirectoryRequest_Withdraw) isDirectoryRequest_Request() {} + // DirectoryProvide announces the sender as a provider of key, reached at // addrs. A key is a sha256 multihash: of "mesh:service:[:]" for -// a service, of "mesh:peer:" for the member itself. provider is set -// only between routers, by the router that holds the member, and refused -// from anyone else: a member can only announce itself. +// a service, of "mesh:peer:" for the member itself. A record lives +// for the mesh's record TTL after its last announcement; announcing it +// again replaces its addresses. provider is set only between routers, by +// the router that holds the member, and refused from anyone else: a member +// can only announce itself. type DirectoryProvide struct { state protoimpl.MessageState `protogen:"open.v1"` Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` @@ -1686,6 +1706,62 @@ func (x *DirectoryProvide) GetProvider() string { return "" } +// DirectoryWithdraw removes the sender's record under key, from the router +// asked and from the routers closest to the key: what a member sends when +// it stops a service or leaves the mesh, so that callers do not find it +// until the record would have expired. provider is as in DirectoryProvide. +type DirectoryWithdraw struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Provider string `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DirectoryWithdraw) Reset() { + *x = DirectoryWithdraw{} + mi := &file_api_agentmesh_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DirectoryWithdraw) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DirectoryWithdraw) ProtoMessage() {} + +func (x *DirectoryWithdraw) ProtoReflect() protoreflect.Message { + mi := &file_api_agentmesh_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DirectoryWithdraw.ProtoReflect.Descriptor instead. +func (*DirectoryWithdraw) Descriptor() ([]byte, []int) { + return file_api_agentmesh_proto_rawDescGZIP(), []int{16} +} + +func (x *DirectoryWithdraw) GetKey() []byte { + if x != nil { + return x.Key + } + return nil +} + +func (x *DirectoryWithdraw) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + // DirectoryFindProviders asks for up to limit providers of key. type DirectoryFindProviders struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1697,7 +1773,7 @@ type DirectoryFindProviders struct { func (x *DirectoryFindProviders) Reset() { *x = DirectoryFindProviders{} - mi := &file_api_agentmesh_proto_msgTypes[16] + mi := &file_api_agentmesh_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1709,7 +1785,7 @@ func (x *DirectoryFindProviders) String() string { func (*DirectoryFindProviders) ProtoMessage() {} func (x *DirectoryFindProviders) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[16] + mi := &file_api_agentmesh_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1722,7 +1798,7 @@ func (x *DirectoryFindProviders) ProtoReflect() protoreflect.Message { // Deprecated: Use DirectoryFindProviders.ProtoReflect.Descriptor instead. func (*DirectoryFindProviders) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{16} + return file_api_agentmesh_proto_rawDescGZIP(), []int{17} } func (x *DirectoryFindProviders) GetKey() []byte { @@ -1749,7 +1825,7 @@ type DirectoryFindPeer struct { func (x *DirectoryFindPeer) Reset() { *x = DirectoryFindPeer{} - mi := &file_api_agentmesh_proto_msgTypes[17] + mi := &file_api_agentmesh_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1761,7 +1837,7 @@ func (x *DirectoryFindPeer) String() string { func (*DirectoryFindPeer) ProtoMessage() {} func (x *DirectoryFindPeer) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[17] + mi := &file_api_agentmesh_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1774,7 +1850,7 @@ func (x *DirectoryFindPeer) ProtoReflect() protoreflect.Message { // Deprecated: Use DirectoryFindPeer.ProtoReflect.Descriptor instead. func (*DirectoryFindPeer) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{17} + return file_api_agentmesh_proto_rawDescGZIP(), []int{18} } func (x *DirectoryFindPeer) GetPeerId() string { @@ -1798,7 +1874,7 @@ type DirectoryResponse struct { func (x *DirectoryResponse) Reset() { *x = DirectoryResponse{} - mi := &file_api_agentmesh_proto_msgTypes[18] + mi := &file_api_agentmesh_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1810,7 +1886,7 @@ func (x *DirectoryResponse) String() string { func (*DirectoryResponse) ProtoMessage() {} func (x *DirectoryResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[18] + mi := &file_api_agentmesh_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1823,7 +1899,7 @@ func (x *DirectoryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DirectoryResponse.ProtoReflect.Descriptor instead. func (*DirectoryResponse) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{18} + return file_api_agentmesh_proto_rawDescGZIP(), []int{19} } func (x *DirectoryResponse) GetProviders() []*PeerRecord { @@ -1859,7 +1935,7 @@ type PeerRecord struct { func (x *PeerRecord) Reset() { *x = PeerRecord{} - mi := &file_api_agentmesh_proto_msgTypes[19] + mi := &file_api_agentmesh_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1871,7 +1947,7 @@ func (x *PeerRecord) String() string { func (*PeerRecord) ProtoMessage() {} func (x *PeerRecord) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[19] + mi := &file_api_agentmesh_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1884,7 +1960,7 @@ func (x *PeerRecord) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerRecord.ProtoReflect.Descriptor instead. func (*PeerRecord) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{19} + return file_api_agentmesh_proto_rawDescGZIP(), []int{20} } func (x *PeerRecord) GetPeerId() string { @@ -1926,7 +2002,7 @@ type RouterInfo struct { func (x *RouterInfo) Reset() { *x = RouterInfo{} - mi := &file_api_agentmesh_proto_msgTypes[20] + mi := &file_api_agentmesh_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1938,7 +2014,7 @@ func (x *RouterInfo) String() string { func (*RouterInfo) ProtoMessage() {} func (x *RouterInfo) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[20] + mi := &file_api_agentmesh_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1951,7 +2027,7 @@ func (x *RouterInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use RouterInfo.ProtoReflect.Descriptor instead. func (*RouterInfo) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{20} + return file_api_agentmesh_proto_rawDescGZIP(), []int{21} } func (x *RouterInfo) GetPeerId() string { @@ -2019,7 +2095,7 @@ type RouterLeaseRequest struct { func (x *RouterLeaseRequest) Reset() { *x = RouterLeaseRequest{} - mi := &file_api_agentmesh_proto_msgTypes[21] + mi := &file_api_agentmesh_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2031,7 +2107,7 @@ func (x *RouterLeaseRequest) String() string { func (*RouterLeaseRequest) ProtoMessage() {} func (x *RouterLeaseRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[21] + mi := &file_api_agentmesh_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2044,7 +2120,7 @@ func (x *RouterLeaseRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RouterLeaseRequest.ProtoReflect.Descriptor instead. func (*RouterLeaseRequest) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{21} + return file_api_agentmesh_proto_rawDescGZIP(), []int{22} } func (x *RouterLeaseRequest) GetPeerId() string { @@ -2128,7 +2204,7 @@ type RouterLeaseResponse struct { func (x *RouterLeaseResponse) Reset() { *x = RouterLeaseResponse{} - mi := &file_api_agentmesh_proto_msgTypes[22] + mi := &file_api_agentmesh_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2140,7 +2216,7 @@ func (x *RouterLeaseResponse) String() string { func (*RouterLeaseResponse) ProtoMessage() {} func (x *RouterLeaseResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[22] + mi := &file_api_agentmesh_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2153,7 +2229,7 @@ func (x *RouterLeaseResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RouterLeaseResponse.ProtoReflect.Descriptor instead. func (*RouterLeaseResponse) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{22} + return file_api_agentmesh_proto_rawDescGZIP(), []int{23} } func (x *RouterLeaseResponse) GetSuccess() bool { @@ -2195,7 +2271,7 @@ type PolicyRole struct { func (x *PolicyRole) Reset() { *x = PolicyRole{} - mi := &file_api_agentmesh_proto_msgTypes[23] + mi := &file_api_agentmesh_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2207,7 +2283,7 @@ func (x *PolicyRole) String() string { func (*PolicyRole) ProtoMessage() {} func (x *PolicyRole) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[23] + mi := &file_api_agentmesh_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2220,7 +2296,7 @@ func (x *PolicyRole) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyRole.ProtoReflect.Descriptor instead. func (*PolicyRole) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{23} + return file_api_agentmesh_proto_rawDescGZIP(), []int{24} } func (x *PolicyRole) GetName() string { @@ -2287,7 +2363,7 @@ type HTTPGrant struct { func (x *HTTPGrant) Reset() { *x = HTTPGrant{} - mi := &file_api_agentmesh_proto_msgTypes[24] + mi := &file_api_agentmesh_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2299,7 +2375,7 @@ func (x *HTTPGrant) String() string { func (*HTTPGrant) ProtoMessage() {} func (x *HTTPGrant) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[24] + mi := &file_api_agentmesh_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2312,7 +2388,7 @@ func (x *HTTPGrant) ProtoReflect() protoreflect.Message { // Deprecated: Use HTTPGrant.ProtoReflect.Descriptor instead. func (*HTTPGrant) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{24} + return file_api_agentmesh_proto_rawDescGZIP(), []int{25} } func (x *HTTPGrant) GetService() string { @@ -2380,7 +2456,7 @@ type EgressDestination struct { func (x *EgressDestination) Reset() { *x = EgressDestination{} - mi := &file_api_agentmesh_proto_msgTypes[25] + mi := &file_api_agentmesh_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2392,7 +2468,7 @@ func (x *EgressDestination) String() string { func (*EgressDestination) ProtoMessage() {} func (x *EgressDestination) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[25] + mi := &file_api_agentmesh_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2405,7 +2481,7 @@ func (x *EgressDestination) ProtoReflect() protoreflect.Message { // Deprecated: Use EgressDestination.ProtoReflect.Descriptor instead. func (*EgressDestination) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{25} + return file_api_agentmesh_proto_rawDescGZIP(), []int{26} } func (x *EgressDestination) GetName() string { @@ -2490,7 +2566,7 @@ type Inspection struct { func (x *Inspection) Reset() { *x = Inspection{} - mi := &file_api_agentmesh_proto_msgTypes[26] + mi := &file_api_agentmesh_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2502,7 +2578,7 @@ func (x *Inspection) String() string { func (*Inspection) ProtoMessage() {} func (x *Inspection) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[26] + mi := &file_api_agentmesh_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2515,7 +2591,7 @@ func (x *Inspection) ProtoReflect() protoreflect.Message { // Deprecated: Use Inspection.ProtoReflect.Descriptor instead. func (*Inspection) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{26} + return file_api_agentmesh_proto_rawDescGZIP(), []int{27} } func (x *Inspection) GetInspectors() []*Inspector { @@ -2538,7 +2614,7 @@ type Inspector struct { func (x *Inspector) Reset() { *x = Inspector{} - mi := &file_api_agentmesh_proto_msgTypes[27] + mi := &file_api_agentmesh_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2550,7 +2626,7 @@ func (x *Inspector) String() string { func (*Inspector) ProtoMessage() {} func (x *Inspector) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[27] + mi := &file_api_agentmesh_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2563,7 +2639,7 @@ func (x *Inspector) ProtoReflect() protoreflect.Message { // Deprecated: Use Inspector.ProtoReflect.Descriptor instead. func (*Inspector) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{27} + return file_api_agentmesh_proto_rawDescGZIP(), []int{28} } func (x *Inspector) GetKind() isInspector_Kind { @@ -2628,7 +2704,7 @@ type ModelArmor struct { func (x *ModelArmor) Reset() { *x = ModelArmor{} - mi := &file_api_agentmesh_proto_msgTypes[28] + mi := &file_api_agentmesh_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2640,7 +2716,7 @@ func (x *ModelArmor) String() string { func (*ModelArmor) ProtoMessage() {} func (x *ModelArmor) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[28] + mi := &file_api_agentmesh_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2653,7 +2729,7 @@ func (x *ModelArmor) ProtoReflect() protoreflect.Message { // Deprecated: Use ModelArmor.ProtoReflect.Descriptor instead. func (*ModelArmor) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{28} + return file_api_agentmesh_proto_rawDescGZIP(), []int{29} } func (x *ModelArmor) GetTemplate() string { @@ -2711,7 +2787,7 @@ type ExtProc struct { func (x *ExtProc) Reset() { *x = ExtProc{} - mi := &file_api_agentmesh_proto_msgTypes[29] + mi := &file_api_agentmesh_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2723,7 +2799,7 @@ func (x *ExtProc) String() string { func (*ExtProc) ProtoMessage() {} func (x *ExtProc) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[29] + mi := &file_api_agentmesh_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2736,7 +2812,7 @@ func (x *ExtProc) ProtoReflect() protoreflect.Message { // Deprecated: Use ExtProc.ProtoReflect.Descriptor instead. func (*ExtProc) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{29} + return file_api_agentmesh_proto_rawDescGZIP(), []int{30} } func (x *ExtProc) GetTarget() string { @@ -2809,7 +2885,7 @@ type ExtProcProcessingMode struct { func (x *ExtProcProcessingMode) Reset() { *x = ExtProcProcessingMode{} - mi := &file_api_agentmesh_proto_msgTypes[30] + mi := &file_api_agentmesh_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2821,7 +2897,7 @@ func (x *ExtProcProcessingMode) String() string { func (*ExtProcProcessingMode) ProtoMessage() {} func (x *ExtProcProcessingMode) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[30] + mi := &file_api_agentmesh_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2834,7 +2910,7 @@ func (x *ExtProcProcessingMode) ProtoReflect() protoreflect.Message { // Deprecated: Use ExtProcProcessingMode.ProtoReflect.Descriptor instead. func (*ExtProcProcessingMode) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{30} + return file_api_agentmesh_proto_rawDescGZIP(), []int{31} } func (x *ExtProcProcessingMode) GetRequestHeaderMode() ExtProcProcessingMode_HeaderMode { @@ -2894,7 +2970,7 @@ type CredentialBroker struct { func (x *CredentialBroker) Reset() { *x = CredentialBroker{} - mi := &file_api_agentmesh_proto_msgTypes[31] + mi := &file_api_agentmesh_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2906,7 +2982,7 @@ func (x *CredentialBroker) String() string { func (*CredentialBroker) ProtoMessage() {} func (x *CredentialBroker) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[31] + mi := &file_api_agentmesh_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2919,7 +2995,7 @@ func (x *CredentialBroker) ProtoReflect() protoreflect.Message { // Deprecated: Use CredentialBroker.ProtoReflect.Descriptor instead. func (*CredentialBroker) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{31} + return file_api_agentmesh_proto_rawDescGZIP(), []int{32} } func (x *CredentialBroker) GetKind() isCredentialBroker_Kind { @@ -3016,7 +3092,7 @@ type OIDCFederation struct { func (x *OIDCFederation) Reset() { *x = OIDCFederation{} - mi := &file_api_agentmesh_proto_msgTypes[32] + mi := &file_api_agentmesh_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3028,7 +3104,7 @@ func (x *OIDCFederation) String() string { func (*OIDCFederation) ProtoMessage() {} func (x *OIDCFederation) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[32] + mi := &file_api_agentmesh_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3041,7 +3117,7 @@ func (x *OIDCFederation) ProtoReflect() protoreflect.Message { // Deprecated: Use OIDCFederation.ProtoReflect.Descriptor instead. func (*OIDCFederation) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{32} + return file_api_agentmesh_proto_rawDescGZIP(), []int{33} } func (x *OIDCFederation) GetTokenEndpoint() string { @@ -3083,7 +3159,7 @@ type AWSAssumeRole struct { func (x *AWSAssumeRole) Reset() { *x = AWSAssumeRole{} - mi := &file_api_agentmesh_proto_msgTypes[33] + mi := &file_api_agentmesh_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3095,7 +3171,7 @@ func (x *AWSAssumeRole) String() string { func (*AWSAssumeRole) ProtoMessage() {} func (x *AWSAssumeRole) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[33] + mi := &file_api_agentmesh_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3108,7 +3184,7 @@ func (x *AWSAssumeRole) ProtoReflect() protoreflect.Message { // Deprecated: Use AWSAssumeRole.ProtoReflect.Descriptor instead. func (*AWSAssumeRole) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{33} + return file_api_agentmesh_proto_rawDescGZIP(), []int{34} } func (x *AWSAssumeRole) GetRoleArn() string { @@ -3134,7 +3210,7 @@ type PlatformIdentity struct { func (x *PlatformIdentity) Reset() { *x = PlatformIdentity{} - mi := &file_api_agentmesh_proto_msgTypes[34] + mi := &file_api_agentmesh_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3146,7 +3222,7 @@ func (x *PlatformIdentity) String() string { func (*PlatformIdentity) ProtoMessage() {} func (x *PlatformIdentity) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[34] + mi := &file_api_agentmesh_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3159,7 +3235,7 @@ func (x *PlatformIdentity) ProtoReflect() protoreflect.Message { // Deprecated: Use PlatformIdentity.ProtoReflect.Descriptor instead. func (*PlatformIdentity) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{34} + return file_api_agentmesh_proto_rawDescGZIP(), []int{35} } func (x *PlatformIdentity) GetScopes() []string { @@ -3179,7 +3255,7 @@ type PolicyBinding struct { func (x *PolicyBinding) Reset() { *x = PolicyBinding{} - mi := &file_api_agentmesh_proto_msgTypes[35] + mi := &file_api_agentmesh_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3191,7 +3267,7 @@ func (x *PolicyBinding) String() string { func (*PolicyBinding) ProtoMessage() {} func (x *PolicyBinding) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[35] + mi := &file_api_agentmesh_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3204,7 +3280,7 @@ func (x *PolicyBinding) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyBinding.ProtoReflect.Descriptor instead. func (*PolicyBinding) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{35} + return file_api_agentmesh_proto_rawDescGZIP(), []int{36} } func (x *PolicyBinding) GetRole() string { @@ -3236,7 +3312,7 @@ type PolicyConfig struct { func (x *PolicyConfig) Reset() { *x = PolicyConfig{} - mi := &file_api_agentmesh_proto_msgTypes[36] + mi := &file_api_agentmesh_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3248,7 +3324,7 @@ func (x *PolicyConfig) String() string { func (*PolicyConfig) ProtoMessage() {} func (x *PolicyConfig) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[36] + mi := &file_api_agentmesh_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3261,7 +3337,7 @@ func (x *PolicyConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyConfig.ProtoReflect.Descriptor instead. func (*PolicyConfig) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{36} + return file_api_agentmesh_proto_rawDescGZIP(), []int{37} } func (x *PolicyConfig) GetRoles() []*PolicyRole { @@ -3293,7 +3369,7 @@ type PolicyConfigGetRequest struct { func (x *PolicyConfigGetRequest) Reset() { *x = PolicyConfigGetRequest{} - mi := &file_api_agentmesh_proto_msgTypes[37] + mi := &file_api_agentmesh_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3305,7 +3381,7 @@ func (x *PolicyConfigGetRequest) String() string { func (*PolicyConfigGetRequest) ProtoMessage() {} func (x *PolicyConfigGetRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[37] + mi := &file_api_agentmesh_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3318,7 +3394,7 @@ func (x *PolicyConfigGetRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyConfigGetRequest.ProtoReflect.Descriptor instead. func (*PolicyConfigGetRequest) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{37} + return file_api_agentmesh_proto_rawDescGZIP(), []int{38} } // PolicyConfigGetResponse answers GET /policies for a mesh member holding a @@ -3334,7 +3410,7 @@ type PolicyConfigGetResponse struct { func (x *PolicyConfigGetResponse) Reset() { *x = PolicyConfigGetResponse{} - mi := &file_api_agentmesh_proto_msgTypes[38] + mi := &file_api_agentmesh_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3346,7 +3422,7 @@ func (x *PolicyConfigGetResponse) String() string { func (*PolicyConfigGetResponse) ProtoMessage() {} func (x *PolicyConfigGetResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[38] + mi := &file_api_agentmesh_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3359,7 +3435,7 @@ func (x *PolicyConfigGetResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyConfigGetResponse.ProtoReflect.Descriptor instead. func (*PolicyConfigGetResponse) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{38} + return file_api_agentmesh_proto_rawDescGZIP(), []int{39} } func (x *PolicyConfigGetResponse) GetDatalogRules() []string { @@ -3379,7 +3455,7 @@ type PolicyConfigUpdateResponse struct { func (x *PolicyConfigUpdateResponse) Reset() { *x = PolicyConfigUpdateResponse{} - mi := &file_api_agentmesh_proto_msgTypes[39] + mi := &file_api_agentmesh_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3391,7 +3467,7 @@ func (x *PolicyConfigUpdateResponse) String() string { func (*PolicyConfigUpdateResponse) ProtoMessage() {} func (x *PolicyConfigUpdateResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[39] + mi := &file_api_agentmesh_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3404,7 +3480,7 @@ func (x *PolicyConfigUpdateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyConfigUpdateResponse.ProtoReflect.Descriptor instead. func (*PolicyConfigUpdateResponse) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{39} + return file_api_agentmesh_proto_rawDescGZIP(), []int{40} } func (x *PolicyConfigUpdateResponse) GetSuccess() bool { @@ -3429,7 +3505,7 @@ type EgressAssignmentsRequest struct { func (x *EgressAssignmentsRequest) Reset() { *x = EgressAssignmentsRequest{} - mi := &file_api_agentmesh_proto_msgTypes[40] + mi := &file_api_agentmesh_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3441,7 +3517,7 @@ func (x *EgressAssignmentsRequest) String() string { func (*EgressAssignmentsRequest) ProtoMessage() {} func (x *EgressAssignmentsRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[40] + mi := &file_api_agentmesh_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3454,7 +3530,7 @@ func (x *EgressAssignmentsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EgressAssignmentsRequest.ProtoReflect.Descriptor instead. func (*EgressAssignmentsRequest) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{40} + return file_api_agentmesh_proto_rawDescGZIP(), []int{41} } // EgressAssignmentsResponse answers GET /egress for a mesh member holding a @@ -3470,7 +3546,7 @@ type EgressAssignmentsResponse struct { func (x *EgressAssignmentsResponse) Reset() { *x = EgressAssignmentsResponse{} - mi := &file_api_agentmesh_proto_msgTypes[41] + mi := &file_api_agentmesh_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3482,7 +3558,7 @@ func (x *EgressAssignmentsResponse) String() string { func (*EgressAssignmentsResponse) ProtoMessage() {} func (x *EgressAssignmentsResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[41] + mi := &file_api_agentmesh_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3495,7 +3571,7 @@ func (x *EgressAssignmentsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EgressAssignmentsResponse.ProtoReflect.Descriptor instead. func (*EgressAssignmentsResponse) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{41} + return file_api_agentmesh_proto_rawDescGZIP(), []int{42} } func (x *EgressAssignmentsResponse) GetEgress() []*EgressDestination { @@ -3522,7 +3598,7 @@ type KeysResponse struct { func (x *KeysResponse) Reset() { *x = KeysResponse{} - mi := &file_api_agentmesh_proto_msgTypes[42] + mi := &file_api_agentmesh_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3534,7 +3610,7 @@ func (x *KeysResponse) String() string { func (*KeysResponse) ProtoMessage() {} func (x *KeysResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[42] + mi := &file_api_agentmesh_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3547,7 +3623,7 @@ func (x *KeysResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use KeysResponse.ProtoReflect.Descriptor instead. func (*KeysResponse) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{42} + return file_api_agentmesh_proto_rawDescGZIP(), []int{43} } func (x *KeysResponse) GetPublicKeys() [][]byte { @@ -3600,7 +3676,7 @@ type TokenRefreshRequest struct { func (x *TokenRefreshRequest) Reset() { *x = TokenRefreshRequest{} - mi := &file_api_agentmesh_proto_msgTypes[43] + mi := &file_api_agentmesh_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3612,7 +3688,7 @@ func (x *TokenRefreshRequest) String() string { func (*TokenRefreshRequest) ProtoMessage() {} func (x *TokenRefreshRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[43] + mi := &file_api_agentmesh_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3625,7 +3701,7 @@ func (x *TokenRefreshRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TokenRefreshRequest.ProtoReflect.Descriptor instead. func (*TokenRefreshRequest) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{43} + return file_api_agentmesh_proto_rawDescGZIP(), []int{44} } func (x *TokenRefreshRequest) GetChallengeSignature() []byte { @@ -3667,7 +3743,7 @@ type TokenRefreshResponse struct { func (x *TokenRefreshResponse) Reset() { *x = TokenRefreshResponse{} - mi := &file_api_agentmesh_proto_msgTypes[44] + mi := &file_api_agentmesh_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3679,7 +3755,7 @@ func (x *TokenRefreshResponse) String() string { func (*TokenRefreshResponse) ProtoMessage() {} func (x *TokenRefreshResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[44] + mi := &file_api_agentmesh_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3692,7 +3768,7 @@ func (x *TokenRefreshResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TokenRefreshResponse.ProtoReflect.Descriptor instead. func (*TokenRefreshResponse) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{44} + return file_api_agentmesh_proto_rawDescGZIP(), []int{45} } func (x *TokenRefreshResponse) GetBiscuitToken() []byte { @@ -3729,7 +3805,7 @@ type NodeCatalogReport struct { func (x *NodeCatalogReport) Reset() { *x = NodeCatalogReport{} - mi := &file_api_agentmesh_proto_msgTypes[45] + mi := &file_api_agentmesh_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3741,7 +3817,7 @@ func (x *NodeCatalogReport) String() string { func (*NodeCatalogReport) ProtoMessage() {} func (x *NodeCatalogReport) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[45] + mi := &file_api_agentmesh_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3754,7 +3830,7 @@ func (x *NodeCatalogReport) ProtoReflect() protoreflect.Message { // Deprecated: Use NodeCatalogReport.ProtoReflect.Descriptor instead. func (*NodeCatalogReport) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{45} + return file_api_agentmesh_proto_rawDescGZIP(), []int{46} } func (x *NodeCatalogReport) GetServices() []*ServiceInfo { @@ -3773,7 +3849,7 @@ type TokenRevokeRequest struct { func (x *TokenRevokeRequest) Reset() { *x = TokenRevokeRequest{} - mi := &file_api_agentmesh_proto_msgTypes[46] + mi := &file_api_agentmesh_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3785,7 +3861,7 @@ func (x *TokenRevokeRequest) String() string { func (*TokenRevokeRequest) ProtoMessage() {} func (x *TokenRevokeRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[46] + mi := &file_api_agentmesh_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3798,7 +3874,7 @@ func (x *TokenRevokeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TokenRevokeRequest.ProtoReflect.Descriptor instead. func (*TokenRevokeRequest) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{46} + return file_api_agentmesh_proto_rawDescGZIP(), []int{47} } func (x *TokenRevokeRequest) GetPeerId() string { @@ -3818,7 +3894,7 @@ type TokenRevokeResponse struct { func (x *TokenRevokeResponse) Reset() { *x = TokenRevokeResponse{} - mi := &file_api_agentmesh_proto_msgTypes[47] + mi := &file_api_agentmesh_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3830,7 +3906,7 @@ func (x *TokenRevokeResponse) String() string { func (*TokenRevokeResponse) ProtoMessage() {} func (x *TokenRevokeResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[47] + mi := &file_api_agentmesh_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3843,7 +3919,7 @@ func (x *TokenRevokeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TokenRevokeResponse.ProtoReflect.Descriptor instead. func (*TokenRevokeResponse) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{47} + return file_api_agentmesh_proto_rawDescGZIP(), []int{48} } func (x *TokenRevokeResponse) GetSuccess() bool { @@ -3888,7 +3964,7 @@ type BootstrapTokenCreateRequest struct { func (x *BootstrapTokenCreateRequest) Reset() { *x = BootstrapTokenCreateRequest{} - mi := &file_api_agentmesh_proto_msgTypes[48] + mi := &file_api_agentmesh_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3900,7 +3976,7 @@ func (x *BootstrapTokenCreateRequest) String() string { func (*BootstrapTokenCreateRequest) ProtoMessage() {} func (x *BootstrapTokenCreateRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[48] + mi := &file_api_agentmesh_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3913,7 +3989,7 @@ func (x *BootstrapTokenCreateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use BootstrapTokenCreateRequest.ProtoReflect.Descriptor instead. func (*BootstrapTokenCreateRequest) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{48} + return file_api_agentmesh_proto_rawDescGZIP(), []int{49} } func (x *BootstrapTokenCreateRequest) GetRole() string { @@ -3974,7 +4050,7 @@ type BootstrapTokenCreateResponse struct { func (x *BootstrapTokenCreateResponse) Reset() { *x = BootstrapTokenCreateResponse{} - mi := &file_api_agentmesh_proto_msgTypes[49] + mi := &file_api_agentmesh_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3986,7 +4062,7 @@ func (x *BootstrapTokenCreateResponse) String() string { func (*BootstrapTokenCreateResponse) ProtoMessage() {} func (x *BootstrapTokenCreateResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[49] + mi := &file_api_agentmesh_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3999,7 +4075,7 @@ func (x *BootstrapTokenCreateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use BootstrapTokenCreateResponse.ProtoReflect.Descriptor instead. func (*BootstrapTokenCreateResponse) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{49} + return file_api_agentmesh_proto_rawDescGZIP(), []int{50} } func (x *BootstrapTokenCreateResponse) GetId() string { @@ -4058,7 +4134,7 @@ type BootstrapToken struct { func (x *BootstrapToken) Reset() { *x = BootstrapToken{} - mi := &file_api_agentmesh_proto_msgTypes[50] + mi := &file_api_agentmesh_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4070,7 +4146,7 @@ func (x *BootstrapToken) String() string { func (*BootstrapToken) ProtoMessage() {} func (x *BootstrapToken) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[50] + mi := &file_api_agentmesh_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4083,7 +4159,7 @@ func (x *BootstrapToken) ProtoReflect() protoreflect.Message { // Deprecated: Use BootstrapToken.ProtoReflect.Descriptor instead. func (*BootstrapToken) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{50} + return file_api_agentmesh_proto_rawDescGZIP(), []int{51} } func (x *BootstrapToken) GetId() string { @@ -4165,7 +4241,7 @@ type BootstrapTokenListResponse struct { func (x *BootstrapTokenListResponse) Reset() { *x = BootstrapTokenListResponse{} - mi := &file_api_agentmesh_proto_msgTypes[51] + mi := &file_api_agentmesh_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4177,7 +4253,7 @@ func (x *BootstrapTokenListResponse) String() string { func (*BootstrapTokenListResponse) ProtoMessage() {} func (x *BootstrapTokenListResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[51] + mi := &file_api_agentmesh_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4190,7 +4266,7 @@ func (x *BootstrapTokenListResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use BootstrapTokenListResponse.ProtoReflect.Descriptor instead. func (*BootstrapTokenListResponse) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{51} + return file_api_agentmesh_proto_rawDescGZIP(), []int{52} } func (x *BootstrapTokenListResponse) GetTokens() []*BootstrapToken { @@ -4221,7 +4297,7 @@ type EnrollmentRequest struct { func (x *EnrollmentRequest) Reset() { *x = EnrollmentRequest{} - mi := &file_api_agentmesh_proto_msgTypes[52] + mi := &file_api_agentmesh_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4233,7 +4309,7 @@ func (x *EnrollmentRequest) String() string { func (*EnrollmentRequest) ProtoMessage() {} func (x *EnrollmentRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[52] + mi := &file_api_agentmesh_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4246,7 +4322,7 @@ func (x *EnrollmentRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EnrollmentRequest.ProtoReflect.Descriptor instead. func (*EnrollmentRequest) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{52} + return file_api_agentmesh_proto_rawDescGZIP(), []int{53} } func (x *EnrollmentRequest) GetId() string { @@ -4314,7 +4390,7 @@ type EnrollmentRequestListResponse struct { func (x *EnrollmentRequestListResponse) Reset() { *x = EnrollmentRequestListResponse{} - mi := &file_api_agentmesh_proto_msgTypes[53] + mi := &file_api_agentmesh_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4326,7 +4402,7 @@ func (x *EnrollmentRequestListResponse) String() string { func (*EnrollmentRequestListResponse) ProtoMessage() {} func (x *EnrollmentRequestListResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[53] + mi := &file_api_agentmesh_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4339,7 +4415,7 @@ func (x *EnrollmentRequestListResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EnrollmentRequestListResponse.ProtoReflect.Descriptor instead. func (*EnrollmentRequestListResponse) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{53} + return file_api_agentmesh_proto_rawDescGZIP(), []int{54} } func (x *EnrollmentRequestListResponse) GetRequests() []*EnrollmentRequest { @@ -4365,7 +4441,7 @@ type User struct { func (x *User) Reset() { *x = User{} - mi := &file_api_agentmesh_proto_msgTypes[54] + mi := &file_api_agentmesh_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4377,7 +4453,7 @@ func (x *User) String() string { func (*User) ProtoMessage() {} func (x *User) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[54] + mi := &file_api_agentmesh_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4390,7 +4466,7 @@ func (x *User) ProtoReflect() protoreflect.Message { // Deprecated: Use User.ProtoReflect.Descriptor instead. func (*User) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{54} + return file_api_agentmesh_proto_rawDescGZIP(), []int{55} } func (x *User) GetId() string { @@ -4450,7 +4526,7 @@ type EnrolledNode struct { func (x *EnrolledNode) Reset() { *x = EnrolledNode{} - mi := &file_api_agentmesh_proto_msgTypes[55] + mi := &file_api_agentmesh_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4462,7 +4538,7 @@ func (x *EnrolledNode) String() string { func (*EnrolledNode) ProtoMessage() {} func (x *EnrolledNode) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[55] + mi := &file_api_agentmesh_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4475,7 +4551,7 @@ func (x *EnrolledNode) ProtoReflect() protoreflect.Message { // Deprecated: Use EnrolledNode.ProtoReflect.Descriptor instead. func (*EnrolledNode) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{55} + return file_api_agentmesh_proto_rawDescGZIP(), []int{56} } func (x *EnrolledNode) GetPeerId() string { @@ -4564,7 +4640,7 @@ type RouterLease struct { func (x *RouterLease) Reset() { *x = RouterLease{} - mi := &file_api_agentmesh_proto_msgTypes[56] + mi := &file_api_agentmesh_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4576,7 +4652,7 @@ func (x *RouterLease) String() string { func (*RouterLease) ProtoMessage() {} func (x *RouterLease) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[56] + mi := &file_api_agentmesh_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4589,7 +4665,7 @@ func (x *RouterLease) ProtoReflect() protoreflect.Message { // Deprecated: Use RouterLease.ProtoReflect.Descriptor instead. func (*RouterLease) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{56} + return file_api_agentmesh_proto_rawDescGZIP(), []int{57} } func (x *RouterLease) GetPeerId() string { @@ -4646,7 +4722,7 @@ type NodeServices struct { func (x *NodeServices) Reset() { *x = NodeServices{} - mi := &file_api_agentmesh_proto_msgTypes[57] + mi := &file_api_agentmesh_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4658,7 +4734,7 @@ func (x *NodeServices) String() string { func (*NodeServices) ProtoMessage() {} func (x *NodeServices) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[57] + mi := &file_api_agentmesh_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4671,7 +4747,7 @@ func (x *NodeServices) ProtoReflect() protoreflect.Message { // Deprecated: Use NodeServices.ProtoReflect.Descriptor instead. func (*NodeServices) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{57} + return file_api_agentmesh_proto_rawDescGZIP(), []int{58} } func (x *NodeServices) GetServices() []*ServiceInfo { @@ -4706,7 +4782,7 @@ type AdminStatusResponse struct { func (x *AdminStatusResponse) Reset() { *x = AdminStatusResponse{} - mi := &file_api_agentmesh_proto_msgTypes[58] + mi := &file_api_agentmesh_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4718,7 +4794,7 @@ func (x *AdminStatusResponse) String() string { func (*AdminStatusResponse) ProtoMessage() {} func (x *AdminStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[58] + mi := &file_api_agentmesh_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4731,7 +4807,7 @@ func (x *AdminStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AdminStatusResponse.ProtoReflect.Descriptor instead. func (*AdminStatusResponse) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{58} + return file_api_agentmesh_proto_rawDescGZIP(), []int{59} } func (x *AdminStatusResponse) GetUsers() []*User { @@ -4799,7 +4875,7 @@ type UserStatusResponse struct { func (x *UserStatusResponse) Reset() { *x = UserStatusResponse{} - mi := &file_api_agentmesh_proto_msgTypes[59] + mi := &file_api_agentmesh_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4811,7 +4887,7 @@ func (x *UserStatusResponse) String() string { func (*UserStatusResponse) ProtoMessage() {} func (x *UserStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[59] + mi := &file_api_agentmesh_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4824,7 +4900,7 @@ func (x *UserStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UserStatusResponse.ProtoReflect.Descriptor instead. func (*UserStatusResponse) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{59} + return file_api_agentmesh_proto_rawDescGZIP(), []int{60} } func (x *UserStatusResponse) GetUser() *User { @@ -4876,7 +4952,7 @@ type IdentityEvidenceResponse struct { func (x *IdentityEvidenceResponse) Reset() { *x = IdentityEvidenceResponse{} - mi := &file_api_agentmesh_proto_msgTypes[60] + mi := &file_api_agentmesh_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4888,7 +4964,7 @@ func (x *IdentityEvidenceResponse) String() string { func (*IdentityEvidenceResponse) ProtoMessage() {} func (x *IdentityEvidenceResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[60] + mi := &file_api_agentmesh_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4901,7 +4977,7 @@ func (x *IdentityEvidenceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use IdentityEvidenceResponse.ProtoReflect.Descriptor instead. func (*IdentityEvidenceResponse) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{60} + return file_api_agentmesh_proto_rawDescGZIP(), []int{61} } func (x *IdentityEvidenceResponse) GetPeerId() string { @@ -4962,7 +5038,7 @@ type PeerEvidenceResponse struct { func (x *PeerEvidenceResponse) Reset() { *x = PeerEvidenceResponse{} - mi := &file_api_agentmesh_proto_msgTypes[61] + mi := &file_api_agentmesh_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4974,7 +5050,7 @@ func (x *PeerEvidenceResponse) String() string { func (*PeerEvidenceResponse) ProtoMessage() {} func (x *PeerEvidenceResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[61] + mi := &file_api_agentmesh_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4987,7 +5063,7 @@ func (x *PeerEvidenceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerEvidenceResponse.ProtoReflect.Descriptor instead. func (*PeerEvidenceResponse) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{61} + return file_api_agentmesh_proto_rawDescGZIP(), []int{62} } func (x *PeerEvidenceResponse) GetPeerId() string { @@ -5072,7 +5148,7 @@ type MemberCredential struct { func (x *MemberCredential) Reset() { *x = MemberCredential{} - mi := &file_api_agentmesh_proto_msgTypes[62] + mi := &file_api_agentmesh_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5084,7 +5160,7 @@ func (x *MemberCredential) String() string { func (*MemberCredential) ProtoMessage() {} func (x *MemberCredential) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[62] + mi := &file_api_agentmesh_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5097,7 +5173,7 @@ func (x *MemberCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use MemberCredential.ProtoReflect.Descriptor instead. func (*MemberCredential) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{62} + return file_api_agentmesh_proto_rawDescGZIP(), []int{63} } func (x *MemberCredential) GetControlPlaneUrl() string { @@ -5162,7 +5238,7 @@ type TrustedSigningKey struct { func (x *TrustedSigningKey) Reset() { *x = TrustedSigningKey{} - mi := &file_api_agentmesh_proto_msgTypes[63] + mi := &file_api_agentmesh_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5174,7 +5250,7 @@ func (x *TrustedSigningKey) String() string { func (*TrustedSigningKey) ProtoMessage() {} func (x *TrustedSigningKey) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[63] + mi := &file_api_agentmesh_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5187,7 +5263,7 @@ func (x *TrustedSigningKey) ProtoReflect() protoreflect.Message { // Deprecated: Use TrustedSigningKey.ProtoReflect.Descriptor instead. func (*TrustedSigningKey) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{63} + return file_api_agentmesh_proto_rawDescGZIP(), []int{64} } func (x *TrustedSigningKey) GetPublicKey() []byte { @@ -5216,7 +5292,7 @@ type OIDCSession struct { func (x *OIDCSession) Reset() { *x = OIDCSession{} - mi := &file_api_agentmesh_proto_msgTypes[64] + mi := &file_api_agentmesh_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5228,7 +5304,7 @@ func (x *OIDCSession) String() string { func (*OIDCSession) ProtoMessage() {} func (x *OIDCSession) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[64] + mi := &file_api_agentmesh_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5241,7 +5317,7 @@ func (x *OIDCSession) ProtoReflect() protoreflect.Message { // Deprecated: Use OIDCSession.ProtoReflect.Descriptor instead. func (*OIDCSession) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{64} + return file_api_agentmesh_proto_rawDescGZIP(), []int{65} } func (x *OIDCSession) GetIssuer() string { @@ -5293,7 +5369,7 @@ type TaskAuthorizationRule struct { func (x *TaskAuthorizationRule) Reset() { *x = TaskAuthorizationRule{} - mi := &file_api_agentmesh_proto_msgTypes[65] + mi := &file_api_agentmesh_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5305,7 +5381,7 @@ func (x *TaskAuthorizationRule) String() string { func (*TaskAuthorizationRule) ProtoMessage() {} func (x *TaskAuthorizationRule) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[65] + mi := &file_api_agentmesh_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5318,7 +5394,7 @@ func (x *TaskAuthorizationRule) ProtoReflect() protoreflect.Message { // Deprecated: Use TaskAuthorizationRule.ProtoReflect.Descriptor instead. func (*TaskAuthorizationRule) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{65} + return file_api_agentmesh_proto_rawDescGZIP(), []int{66} } func (x *TaskAuthorizationRule) GetName() string { @@ -5370,7 +5446,7 @@ type TaskRule struct { func (x *TaskRule) Reset() { *x = TaskRule{} - mi := &file_api_agentmesh_proto_msgTypes[66] + mi := &file_api_agentmesh_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5382,7 +5458,7 @@ func (x *TaskRule) String() string { func (*TaskRule) ProtoMessage() {} func (x *TaskRule) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[66] + mi := &file_api_agentmesh_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5395,7 +5471,7 @@ func (x *TaskRule) ProtoReflect() protoreflect.Message { // Deprecated: Use TaskRule.ProtoReflect.Descriptor instead. func (*TaskRule) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{66} + return file_api_agentmesh_proto_rawDescGZIP(), []int{67} } func (x *TaskRule) GetDescription() string { @@ -5444,7 +5520,7 @@ type TaskOperation struct { func (x *TaskOperation) Reset() { *x = TaskOperation{} - mi := &file_api_agentmesh_proto_msgTypes[67] + mi := &file_api_agentmesh_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5456,7 +5532,7 @@ func (x *TaskOperation) String() string { func (*TaskOperation) ProtoMessage() {} func (x *TaskOperation) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[67] + mi := &file_api_agentmesh_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5469,7 +5545,7 @@ func (x *TaskOperation) ProtoReflect() protoreflect.Message { // Deprecated: Use TaskOperation.ProtoReflect.Descriptor instead. func (*TaskOperation) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{67} + return file_api_agentmesh_proto_rawDescGZIP(), []int{68} } func (x *TaskOperation) GetAllowedTools() []string { @@ -5529,7 +5605,7 @@ type TokenExchangeRequest struct { func (x *TokenExchangeRequest) Reset() { *x = TokenExchangeRequest{} - mi := &file_api_agentmesh_proto_msgTypes[68] + mi := &file_api_agentmesh_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5541,7 +5617,7 @@ func (x *TokenExchangeRequest) String() string { func (*TokenExchangeRequest) ProtoMessage() {} func (x *TokenExchangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[68] + mi := &file_api_agentmesh_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5554,7 +5630,7 @@ func (x *TokenExchangeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TokenExchangeRequest.ProtoReflect.Descriptor instead. func (*TokenExchangeRequest) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{68} + return file_api_agentmesh_proto_rawDescGZIP(), []int{69} } func (x *TokenExchangeRequest) GetSubjectToken() string { @@ -5604,7 +5680,7 @@ type TokenExchangeResponse struct { func (x *TokenExchangeResponse) Reset() { *x = TokenExchangeResponse{} - mi := &file_api_agentmesh_proto_msgTypes[69] + mi := &file_api_agentmesh_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5616,7 +5692,7 @@ func (x *TokenExchangeResponse) String() string { func (*TokenExchangeResponse) ProtoMessage() {} func (x *TokenExchangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[69] + mi := &file_api_agentmesh_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5629,7 +5705,7 @@ func (x *TokenExchangeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TokenExchangeResponse.ProtoReflect.Descriptor instead. func (*TokenExchangeResponse) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{69} + return file_api_agentmesh_proto_rawDescGZIP(), []int{70} } func (x *TokenExchangeResponse) GetBiscuitToken() []byte { @@ -5685,7 +5761,7 @@ type STSTokenRequest struct { func (x *STSTokenRequest) Reset() { *x = STSTokenRequest{} - mi := &file_api_agentmesh_proto_msgTypes[70] + mi := &file_api_agentmesh_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5697,7 +5773,7 @@ func (x *STSTokenRequest) String() string { func (*STSTokenRequest) ProtoMessage() {} func (x *STSTokenRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[70] + mi := &file_api_agentmesh_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5710,7 +5786,7 @@ func (x *STSTokenRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use STSTokenRequest.ProtoReflect.Descriptor instead. func (*STSTokenRequest) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{70} + return file_api_agentmesh_proto_rawDescGZIP(), []int{71} } func (x *STSTokenRequest) GetBiscuit() []byte { @@ -5761,7 +5837,7 @@ type STSTokenResponse struct { func (x *STSTokenResponse) Reset() { *x = STSTokenResponse{} - mi := &file_api_agentmesh_proto_msgTypes[71] + mi := &file_api_agentmesh_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5773,7 +5849,7 @@ func (x *STSTokenResponse) String() string { func (*STSTokenResponse) ProtoMessage() {} func (x *STSTokenResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[71] + mi := &file_api_agentmesh_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5786,7 +5862,7 @@ func (x *STSTokenResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use STSTokenResponse.ProtoReflect.Descriptor instead. func (*STSTokenResponse) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{71} + return file_api_agentmesh_proto_rawDescGZIP(), []int{72} } func (x *STSTokenResponse) GetJwt() string { @@ -5837,7 +5913,7 @@ type RevocationsResponse struct { func (x *RevocationsResponse) Reset() { *x = RevocationsResponse{} - mi := &file_api_agentmesh_proto_msgTypes[72] + mi := &file_api_agentmesh_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5849,7 +5925,7 @@ func (x *RevocationsResponse) String() string { func (*RevocationsResponse) ProtoMessage() {} func (x *RevocationsResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_agentmesh_proto_msgTypes[72] + mi := &file_api_agentmesh_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5862,7 +5938,7 @@ func (x *RevocationsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RevocationsResponse.ProtoReflect.Descriptor instead. func (*RevocationsResponse) Descriptor() ([]byte, []int) { - return file_api_agentmesh_proto_rawDescGZIP(), []int{72} + return file_api_agentmesh_proto_rawDescGZIP(), []int{73} } func (x *RevocationsResponse) GetRevocationIds() []string { @@ -5986,16 +6062,20 @@ const file_api_agentmesh_proto_rawDesc = "" + "\vreplication\x18\x01 \x01(\x05R\vreplication\x12H\n" + "\x12reprovide_interval\x18\x02 \x01(\v2\x19.google.protobuf.DurationR\x11reprovideInterval\x128\n" + "\n" + - "record_ttl\x18\x03 \x01(\v2\x19.google.protobuf.DurationR\trecordTtl\"\xe8\x01\n" + + "record_ttl\x18\x03 \x01(\v2\x19.google.protobuf.DurationR\trecordTtl\"\xa7\x02\n" + "\x10DirectoryRequest\x12:\n" + "\aprovide\x18\x01 \x01(\v2\x1e.agentmesh.v1.DirectoryProvideH\x00R\aprovide\x12M\n" + "\x0efind_providers\x18\x02 \x01(\v2$.agentmesh.v1.DirectoryFindProvidersH\x00R\rfindProviders\x12>\n" + - "\tfind_peer\x18\x03 \x01(\v2\x1f.agentmesh.v1.DirectoryFindPeerH\x00R\bfindPeerB\t\n" + + "\tfind_peer\x18\x03 \x01(\v2\x1f.agentmesh.v1.DirectoryFindPeerH\x00R\bfindPeer\x12=\n" + + "\bwithdraw\x18\x04 \x01(\v2\x1f.agentmesh.v1.DirectoryWithdrawH\x00R\bwithdrawB\t\n" + "\arequest\"V\n" + "\x10DirectoryProvide\x12\x10\n" + "\x03key\x18\x01 \x01(\fR\x03key\x12\x14\n" + "\x05addrs\x18\x02 \x03(\tR\x05addrs\x12\x1a\n" + - "\bprovider\x18\x03 \x01(\tR\bprovider\"@\n" + + "\bprovider\x18\x03 \x01(\tR\bprovider\"A\n" + + "\x11DirectoryWithdraw\x12\x10\n" + + "\x03key\x18\x01 \x01(\fR\x03key\x12\x1a\n" + + "\bprovider\x18\x02 \x01(\tR\bprovider\"@\n" + "\x16DirectoryFindProviders\x12\x10\n" + "\x03key\x18\x01 \x01(\fR\x03key\x12\x14\n" + "\x05limit\x18\x02 \x01(\rR\x05limit\",\n" + @@ -6383,7 +6463,7 @@ func file_api_agentmesh_proto_rawDescGZIP() []byte { } var file_api_agentmesh_proto_enumTypes = make([]protoimpl.EnumInfo, 8) -var file_api_agentmesh_proto_msgTypes = make([]protoimpl.MessageInfo, 81) +var file_api_agentmesh_proto_msgTypes = make([]protoimpl.MessageInfo, 82) var file_api_agentmesh_proto_goTypes = []any{ (EnrollmentStatus)(0), // 0: agentmesh.v1.EnrollmentStatus (ServiceType)(0), // 1: agentmesh.v1.ServiceType @@ -6409,178 +6489,180 @@ var file_api_agentmesh_proto_goTypes = []any{ (*DirectoryParameters)(nil), // 21: agentmesh.v1.DirectoryParameters (*DirectoryRequest)(nil), // 22: agentmesh.v1.DirectoryRequest (*DirectoryProvide)(nil), // 23: agentmesh.v1.DirectoryProvide - (*DirectoryFindProviders)(nil), // 24: agentmesh.v1.DirectoryFindProviders - (*DirectoryFindPeer)(nil), // 25: agentmesh.v1.DirectoryFindPeer - (*DirectoryResponse)(nil), // 26: agentmesh.v1.DirectoryResponse - (*PeerRecord)(nil), // 27: agentmesh.v1.PeerRecord - (*RouterInfo)(nil), // 28: agentmesh.v1.RouterInfo - (*RouterLeaseRequest)(nil), // 29: agentmesh.v1.RouterLeaseRequest - (*RouterLeaseResponse)(nil), // 30: agentmesh.v1.RouterLeaseResponse - (*PolicyRole)(nil), // 31: agentmesh.v1.PolicyRole - (*HTTPGrant)(nil), // 32: agentmesh.v1.HTTPGrant - (*EgressDestination)(nil), // 33: agentmesh.v1.EgressDestination - (*Inspection)(nil), // 34: agentmesh.v1.Inspection - (*Inspector)(nil), // 35: agentmesh.v1.Inspector - (*ModelArmor)(nil), // 36: agentmesh.v1.ModelArmor - (*ExtProc)(nil), // 37: agentmesh.v1.ExtProc - (*ExtProcProcessingMode)(nil), // 38: agentmesh.v1.ExtProcProcessingMode - (*CredentialBroker)(nil), // 39: agentmesh.v1.CredentialBroker - (*OIDCFederation)(nil), // 40: agentmesh.v1.OIDCFederation - (*AWSAssumeRole)(nil), // 41: agentmesh.v1.AWSAssumeRole - (*PlatformIdentity)(nil), // 42: agentmesh.v1.PlatformIdentity - (*PolicyBinding)(nil), // 43: agentmesh.v1.PolicyBinding - (*PolicyConfig)(nil), // 44: agentmesh.v1.PolicyConfig - (*PolicyConfigGetRequest)(nil), // 45: agentmesh.v1.PolicyConfigGetRequest - (*PolicyConfigGetResponse)(nil), // 46: agentmesh.v1.PolicyConfigGetResponse - (*PolicyConfigUpdateResponse)(nil), // 47: agentmesh.v1.PolicyConfigUpdateResponse - (*EgressAssignmentsRequest)(nil), // 48: agentmesh.v1.EgressAssignmentsRequest - (*EgressAssignmentsResponse)(nil), // 49: agentmesh.v1.EgressAssignmentsResponse - (*KeysResponse)(nil), // 50: agentmesh.v1.KeysResponse - (*TokenRefreshRequest)(nil), // 51: agentmesh.v1.TokenRefreshRequest - (*TokenRefreshResponse)(nil), // 52: agentmesh.v1.TokenRefreshResponse - (*NodeCatalogReport)(nil), // 53: agentmesh.v1.NodeCatalogReport - (*TokenRevokeRequest)(nil), // 54: agentmesh.v1.TokenRevokeRequest - (*TokenRevokeResponse)(nil), // 55: agentmesh.v1.TokenRevokeResponse - (*BootstrapTokenCreateRequest)(nil), // 56: agentmesh.v1.BootstrapTokenCreateRequest - (*BootstrapTokenCreateResponse)(nil), // 57: agentmesh.v1.BootstrapTokenCreateResponse - (*BootstrapToken)(nil), // 58: agentmesh.v1.BootstrapToken - (*BootstrapTokenListResponse)(nil), // 59: agentmesh.v1.BootstrapTokenListResponse - (*EnrollmentRequest)(nil), // 60: agentmesh.v1.EnrollmentRequest - (*EnrollmentRequestListResponse)(nil), // 61: agentmesh.v1.EnrollmentRequestListResponse - (*User)(nil), // 62: agentmesh.v1.User - (*EnrolledNode)(nil), // 63: agentmesh.v1.EnrolledNode - (*RouterLease)(nil), // 64: agentmesh.v1.RouterLease - (*NodeServices)(nil), // 65: agentmesh.v1.NodeServices - (*AdminStatusResponse)(nil), // 66: agentmesh.v1.AdminStatusResponse - (*UserStatusResponse)(nil), // 67: agentmesh.v1.UserStatusResponse - (*IdentityEvidenceResponse)(nil), // 68: agentmesh.v1.IdentityEvidenceResponse - (*PeerEvidenceResponse)(nil), // 69: agentmesh.v1.PeerEvidenceResponse - (*MemberCredential)(nil), // 70: agentmesh.v1.MemberCredential - (*TrustedSigningKey)(nil), // 71: agentmesh.v1.TrustedSigningKey - (*OIDCSession)(nil), // 72: agentmesh.v1.OIDCSession - (*TaskAuthorizationRule)(nil), // 73: agentmesh.v1.TaskAuthorizationRule - (*TaskRule)(nil), // 74: agentmesh.v1.TaskRule - (*TaskOperation)(nil), // 75: agentmesh.v1.TaskOperation - (*TokenExchangeRequest)(nil), // 76: agentmesh.v1.TokenExchangeRequest - (*TokenExchangeResponse)(nil), // 77: agentmesh.v1.TokenExchangeResponse - (*STSTokenRequest)(nil), // 78: agentmesh.v1.STSTokenRequest - (*STSTokenResponse)(nil), // 79: agentmesh.v1.STSTokenResponse - (*RevocationsResponse)(nil), // 80: agentmesh.v1.RevocationsResponse - nil, // 81: agentmesh.v1.EnrollRequest.LabelsEntry - nil, // 82: agentmesh.v1.BootstrapEnrollRequest.LabelsEntry - nil, // 83: agentmesh.v1.CommandBackend.EnvEntry - nil, // 84: agentmesh.v1.RouterInfo.LabelsEntry - nil, // 85: agentmesh.v1.EnrollmentRequest.LabelsEntry - nil, // 86: agentmesh.v1.EnrolledNode.LabelsEntry - nil, // 87: agentmesh.v1.AdminStatusResponse.NodeCatalogEntry - nil, // 88: agentmesh.v1.PeerEvidenceResponse.LabelsEntry - (*durationpb.Duration)(nil), // 89: google.protobuf.Duration - (*timestamppb.Timestamp)(nil), // 90: google.protobuf.Timestamp + (*DirectoryWithdraw)(nil), // 24: agentmesh.v1.DirectoryWithdraw + (*DirectoryFindProviders)(nil), // 25: agentmesh.v1.DirectoryFindProviders + (*DirectoryFindPeer)(nil), // 26: agentmesh.v1.DirectoryFindPeer + (*DirectoryResponse)(nil), // 27: agentmesh.v1.DirectoryResponse + (*PeerRecord)(nil), // 28: agentmesh.v1.PeerRecord + (*RouterInfo)(nil), // 29: agentmesh.v1.RouterInfo + (*RouterLeaseRequest)(nil), // 30: agentmesh.v1.RouterLeaseRequest + (*RouterLeaseResponse)(nil), // 31: agentmesh.v1.RouterLeaseResponse + (*PolicyRole)(nil), // 32: agentmesh.v1.PolicyRole + (*HTTPGrant)(nil), // 33: agentmesh.v1.HTTPGrant + (*EgressDestination)(nil), // 34: agentmesh.v1.EgressDestination + (*Inspection)(nil), // 35: agentmesh.v1.Inspection + (*Inspector)(nil), // 36: agentmesh.v1.Inspector + (*ModelArmor)(nil), // 37: agentmesh.v1.ModelArmor + (*ExtProc)(nil), // 38: agentmesh.v1.ExtProc + (*ExtProcProcessingMode)(nil), // 39: agentmesh.v1.ExtProcProcessingMode + (*CredentialBroker)(nil), // 40: agentmesh.v1.CredentialBroker + (*OIDCFederation)(nil), // 41: agentmesh.v1.OIDCFederation + (*AWSAssumeRole)(nil), // 42: agentmesh.v1.AWSAssumeRole + (*PlatformIdentity)(nil), // 43: agentmesh.v1.PlatformIdentity + (*PolicyBinding)(nil), // 44: agentmesh.v1.PolicyBinding + (*PolicyConfig)(nil), // 45: agentmesh.v1.PolicyConfig + (*PolicyConfigGetRequest)(nil), // 46: agentmesh.v1.PolicyConfigGetRequest + (*PolicyConfigGetResponse)(nil), // 47: agentmesh.v1.PolicyConfigGetResponse + (*PolicyConfigUpdateResponse)(nil), // 48: agentmesh.v1.PolicyConfigUpdateResponse + (*EgressAssignmentsRequest)(nil), // 49: agentmesh.v1.EgressAssignmentsRequest + (*EgressAssignmentsResponse)(nil), // 50: agentmesh.v1.EgressAssignmentsResponse + (*KeysResponse)(nil), // 51: agentmesh.v1.KeysResponse + (*TokenRefreshRequest)(nil), // 52: agentmesh.v1.TokenRefreshRequest + (*TokenRefreshResponse)(nil), // 53: agentmesh.v1.TokenRefreshResponse + (*NodeCatalogReport)(nil), // 54: agentmesh.v1.NodeCatalogReport + (*TokenRevokeRequest)(nil), // 55: agentmesh.v1.TokenRevokeRequest + (*TokenRevokeResponse)(nil), // 56: agentmesh.v1.TokenRevokeResponse + (*BootstrapTokenCreateRequest)(nil), // 57: agentmesh.v1.BootstrapTokenCreateRequest + (*BootstrapTokenCreateResponse)(nil), // 58: agentmesh.v1.BootstrapTokenCreateResponse + (*BootstrapToken)(nil), // 59: agentmesh.v1.BootstrapToken + (*BootstrapTokenListResponse)(nil), // 60: agentmesh.v1.BootstrapTokenListResponse + (*EnrollmentRequest)(nil), // 61: agentmesh.v1.EnrollmentRequest + (*EnrollmentRequestListResponse)(nil), // 62: agentmesh.v1.EnrollmentRequestListResponse + (*User)(nil), // 63: agentmesh.v1.User + (*EnrolledNode)(nil), // 64: agentmesh.v1.EnrolledNode + (*RouterLease)(nil), // 65: agentmesh.v1.RouterLease + (*NodeServices)(nil), // 66: agentmesh.v1.NodeServices + (*AdminStatusResponse)(nil), // 67: agentmesh.v1.AdminStatusResponse + (*UserStatusResponse)(nil), // 68: agentmesh.v1.UserStatusResponse + (*IdentityEvidenceResponse)(nil), // 69: agentmesh.v1.IdentityEvidenceResponse + (*PeerEvidenceResponse)(nil), // 70: agentmesh.v1.PeerEvidenceResponse + (*MemberCredential)(nil), // 71: agentmesh.v1.MemberCredential + (*TrustedSigningKey)(nil), // 72: agentmesh.v1.TrustedSigningKey + (*OIDCSession)(nil), // 73: agentmesh.v1.OIDCSession + (*TaskAuthorizationRule)(nil), // 74: agentmesh.v1.TaskAuthorizationRule + (*TaskRule)(nil), // 75: agentmesh.v1.TaskRule + (*TaskOperation)(nil), // 76: agentmesh.v1.TaskOperation + (*TokenExchangeRequest)(nil), // 77: agentmesh.v1.TokenExchangeRequest + (*TokenExchangeResponse)(nil), // 78: agentmesh.v1.TokenExchangeResponse + (*STSTokenRequest)(nil), // 79: agentmesh.v1.STSTokenRequest + (*STSTokenResponse)(nil), // 80: agentmesh.v1.STSTokenResponse + (*RevocationsResponse)(nil), // 81: agentmesh.v1.RevocationsResponse + nil, // 82: agentmesh.v1.EnrollRequest.LabelsEntry + nil, // 83: agentmesh.v1.BootstrapEnrollRequest.LabelsEntry + nil, // 84: agentmesh.v1.CommandBackend.EnvEntry + nil, // 85: agentmesh.v1.RouterInfo.LabelsEntry + nil, // 86: agentmesh.v1.EnrollmentRequest.LabelsEntry + nil, // 87: agentmesh.v1.EnrolledNode.LabelsEntry + nil, // 88: agentmesh.v1.AdminStatusResponse.NodeCatalogEntry + nil, // 89: agentmesh.v1.PeerEvidenceResponse.LabelsEntry + (*durationpb.Duration)(nil), // 90: google.protobuf.Duration + (*timestamppb.Timestamp)(nil), // 91: google.protobuf.Timestamp } var file_api_agentmesh_proto_depIdxs = []int32{ 4, // 0: agentmesh.v1.RouterGoAway.reason:type_name -> agentmesh.v1.RouterGoAway.Reason - 89, // 1: agentmesh.v1.RouterGoAway.retry_after:type_name -> google.protobuf.Duration + 90, // 1: agentmesh.v1.RouterGoAway.retry_after:type_name -> google.protobuf.Duration 5, // 2: agentmesh.v1.MeshEvent.type:type_name -> agentmesh.v1.MeshEvent.Type - 90, // 3: agentmesh.v1.MeshEvent.event_time:type_name -> google.protobuf.Timestamp - 81, // 4: agentmesh.v1.EnrollRequest.labels:type_name -> agentmesh.v1.EnrollRequest.LabelsEntry - 90, // 5: agentmesh.v1.EnrollResponse.expire_time:type_name -> google.protobuf.Timestamp - 82, // 6: agentmesh.v1.BootstrapEnrollRequest.labels:type_name -> agentmesh.v1.BootstrapEnrollRequest.LabelsEntry + 91, // 3: agentmesh.v1.MeshEvent.event_time:type_name -> google.protobuf.Timestamp + 82, // 4: agentmesh.v1.EnrollRequest.labels:type_name -> agentmesh.v1.EnrollRequest.LabelsEntry + 91, // 5: agentmesh.v1.EnrollResponse.expire_time:type_name -> google.protobuf.Timestamp + 83, // 6: agentmesh.v1.BootstrapEnrollRequest.labels:type_name -> agentmesh.v1.BootstrapEnrollRequest.LabelsEntry 0, // 7: agentmesh.v1.BootstrapEnrollResponse.status:type_name -> agentmesh.v1.EnrollmentStatus - 90, // 8: agentmesh.v1.BootstrapEnrollResponse.expire_time:type_name -> google.protobuf.Timestamp + 91, // 8: agentmesh.v1.BootstrapEnrollResponse.expire_time:type_name -> google.protobuf.Timestamp 1, // 9: agentmesh.v1.ServiceInfo.type:type_name -> agentmesh.v1.ServiceType - 83, // 10: agentmesh.v1.CommandBackend.env:type_name -> agentmesh.v1.CommandBackend.EnvEntry + 84, // 10: agentmesh.v1.CommandBackend.env:type_name -> agentmesh.v1.CommandBackend.EnvEntry 16, // 11: agentmesh.v1.RegisterServiceRequest.service:type_name -> agentmesh.v1.ServiceInfo 17, // 12: agentmesh.v1.RegisterServiceRequest.command:type_name -> agentmesh.v1.CommandBackend - 28, // 13: agentmesh.v1.ControlPlaneInfoResponse.routers:type_name -> agentmesh.v1.RouterInfo + 29, // 13: agentmesh.v1.ControlPlaneInfoResponse.routers:type_name -> agentmesh.v1.RouterInfo 21, // 14: agentmesh.v1.ControlPlaneInfoResponse.directory:type_name -> agentmesh.v1.DirectoryParameters - 89, // 15: agentmesh.v1.DirectoryParameters.reprovide_interval:type_name -> google.protobuf.Duration - 89, // 16: agentmesh.v1.DirectoryParameters.record_ttl:type_name -> google.protobuf.Duration + 90, // 15: agentmesh.v1.DirectoryParameters.reprovide_interval:type_name -> google.protobuf.Duration + 90, // 16: agentmesh.v1.DirectoryParameters.record_ttl:type_name -> google.protobuf.Duration 23, // 17: agentmesh.v1.DirectoryRequest.provide:type_name -> agentmesh.v1.DirectoryProvide - 24, // 18: agentmesh.v1.DirectoryRequest.find_providers:type_name -> agentmesh.v1.DirectoryFindProviders - 25, // 19: agentmesh.v1.DirectoryRequest.find_peer:type_name -> agentmesh.v1.DirectoryFindPeer - 27, // 20: agentmesh.v1.DirectoryResponse.providers:type_name -> agentmesh.v1.PeerRecord - 27, // 21: agentmesh.v1.DirectoryResponse.peer:type_name -> agentmesh.v1.PeerRecord - 84, // 22: agentmesh.v1.RouterInfo.labels:type_name -> agentmesh.v1.RouterInfo.LabelsEntry - 89, // 23: agentmesh.v1.RouterLeaseRequest.ttl:type_name -> google.protobuf.Duration - 90, // 24: agentmesh.v1.RouterLeaseResponse.expire_time:type_name -> google.protobuf.Timestamp - 32, // 25: agentmesh.v1.PolicyRole.http:type_name -> agentmesh.v1.HTTPGrant - 39, // 26: agentmesh.v1.EgressDestination.broker:type_name -> agentmesh.v1.CredentialBroker - 34, // 27: agentmesh.v1.EgressDestination.inspection:type_name -> agentmesh.v1.Inspection - 2, // 28: agentmesh.v1.EgressDestination.mode:type_name -> agentmesh.v1.EgressMode - 35, // 29: agentmesh.v1.Inspection.inspectors:type_name -> agentmesh.v1.Inspector - 36, // 30: agentmesh.v1.Inspector.model_armor:type_name -> agentmesh.v1.ModelArmor - 37, // 31: agentmesh.v1.Inspector.ext_proc:type_name -> agentmesh.v1.ExtProc - 3, // 32: agentmesh.v1.ModelArmor.response:type_name -> agentmesh.v1.ResponseInspection - 89, // 33: agentmesh.v1.ModelArmor.timeout:type_name -> google.protobuf.Duration - 38, // 34: agentmesh.v1.ExtProc.processing_mode:type_name -> agentmesh.v1.ExtProcProcessingMode - 89, // 35: agentmesh.v1.ExtProc.message_timeout:type_name -> google.protobuf.Duration - 6, // 36: agentmesh.v1.ExtProcProcessingMode.request_header_mode:type_name -> agentmesh.v1.ExtProcProcessingMode.HeaderMode - 6, // 37: agentmesh.v1.ExtProcProcessingMode.response_header_mode:type_name -> agentmesh.v1.ExtProcProcessingMode.HeaderMode - 7, // 38: agentmesh.v1.ExtProcProcessingMode.request_body_mode:type_name -> agentmesh.v1.ExtProcProcessingMode.BodyMode - 7, // 39: agentmesh.v1.ExtProcProcessingMode.response_body_mode:type_name -> agentmesh.v1.ExtProcProcessingMode.BodyMode - 6, // 40: agentmesh.v1.ExtProcProcessingMode.request_trailer_mode:type_name -> agentmesh.v1.ExtProcProcessingMode.HeaderMode - 6, // 41: agentmesh.v1.ExtProcProcessingMode.response_trailer_mode:type_name -> agentmesh.v1.ExtProcProcessingMode.HeaderMode - 40, // 42: agentmesh.v1.CredentialBroker.oidc_federation:type_name -> agentmesh.v1.OIDCFederation - 41, // 43: agentmesh.v1.CredentialBroker.aws_assume_role:type_name -> agentmesh.v1.AWSAssumeRole - 42, // 44: agentmesh.v1.CredentialBroker.platform_identity:type_name -> agentmesh.v1.PlatformIdentity - 31, // 45: agentmesh.v1.PolicyConfig.roles:type_name -> agentmesh.v1.PolicyRole - 43, // 46: agentmesh.v1.PolicyConfig.bindings:type_name -> agentmesh.v1.PolicyBinding - 33, // 47: agentmesh.v1.PolicyConfig.egress:type_name -> agentmesh.v1.EgressDestination - 33, // 48: agentmesh.v1.EgressAssignmentsResponse.egress:type_name -> agentmesh.v1.EgressDestination - 90, // 49: agentmesh.v1.KeysResponse.sign_time:type_name -> google.protobuf.Timestamp - 90, // 50: agentmesh.v1.TokenRefreshResponse.expire_time:type_name -> google.protobuf.Timestamp - 16, // 51: agentmesh.v1.NodeCatalogReport.services:type_name -> agentmesh.v1.ServiceInfo - 90, // 52: agentmesh.v1.BootstrapTokenCreateResponse.expire_time:type_name -> google.protobuf.Timestamp - 90, // 53: agentmesh.v1.BootstrapToken.create_time:type_name -> google.protobuf.Timestamp - 90, // 54: agentmesh.v1.BootstrapToken.expire_time:type_name -> google.protobuf.Timestamp - 90, // 55: agentmesh.v1.BootstrapToken.revoke_time:type_name -> google.protobuf.Timestamp - 58, // 56: agentmesh.v1.BootstrapTokenListResponse.tokens:type_name -> agentmesh.v1.BootstrapToken - 0, // 57: agentmesh.v1.EnrollmentRequest.status:type_name -> agentmesh.v1.EnrollmentStatus - 85, // 58: agentmesh.v1.EnrollmentRequest.labels:type_name -> agentmesh.v1.EnrollmentRequest.LabelsEntry - 90, // 59: agentmesh.v1.EnrollmentRequest.create_time:type_name -> google.protobuf.Timestamp - 90, // 60: agentmesh.v1.EnrollmentRequest.resolve_time:type_name -> google.protobuf.Timestamp - 60, // 61: agentmesh.v1.EnrollmentRequestListResponse.requests:type_name -> agentmesh.v1.EnrollmentRequest - 90, // 62: agentmesh.v1.User.create_time:type_name -> google.protobuf.Timestamp - 86, // 63: agentmesh.v1.EnrolledNode.labels:type_name -> agentmesh.v1.EnrolledNode.LabelsEntry - 90, // 64: agentmesh.v1.EnrolledNode.enroll_time:type_name -> google.protobuf.Timestamp - 90, // 65: agentmesh.v1.EnrolledNode.expire_time:type_name -> google.protobuf.Timestamp - 90, // 66: agentmesh.v1.RouterLease.last_renewal_time:type_name -> google.protobuf.Timestamp - 90, // 67: agentmesh.v1.RouterLease.expire_time:type_name -> google.protobuf.Timestamp - 16, // 68: agentmesh.v1.NodeServices.services:type_name -> agentmesh.v1.ServiceInfo - 90, // 69: agentmesh.v1.NodeServices.report_time:type_name -> google.protobuf.Timestamp - 62, // 70: agentmesh.v1.AdminStatusResponse.users:type_name -> agentmesh.v1.User - 64, // 71: agentmesh.v1.AdminStatusResponse.active_routers:type_name -> agentmesh.v1.RouterLease - 63, // 72: agentmesh.v1.AdminStatusResponse.enrolled_nodes:type_name -> agentmesh.v1.EnrolledNode - 60, // 73: agentmesh.v1.AdminStatusResponse.enrollment_requests:type_name -> agentmesh.v1.EnrollmentRequest - 58, // 74: agentmesh.v1.AdminStatusResponse.bootstrap_tokens:type_name -> agentmesh.v1.BootstrapToken - 44, // 75: agentmesh.v1.AdminStatusResponse.policy:type_name -> agentmesh.v1.PolicyConfig - 87, // 76: agentmesh.v1.AdminStatusResponse.node_catalog:type_name -> agentmesh.v1.AdminStatusResponse.NodeCatalogEntry - 62, // 77: agentmesh.v1.UserStatusResponse.user:type_name -> agentmesh.v1.User - 63, // 78: agentmesh.v1.UserStatusResponse.enrolled_nodes:type_name -> agentmesh.v1.EnrolledNode - 58, // 79: agentmesh.v1.UserStatusResponse.bootstrap_tokens:type_name -> agentmesh.v1.BootstrapToken - 64, // 80: agentmesh.v1.UserStatusResponse.active_routers:type_name -> agentmesh.v1.RouterLease - 44, // 81: agentmesh.v1.UserStatusResponse.policy:type_name -> agentmesh.v1.PolicyConfig - 90, // 82: agentmesh.v1.IdentityEvidenceResponse.biscuit_expire_time:type_name -> google.protobuf.Timestamp - 90, // 83: agentmesh.v1.IdentityEvidenceResponse.check_time:type_name -> google.protobuf.Timestamp - 88, // 84: agentmesh.v1.PeerEvidenceResponse.labels:type_name -> agentmesh.v1.PeerEvidenceResponse.LabelsEntry - 90, // 85: agentmesh.v1.PeerEvidenceResponse.expire_time:type_name -> google.protobuf.Timestamp - 90, // 86: agentmesh.v1.PeerEvidenceResponse.check_time:type_name -> google.protobuf.Timestamp - 90, // 87: agentmesh.v1.MemberCredential.expire_time:type_name -> google.protobuf.Timestamp - 71, // 88: agentmesh.v1.MemberCredential.trusted_keys:type_name -> agentmesh.v1.TrustedSigningKey - 72, // 89: agentmesh.v1.MemberCredential.oidc_session:type_name -> agentmesh.v1.OIDCSession - 90, // 90: agentmesh.v1.TrustedSigningKey.receive_time:type_name -> google.protobuf.Timestamp - 74, // 91: agentmesh.v1.TaskAuthorizationRule.rules:type_name -> agentmesh.v1.TaskRule - 90, // 92: agentmesh.v1.TaskAuthorizationRule.expire_time:type_name -> google.protobuf.Timestamp - 75, // 93: agentmesh.v1.TaskRule.operation:type_name -> agentmesh.v1.TaskOperation - 73, // 94: agentmesh.v1.TokenExchangeRequest.task_rule:type_name -> agentmesh.v1.TaskAuthorizationRule - 90, // 95: agentmesh.v1.TokenExchangeResponse.expire_time:type_name -> google.protobuf.Timestamp - 90, // 96: agentmesh.v1.STSTokenResponse.expire_time:type_name -> google.protobuf.Timestamp - 65, // 97: agentmesh.v1.AdminStatusResponse.NodeCatalogEntry.value:type_name -> agentmesh.v1.NodeServices - 98, // [98:98] is the sub-list for method output_type - 98, // [98:98] is the sub-list for method input_type - 98, // [98:98] is the sub-list for extension type_name - 98, // [98:98] is the sub-list for extension extendee - 0, // [0:98] is the sub-list for field type_name + 25, // 18: agentmesh.v1.DirectoryRequest.find_providers:type_name -> agentmesh.v1.DirectoryFindProviders + 26, // 19: agentmesh.v1.DirectoryRequest.find_peer:type_name -> agentmesh.v1.DirectoryFindPeer + 24, // 20: agentmesh.v1.DirectoryRequest.withdraw:type_name -> agentmesh.v1.DirectoryWithdraw + 28, // 21: agentmesh.v1.DirectoryResponse.providers:type_name -> agentmesh.v1.PeerRecord + 28, // 22: agentmesh.v1.DirectoryResponse.peer:type_name -> agentmesh.v1.PeerRecord + 85, // 23: agentmesh.v1.RouterInfo.labels:type_name -> agentmesh.v1.RouterInfo.LabelsEntry + 90, // 24: agentmesh.v1.RouterLeaseRequest.ttl:type_name -> google.protobuf.Duration + 91, // 25: agentmesh.v1.RouterLeaseResponse.expire_time:type_name -> google.protobuf.Timestamp + 33, // 26: agentmesh.v1.PolicyRole.http:type_name -> agentmesh.v1.HTTPGrant + 40, // 27: agentmesh.v1.EgressDestination.broker:type_name -> agentmesh.v1.CredentialBroker + 35, // 28: agentmesh.v1.EgressDestination.inspection:type_name -> agentmesh.v1.Inspection + 2, // 29: agentmesh.v1.EgressDestination.mode:type_name -> agentmesh.v1.EgressMode + 36, // 30: agentmesh.v1.Inspection.inspectors:type_name -> agentmesh.v1.Inspector + 37, // 31: agentmesh.v1.Inspector.model_armor:type_name -> agentmesh.v1.ModelArmor + 38, // 32: agentmesh.v1.Inspector.ext_proc:type_name -> agentmesh.v1.ExtProc + 3, // 33: agentmesh.v1.ModelArmor.response:type_name -> agentmesh.v1.ResponseInspection + 90, // 34: agentmesh.v1.ModelArmor.timeout:type_name -> google.protobuf.Duration + 39, // 35: agentmesh.v1.ExtProc.processing_mode:type_name -> agentmesh.v1.ExtProcProcessingMode + 90, // 36: agentmesh.v1.ExtProc.message_timeout:type_name -> google.protobuf.Duration + 6, // 37: agentmesh.v1.ExtProcProcessingMode.request_header_mode:type_name -> agentmesh.v1.ExtProcProcessingMode.HeaderMode + 6, // 38: agentmesh.v1.ExtProcProcessingMode.response_header_mode:type_name -> agentmesh.v1.ExtProcProcessingMode.HeaderMode + 7, // 39: agentmesh.v1.ExtProcProcessingMode.request_body_mode:type_name -> agentmesh.v1.ExtProcProcessingMode.BodyMode + 7, // 40: agentmesh.v1.ExtProcProcessingMode.response_body_mode:type_name -> agentmesh.v1.ExtProcProcessingMode.BodyMode + 6, // 41: agentmesh.v1.ExtProcProcessingMode.request_trailer_mode:type_name -> agentmesh.v1.ExtProcProcessingMode.HeaderMode + 6, // 42: agentmesh.v1.ExtProcProcessingMode.response_trailer_mode:type_name -> agentmesh.v1.ExtProcProcessingMode.HeaderMode + 41, // 43: agentmesh.v1.CredentialBroker.oidc_federation:type_name -> agentmesh.v1.OIDCFederation + 42, // 44: agentmesh.v1.CredentialBroker.aws_assume_role:type_name -> agentmesh.v1.AWSAssumeRole + 43, // 45: agentmesh.v1.CredentialBroker.platform_identity:type_name -> agentmesh.v1.PlatformIdentity + 32, // 46: agentmesh.v1.PolicyConfig.roles:type_name -> agentmesh.v1.PolicyRole + 44, // 47: agentmesh.v1.PolicyConfig.bindings:type_name -> agentmesh.v1.PolicyBinding + 34, // 48: agentmesh.v1.PolicyConfig.egress:type_name -> agentmesh.v1.EgressDestination + 34, // 49: agentmesh.v1.EgressAssignmentsResponse.egress:type_name -> agentmesh.v1.EgressDestination + 91, // 50: agentmesh.v1.KeysResponse.sign_time:type_name -> google.protobuf.Timestamp + 91, // 51: agentmesh.v1.TokenRefreshResponse.expire_time:type_name -> google.protobuf.Timestamp + 16, // 52: agentmesh.v1.NodeCatalogReport.services:type_name -> agentmesh.v1.ServiceInfo + 91, // 53: agentmesh.v1.BootstrapTokenCreateResponse.expire_time:type_name -> google.protobuf.Timestamp + 91, // 54: agentmesh.v1.BootstrapToken.create_time:type_name -> google.protobuf.Timestamp + 91, // 55: agentmesh.v1.BootstrapToken.expire_time:type_name -> google.protobuf.Timestamp + 91, // 56: agentmesh.v1.BootstrapToken.revoke_time:type_name -> google.protobuf.Timestamp + 59, // 57: agentmesh.v1.BootstrapTokenListResponse.tokens:type_name -> agentmesh.v1.BootstrapToken + 0, // 58: agentmesh.v1.EnrollmentRequest.status:type_name -> agentmesh.v1.EnrollmentStatus + 86, // 59: agentmesh.v1.EnrollmentRequest.labels:type_name -> agentmesh.v1.EnrollmentRequest.LabelsEntry + 91, // 60: agentmesh.v1.EnrollmentRequest.create_time:type_name -> google.protobuf.Timestamp + 91, // 61: agentmesh.v1.EnrollmentRequest.resolve_time:type_name -> google.protobuf.Timestamp + 61, // 62: agentmesh.v1.EnrollmentRequestListResponse.requests:type_name -> agentmesh.v1.EnrollmentRequest + 91, // 63: agentmesh.v1.User.create_time:type_name -> google.protobuf.Timestamp + 87, // 64: agentmesh.v1.EnrolledNode.labels:type_name -> agentmesh.v1.EnrolledNode.LabelsEntry + 91, // 65: agentmesh.v1.EnrolledNode.enroll_time:type_name -> google.protobuf.Timestamp + 91, // 66: agentmesh.v1.EnrolledNode.expire_time:type_name -> google.protobuf.Timestamp + 91, // 67: agentmesh.v1.RouterLease.last_renewal_time:type_name -> google.protobuf.Timestamp + 91, // 68: agentmesh.v1.RouterLease.expire_time:type_name -> google.protobuf.Timestamp + 16, // 69: agentmesh.v1.NodeServices.services:type_name -> agentmesh.v1.ServiceInfo + 91, // 70: agentmesh.v1.NodeServices.report_time:type_name -> google.protobuf.Timestamp + 63, // 71: agentmesh.v1.AdminStatusResponse.users:type_name -> agentmesh.v1.User + 65, // 72: agentmesh.v1.AdminStatusResponse.active_routers:type_name -> agentmesh.v1.RouterLease + 64, // 73: agentmesh.v1.AdminStatusResponse.enrolled_nodes:type_name -> agentmesh.v1.EnrolledNode + 61, // 74: agentmesh.v1.AdminStatusResponse.enrollment_requests:type_name -> agentmesh.v1.EnrollmentRequest + 59, // 75: agentmesh.v1.AdminStatusResponse.bootstrap_tokens:type_name -> agentmesh.v1.BootstrapToken + 45, // 76: agentmesh.v1.AdminStatusResponse.policy:type_name -> agentmesh.v1.PolicyConfig + 88, // 77: agentmesh.v1.AdminStatusResponse.node_catalog:type_name -> agentmesh.v1.AdminStatusResponse.NodeCatalogEntry + 63, // 78: agentmesh.v1.UserStatusResponse.user:type_name -> agentmesh.v1.User + 64, // 79: agentmesh.v1.UserStatusResponse.enrolled_nodes:type_name -> agentmesh.v1.EnrolledNode + 59, // 80: agentmesh.v1.UserStatusResponse.bootstrap_tokens:type_name -> agentmesh.v1.BootstrapToken + 65, // 81: agentmesh.v1.UserStatusResponse.active_routers:type_name -> agentmesh.v1.RouterLease + 45, // 82: agentmesh.v1.UserStatusResponse.policy:type_name -> agentmesh.v1.PolicyConfig + 91, // 83: agentmesh.v1.IdentityEvidenceResponse.biscuit_expire_time:type_name -> google.protobuf.Timestamp + 91, // 84: agentmesh.v1.IdentityEvidenceResponse.check_time:type_name -> google.protobuf.Timestamp + 89, // 85: agentmesh.v1.PeerEvidenceResponse.labels:type_name -> agentmesh.v1.PeerEvidenceResponse.LabelsEntry + 91, // 86: agentmesh.v1.PeerEvidenceResponse.expire_time:type_name -> google.protobuf.Timestamp + 91, // 87: agentmesh.v1.PeerEvidenceResponse.check_time:type_name -> google.protobuf.Timestamp + 91, // 88: agentmesh.v1.MemberCredential.expire_time:type_name -> google.protobuf.Timestamp + 72, // 89: agentmesh.v1.MemberCredential.trusted_keys:type_name -> agentmesh.v1.TrustedSigningKey + 73, // 90: agentmesh.v1.MemberCredential.oidc_session:type_name -> agentmesh.v1.OIDCSession + 91, // 91: agentmesh.v1.TrustedSigningKey.receive_time:type_name -> google.protobuf.Timestamp + 75, // 92: agentmesh.v1.TaskAuthorizationRule.rules:type_name -> agentmesh.v1.TaskRule + 91, // 93: agentmesh.v1.TaskAuthorizationRule.expire_time:type_name -> google.protobuf.Timestamp + 76, // 94: agentmesh.v1.TaskRule.operation:type_name -> agentmesh.v1.TaskOperation + 74, // 95: agentmesh.v1.TokenExchangeRequest.task_rule:type_name -> agentmesh.v1.TaskAuthorizationRule + 91, // 96: agentmesh.v1.TokenExchangeResponse.expire_time:type_name -> google.protobuf.Timestamp + 91, // 97: agentmesh.v1.STSTokenResponse.expire_time:type_name -> google.protobuf.Timestamp + 66, // 98: agentmesh.v1.AdminStatusResponse.NodeCatalogEntry.value:type_name -> agentmesh.v1.NodeServices + 99, // [99:99] is the sub-list for method output_type + 99, // [99:99] is the sub-list for method input_type + 99, // [99:99] is the sub-list for extension type_name + 99, // [99:99] is the sub-list for extension extendee + 0, // [0:99] is the sub-list for field type_name } func init() { file_api_agentmesh_proto_init() } @@ -6596,12 +6678,13 @@ func file_api_agentmesh_proto_init() { (*DirectoryRequest_Provide)(nil), (*DirectoryRequest_FindProviders)(nil), (*DirectoryRequest_FindPeer)(nil), + (*DirectoryRequest_Withdraw)(nil), } - file_api_agentmesh_proto_msgTypes[27].OneofWrappers = []any{ + file_api_agentmesh_proto_msgTypes[28].OneofWrappers = []any{ (*Inspector_ModelArmor)(nil), (*Inspector_ExtProc)(nil), } - file_api_agentmesh_proto_msgTypes[31].OneofWrappers = []any{ + file_api_agentmesh_proto_msgTypes[32].OneofWrappers = []any{ (*CredentialBroker_StaticSecret)(nil), (*CredentialBroker_OidcFederation)(nil), (*CredentialBroker_AwsAssumeRole)(nil), @@ -6613,7 +6696,7 @@ func file_api_agentmesh_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_agentmesh_proto_rawDesc), len(file_api_agentmesh_proto_rawDesc)), NumEnums: 8, - NumMessages: 81, + NumMessages: 82, NumExtensions: 0, NumServices: 0, }, diff --git a/api/agentmesh.proto b/api/agentmesh.proto index e0a0f83b..c855a0b0 100644 --- a/api/agentmesh.proto +++ b/api/agentmesh.proto @@ -222,28 +222,42 @@ message DirectoryParameters { // DirectoryRequest is what a member asks a router it is attached to over // /mesh/directory/1.0.0: one request per stream, answered with one -// DirectoryResponse. The member keeps no DHT of its own; the router runs -// the lookup across the routers' DHT and answers from it. A router sends -// the same message to another router to place a member's record on it. +// DirectoryResponse. The member keeps no DHT of its own; the router keeps +// its records, places them on the routers closest to each key, and answers +// a lookup from its own records and those routers'. A router sends the +// same message to another router to place a member's record, or its +// withdrawal, on it. message DirectoryRequest { oneof request { DirectoryProvide provide = 1; DirectoryFindProviders find_providers = 2; DirectoryFindPeer find_peer = 3; + DirectoryWithdraw withdraw = 4; } } // DirectoryProvide announces the sender as a provider of key, reached at // addrs. A key is a sha256 multihash: of "mesh:service:[:]" for -// a service, of "mesh:peer:" for the member itself. provider is set -// only between routers, by the router that holds the member, and refused -// from anyone else: a member can only announce itself. +// a service, of "mesh:peer:" for the member itself. A record lives +// for the mesh's record TTL after its last announcement; announcing it +// again replaces its addresses. provider is set only between routers, by +// the router that holds the member, and refused from anyone else: a member +// can only announce itself. message DirectoryProvide { bytes key = 1; repeated string addrs = 2; string provider = 3; } +// DirectoryWithdraw removes the sender's record under key, from the router +// asked and from the routers closest to the key: what a member sends when +// it stops a service or leaves the mesh, so that callers do not find it +// until the record would have expired. provider is as in DirectoryProvide. +message DirectoryWithdraw { + bytes key = 1; + string provider = 2; +} + // DirectoryFindProviders asks for up to limit providers of key. message DirectoryFindProviders { bytes key = 1; diff --git a/go.mod b/go.mod index bd940f35..85aaa680 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( github.com/envoyproxy/go-control-plane/envoy v1.39.0 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/hashicorp/golang-lru/v2 v2.0.7 - github.com/ipfs/go-cid v0.6.2 github.com/ipfs/go-log/v2 v2.9.2 github.com/jackc/pgx/v5 v5.11.0 github.com/libp2p/go-libp2p v0.49.0 @@ -67,6 +66,7 @@ require ( github.com/huin/goupnp v1.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/ipfs/boxo v0.41.0 // indirect + github.com/ipfs/go-cid v0.6.2 // indirect github.com/ipfs/go-datastore v0.9.2 // indirect github.com/ipld/go-ipld-prime v0.24.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect diff --git a/internal/node/directory_client.go b/internal/node/directory_client.go index 78903823..5d0e44b8 100644 --- a/internal/node/directory_client.go +++ b/internal/node/directory_client.go @@ -44,6 +44,9 @@ type Directory interface { // addresses it announces, to a router it is attached to; the router // keeps the record and places it on the routers closest to the key. Provide(ctx context.Context, key []byte) error + // Withdraw removes this member's record under key from the router it is + // attached to and from the routers the record was placed on. + Withdraw(ctx context.Context, key []byte) error // FindProviders is who provides key, up to limit, with their addresses. FindProviders(ctx context.Context, key []byte, limit int) ([]peer.AddrInfo, error) // FindPeer is where p is reached; routing.ErrNotFound when nobody knows. @@ -52,8 +55,8 @@ type Directory interface { const ( // directoryRequestTimeout bounds one request to one router: the router's - // lookup across the mesh included, which is one round between routers - // that all know each other. + // lookup on the routers closest to the key included, which is one round + // between routers that all know each other. directoryRequestTimeout = 10 * time.Second directoryMaxMessage = 64 * 1024 directoryDefaultLimit = 20 @@ -80,6 +83,7 @@ var directoryRequestSeconds = promauto.NewHistogramVec( const ( directoryOpProvide = "provide" + directoryOpWithdraw = "withdraw" directoryOpFindProviders = "find_providers" directoryOpFindPeer = "find_peer" @@ -89,7 +93,7 @@ const ( ) func init() { - for _, op := range []string{directoryOpProvide, directoryOpFindProviders, directoryOpFindPeer} { + for _, op := range []string{directoryOpProvide, directoryOpWithdraw, directoryOpFindProviders, directoryOpFindPeer} { for _, result := range []string{directoryResultOK, directoryResultEmpty, directoryResultFailed} { directoryRequestsTotal.WithLabelValues(op, result) } @@ -126,6 +130,11 @@ func (d *routerDirectory) Provide(ctx context.Context, key []byte) error { return nil } +func (d *routerDirectory) Withdraw(ctx context.Context, key []byte) error { + _, err := d.request(ctx, directoryOpWithdraw, &api.DirectoryRequest{Request: &api.DirectoryRequest_Withdraw{Withdraw: &api.DirectoryWithdraw{Key: key}}}) + return err +} + // Announced is the addresses of the record this member last announced: // what the directory holds for it, as against what it would announce now. func (d *routerDirectory) Announced() []multiaddr.Multiaddr { @@ -195,7 +204,7 @@ func (d *routerDirectory) request(ctx context.Context, op string, req *api.Direc resp, err := d.ask(ctx, router, data) if err == nil { result := directoryResultOK - if op != directoryOpProvide && len(resp.GetProviders()) == 0 && resp.GetPeer() == nil { + if op != directoryOpProvide && op != directoryOpWithdraw && len(resp.GetProviders()) == 0 && resp.GetPeer() == nil { result = directoryResultEmpty } directoryRequestsTotal.WithLabelValues(op, result).Inc() diff --git a/internal/node/directory_client_test.go b/internal/node/directory_client_test.go index dc295d53..52e08f47 100644 --- a/internal/node/directory_client_test.go +++ b/internal/node/directory_client_test.go @@ -122,6 +122,8 @@ func (d *fakeDirectoryServer) handle(s network.Stream) { } d.records[string(q.GetKey())][s.Conn().RemotePeer()] = addrs d.provided = append(d.provided, q.GetKey()) + case req.GetWithdraw() != nil: + delete(d.records[string(req.GetWithdraw().GetKey())], s.Conn().RemotePeer()) case req.GetFindProviders() != nil: for p, addrs := range d.records[string(req.GetFindProviders().GetKey())] { resp.Providers = append(resp.Providers, recordOf(p, addrs)) diff --git a/internal/node/node.go b/internal/node/node.go index 5b5673b5..9e89ed6a 100644 --- a/internal/node/node.go +++ b/internal/node/node.go @@ -2023,9 +2023,22 @@ func (n *AgentMeshNode) UnregisterService(ctx context.Context, serviceName strin return n.services.Unregister(ctx, serviceName) } -// Teardown detaches all registered services and closes the libp2p host. -// Store is owned by the caller and is not closed here. +// withdrawTimeout bounds what a node leaving the mesh waits for its +// records to be withdrawn: a few round trips to the routers it holds. +const withdrawTimeout = 5 * time.Second + +// Teardown withdraws the node's records from the directory, detaches all +// registered services and closes the libp2p host. Store is owned by the +// caller and is not closed here. func (n *AgentMeshNode) Teardown() error { + if n.services != nil && n.Directory != nil && n.Host != nil { + ctx, cancel := context.WithTimeout(context.Background(), withdrawTimeout) + n.services.WithdrawAll(ctx) + if err := n.Directory.Withdraw(ctx, api.PeerKey(n.Host.ID())); err != nil && !errors.Is(err, errNoDirectoryRouter) { + logger.Warnf("[Directory] Withdrawing this node's record: %v", err) + } + cancel() + } if n.services != nil { n.services.TeardownAll() } diff --git a/internal/node/service_registry.go b/internal/node/service_registry.go index 4bccb4f2..07e2d90a 100644 --- a/internal/node/service_registry.go +++ b/internal/node/service_registry.go @@ -16,6 +16,7 @@ package node import ( "context" + "errors" "fmt" "sync" "sync/atomic" @@ -24,10 +25,12 @@ import ( "github.com/google/agentmesh/api" ) -// directoryProvider is the one directory call ServiceRegistry makes. -// Production wires this to the node's Directory; tests use a fake. +// directoryProvider is what ServiceRegistry asks of the directory: to +// announce a key and to withdraw it. Production wires this to the node's +// Directory; tests use a fake. type directoryProvider interface { Provide(ctx context.Context, key []byte) error + Withdraw(ctx context.Context, key []byte) error } // backendProber is implemented by services that can be asked whether their @@ -183,16 +186,31 @@ func (r *ServiceRegistry) Register(ctx context.Context, svc Service) error { return nil } -// Unregister removes the service from the map and calls Teardown. -// Unknown names are a no-op. +// Unregister removes the service from the map, withdraws its records from +// the directory and calls Teardown. The record by type stays while another +// service of that type is registered. Unknown names are a no-op. func (r *ServiceRegistry) Unregister(ctx context.Context, name string) error { r.mu.Lock() svc, ok := r.services[name] delete(r.services, name) + typeStillServed := false + if ok { + for _, other := range r.services { + if other.Info().Type == svc.Info().Type { + typeStillServed = true + break + } + } + } r.mu.Unlock() if !ok { return nil } + info := svc.Info() + r.withdraw(ctx, info.Type, info.Name) + if !typeStillServed { + r.withdraw(ctx, info.Type, "") + } if err := svc.Teardown(); err != nil { logger.Errorf("[ServiceRegistry] Teardown %s: %v", name, err) } @@ -200,6 +218,40 @@ func (r *ServiceRegistry) Unregister(ctx context.Context, name string) error { return nil } +// withdraw removes one of the registry's records from the directory. A +// failure is logged: the record expires on its own within the mesh's +// record TTL. +func (r *ServiceRegistry) withdraw(ctx context.Context, serviceType api.ServiceType, name string) { + key, err := api.ServiceKey(serviceType, name) + if err != nil { + return + } + withdrawCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + if err := r.dht.Withdraw(withdrawCtx, key); err != nil && !errors.Is(err, errNoDirectoryRouter) { + logger.Warnf("[ServiceRegistry] Withdrawing %s/%s from the directory: %v", serviceType, name, err) + } +} + +// WithdrawAll withdraws every registered service's records from the +// directory, by name and by type, as a node does before it leaves the mesh. +func (r *ServiceRegistry) WithdrawAll(ctx context.Context) { + r.mu.RLock() + infos := make([]*api.ServiceInfo, 0, len(r.services)) + for _, svc := range r.services { + infos = append(infos, svc.Info()) + } + r.mu.RUnlock() + types := map[api.ServiceType]bool{} + for _, info := range infos { + r.withdraw(ctx, info.Type, info.Name) + types[info.Type] = true + } + for t := range types { + r.withdraw(ctx, t, "") + } +} + // Get returns the service registered under name, if any. func (r *ServiceRegistry) Get(name string) (Service, bool) { r.mu.RLock() diff --git a/internal/node/service_registry_test.go b/internal/node/service_registry_test.go index 9349e74f..01239131 100644 --- a/internal/node/service_registry_test.go +++ b/internal/node/service_registry_test.go @@ -15,6 +15,7 @@ package node import ( + "bytes" "context" "errors" "net/http" @@ -27,9 +28,10 @@ import ( ) type fakeDHT struct { - calls [][]byte - failNth int - count int32 + calls [][]byte + withdrawn [][]byte + failNth int + count int32 } func (f *fakeDHT) Provide(ctx context.Context, key []byte) error { @@ -41,6 +43,11 @@ func (f *fakeDHT) Provide(ctx context.Context, key []byte) error { return nil } +func (f *fakeDHT) Withdraw(ctx context.Context, key []byte) error { + f.withdrawn = append(f.withdrawn, key) + return nil +} + type fakeService struct { info *api.ServiceInfo initCalls int @@ -422,3 +429,59 @@ func TestServiceRegistry_ReplacingServiceTearsDownPrevious(t *testing.T) { t.Error("registry should hold the replacement instance") } } + +// Unregistering a service withdraws its record by name, and its record by +// type only once no service of that type is left; leaving the mesh +// withdraws every record. +func TestServiceRegistry_UnregisterWithdrawsRecords(t *testing.T) { + dht := &fakeDHT{} + r := newServiceRegistryForTest(dht) + for _, name := range []string{"calc", "weather"} { + if err := r.Register(context.Background(), newFakeSvc(name, api.ServiceType_SERVICE_TYPE_MCP)); err != nil { + t.Fatalf("Register %s: %v", name, err) + } + } + calcKey, _ := api.ServiceKey(api.ServiceType_SERVICE_TYPE_MCP, "calc") + weatherKey, _ := api.ServiceKey(api.ServiceType_SERVICE_TYPE_MCP, "weather") + typeKey, _ := api.ServiceKey(api.ServiceType_SERVICE_TYPE_MCP, "") + + if err := r.Unregister(context.Background(), "calc"); err != nil { + t.Fatal(err) + } + if len(dht.withdrawn) != 1 || !bytes.Equal(dht.withdrawn[0], calcKey) { + t.Fatalf("withdrawn after unregistering calc = %d keys, want calc's alone: weather still serves the type", len(dht.withdrawn)) + } + if err := r.Unregister(context.Background(), "weather"); err != nil { + t.Fatal(err) + } + if len(dht.withdrawn) != 3 || !bytes.Equal(dht.withdrawn[1], weatherKey) || !bytes.Equal(dht.withdrawn[2], typeKey) { + t.Fatalf("withdrawn after unregistering weather = %d keys, want weather's then the type's", len(dht.withdrawn)) + } + if err := r.Unregister(context.Background(), "nobody"); err != nil || len(dht.withdrawn) != 3 { + t.Fatalf("unregistering an unknown service: err %v, %d withdrawals; want a no-op", err, len(dht.withdrawn)) + } + + dht.withdrawn = nil + if err := r.Register(context.Background(), newFakeSvc("calc", api.ServiceType_SERVICE_TYPE_MCP)); err != nil { + t.Fatal(err) + } + if err := r.Register(context.Background(), newFakeSvc("llm", api.ServiceType_SERVICE_TYPE_INFERENCE)); err != nil { + t.Fatal(err) + } + r.WithdrawAll(context.Background()) + inferenceKey, _ := api.ServiceKey(api.ServiceType_SERVICE_TYPE_INFERENCE, "") + llmKey, _ := api.ServiceKey(api.ServiceType_SERVICE_TYPE_INFERENCE, "llm") + want := map[string]bool{string(calcKey): true, string(typeKey): true, string(llmKey): true, string(inferenceKey): true} + got := map[string]bool{} + for _, k := range dht.withdrawn { + got[string(k)] = true + } + if len(dht.withdrawn) != 4 || len(got) != 4 { + t.Fatalf("WithdrawAll withdrew %d keys, want 4 distinct: two names, two types", len(dht.withdrawn)) + } + for k := range want { + if !got[k] { + t.Fatalf("WithdrawAll did not withdraw %x", k) + } + } +} diff --git a/internal/router/dht.go b/internal/router/dht.go index 44a5a4a5..56956a55 100644 --- a/internal/router/dht.go +++ b/internal/router/dht.go @@ -19,28 +19,24 @@ import ( "time" dht "github.com/libp2p/go-libp2p-kad-dht" - "github.com/libp2p/go-libp2p-kad-dht/records" "github.com/libp2p/go-libp2p/core/peer" "github.com/google/agentmesh/api" ) // dhtTableBucketSize is kademlia's k on a router: how many routers one -// bucket of its table holds, and how many closest routers it names in a -// FIND_NODE answer. The routers are a full mesh, so a table that holds them -// all costs nothing, and it is what lets a member find the routers closest -// to a key in one answer from a router it is attached to (node.dhtOptions): -// the answer lists the closest routers in order and the member keeps the -// first 2r. A mesh with more routers than this per bucket still works; a -// lookup takes a hop more. +// bucket of its table holds. The routers are a full mesh, so a table that +// holds them all costs nothing, and it is what lets a router name the +// routers closest to a key from its own table (closestRouters). A mesh with +// more routers than this per bucket still works; the closest routers are +// then the closest the table knows. const dhtTableBucketSize = 64 -// dhtOptions configures the router's DHT: a server for the members and the -// other routers, with every router in its table. The record TTL is the -// mesh's (syncDirectory): three of the members' reprovide intervals, so a -// node that is gone drops out of discovery within it and two missed -// announcements do not unlist one that is not. The library default (48h) -// would keep every past identity listed. +// dhtOptions configures the router's DHT: a server among the other +// routers, with every router in its table. The table is what the directory +// uses: the routers closest to a key are where a record is placed and +// where a lookup asks (closestRouters). The records themselves are kept +// in recordStore, not in the DHT's provider store. // // Its table holds, and its lookups ask, authenticated peer routers only: a // peer in server mode that never authenticated would otherwise be handed @@ -51,7 +47,6 @@ func (r *Router) dhtOptions() []dht.Option { dht.Mode(dht.ModeServer), dht.ProtocolPrefix(api.DHTProtocolPrefix), dht.BucketSize(dhtTableBucketSize), - dht.ProviderManagerOpts(records.ProviderAddrTTL(r.directory.RecordTTL), records.ProvideValidity(r.directory.RecordTTL)), dht.RoutingTableFilter(func(_ interface{}, p peer.ID) bool { return r.isAuthenticatedPeerRouter(p) }), dht.QueryFilter(func(_ interface{}, ai peer.AddrInfo) bool { return r.isAuthenticatedPeerRouter(ai.ID) }), } diff --git a/internal/router/directory.go b/internal/router/directory.go index 78196e29..10d14bd0 100644 --- a/internal/router/directory.go +++ b/internal/router/directory.go @@ -21,7 +21,6 @@ import ( "sync" "time" - "github.com/ipfs/go-cid" kb "github.com/libp2p/go-libp2p-kbucket" "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" @@ -35,10 +34,13 @@ import ( ) const ( - // directoryRequestTimeout bounds one request, the lookup across the - // routers included. A lookup on a mesh of routers that all know each - // other is one round; the bound is for a router that stopped answering. + // directoryRequestTimeout bounds one request from a member, the lookup + // on the routers closest to the key included. directoryRequestTimeout = 20 * time.Second + // directoryLookupTimeout bounds what a lookup waits for one peer router: + // the routers are a full mesh, so an answer is one round trip, and a + // router that does not answer in this time is left out of the answer. + directoryLookupTimeout = 5 * time.Second // directoryMaxMessage bounds a request or response frame. directoryMaxMessage = 64 * 1024 // directoryDefaultLimit and directoryMaxLimit bound how many providers @@ -49,9 +51,11 @@ const ( const ( directoryOpProvide = "provide" + directoryOpWithdraw = "withdraw" directoryOpFindProviders = "find_providers" directoryOpFindPeer = "find_peer" directoryOpPlace = "place" + directoryOpLookup = "lookup" directoryResultOK = "ok" directoryResultEmpty = "empty" @@ -62,7 +66,7 @@ const ( var directoryRequestsTotal = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "agentmesh_router_directory_requests_total", - Help: "Directory requests served to members and routers, by operation and result; place is a record another router put here", + Help: "Directory requests served to members and routers, by operation and result; place is a record or withdrawal another router put here, lookup a find another router ran here", }, []string{"op", "result"}, ) @@ -70,7 +74,7 @@ var directoryRequestsTotal = promauto.NewCounterVec( var directoryRequestSeconds = promauto.NewHistogramVec( prometheus.HistogramOpts{ Name: "agentmesh_router_directory_request_seconds", - Help: "Time to answer a directory request, the lookup across the routers included", + Help: "Time to answer a directory request, the lookup on the closest routers included", Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10}, }, []string{"op"}, @@ -79,28 +83,39 @@ var directoryRequestSeconds = promauto.NewHistogramVec( var directoryPlacementsTotal = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "agentmesh_router_directory_placements_total", - Help: "Copies of a member's record this router placed on the routers closest to the key, by result", + Help: "Copies of a member's record, or of its withdrawal, this router placed on the routers closest to the key, by result", + }, + []string{"result"}, +) + +var directoryLookupsTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "agentmesh_router_directory_lookups_total", + Help: "Peer routers asked during a lookup, by result; error is a router that did not answer in time", }, []string{"result"}, ) func init() { - for _, op := range []string{directoryOpProvide, directoryOpFindProviders, directoryOpFindPeer, directoryOpPlace} { + for _, op := range []string{directoryOpProvide, directoryOpWithdraw, directoryOpFindProviders, directoryOpFindPeer, directoryOpPlace, directoryOpLookup} { for _, result := range []string{directoryResultOK, directoryResultEmpty, directoryResultInvalid, directoryResultError} { directoryRequestsTotal.WithLabelValues(op, result) } } for _, result := range []string{directoryResultOK, directoryResultError} { directoryPlacementsTotal.WithLabelValues(result) + directoryLookupsTotal.WithLabelValues(result) } } +var errDirectoryNotUp = errors.New("the directory is not up") + // handleDirectory serves one DirectoryRequest on a stream from a member // or a router. It is registered through the auth gate, so the peer has // authenticated by the time it runs. A member keeps no DHT: it announces -// its records here and the router keeps them and places them on the -// routers closest to the key; it looks keys up here and the router runs -// the lookup across the routers' DHT. +// and withdraws its records here, and the router keeps them and places +// them on the routers closest to the key; it looks keys up here, and the +// router asks those routers. func (r *Router) handleDirectory(s network.Stream) { defer func() { _ = s.Close() }() remote := s.Conn().RemotePeer() @@ -151,12 +166,24 @@ func (r *Router) serveDirectory(ctx context.Context, remote peer.ID, req *api.Di op = directoryOpPlace } resp, err = r.directoryProvide(ctx, remote, q.Provide) + case *api.DirectoryRequest_Withdraw: + op = directoryOpWithdraw + if q.Withdraw.GetProvider() != "" { + op = directoryOpPlace + } + resp, err = r.directoryWithdraw(ctx, remote, q.Withdraw) case *api.DirectoryRequest_FindProviders: op = directoryOpFindProviders - resp, err = r.directoryFindProviders(ctx, q.FindProviders) + if r.isAuthenticatedPeerRouter(remote) { + op = directoryOpLookup + } + resp, err = r.directoryFindProviders(ctx, remote, q.FindProviders) case *api.DirectoryRequest_FindPeer: op = directoryOpFindPeer - resp, err = r.directoryFindPeer(ctx, q.FindPeer) + if r.isAuthenticatedPeerRouter(remote) { + op = directoryOpLookup + } + resp, err = r.directoryFindPeer(ctx, remote, q.FindPeer) default: return "unknown", r.directoryRefused("unknown", remote, errInvalidDirectoryRequest) } @@ -165,14 +192,14 @@ func (r *Router) serveDirectory(ctx context.Context, remote peer.ID, req *api.Di return op, r.directoryRefused(op, remote, err) } result := directoryResultOK - if len(resp.GetProviders()) == 0 && resp.GetPeer() == nil && op != directoryOpProvide && op != directoryOpPlace { + if len(resp.GetProviders()) == 0 && resp.GetPeer() == nil && (op == directoryOpFindProviders || op == directoryOpFindPeer || op == directoryOpLookup) { result = directoryResultEmpty } directoryRequestsTotal.WithLabelValues(op, result).Inc() return op, resp } -var errInvalidDirectoryRequest = errors.New("request is not provide, find_providers or find_peer") +var errInvalidDirectoryRequest = errors.New("request is not provide, withdraw, find_providers or find_peer") // directoryRefused answers a request the router did not run, and says why // at the level the cause deserves: a malformed request is the peer's @@ -199,38 +226,72 @@ func invalidDirectoryRequest(format string, args ...any) error { return &directoryInvalidError{err: fmt.Errorf(format, args...)} } +// recordProvider is whose record a provide or withdraw is about: the +// sender's own, or the member a peer router names, which only a router may. +func (r *Router) recordProvider(remote peer.ID, named string) (provider peer.ID, placed bool, err error) { + if named == "" { + return remote, false, nil + } + if !r.isAuthenticatedPeerRouter(remote) { + return "", false, invalidDirectoryRequest("only a router may name a provider; %s is not one", remote) + } + p, err := peer.Decode(named) + if err != nil { + return "", false, invalidDirectoryRequest("provider: %w", err) + } + return p, true, nil +} + // directoryProvide stores a record. From a member it is the member's own, // kept here and placed on the routers closest to the key; from a router it -// is a member's record that router placed here, which only a router may do. +// is a member's record that router placed here. A new record for the same +// key and provider replaces the addresses of the old one. func (r *Router) directoryProvide(ctx context.Context, remote peer.ID, q *api.DirectoryProvide) (*api.DirectoryResponse, error) { if err := api.ValidateDirectoryKey(q.GetKey()); err != nil { return nil, invalidDirectoryRequest("%w", err) } - provider := remote - placed := q.GetProvider() != "" - if placed { - if !r.isAuthenticatedPeerRouter(remote) { - return nil, invalidDirectoryRequest("only a router may name a provider; %s is not one", remote) - } - p, err := peer.Decode(q.GetProvider()) - if err != nil { - return nil, invalidDirectoryRequest("provider: %w", err) - } - provider = p + provider, placed, err := r.recordProvider(remote, q.GetProvider()) + if err != nil { + return nil, err } addrs, err := recordAddrs(provider, q.GetAddrs()) if err != nil { return nil, invalidDirectoryRequest("%w", err) } - if r.DHT == nil { - return nil, errors.New("the directory is not up") + if r.records == nil { + return nil, errDirectoryNotUp + } + r.records.put(q.GetKey(), provider, addrs, time.Now()) + if !placed { + r.placeRecord(ctx, provider, &api.DirectoryRequest{Request: &api.DirectoryRequest_Provide{Provide: &api.DirectoryProvide{ + Key: q.GetKey(), Addrs: q.GetAddrs(), Provider: provider.String(), + }}}) } - if err := r.DHT.ProviderStore().AddProvider(ctx, q.GetKey(), peer.AddrInfo{ID: provider, Addrs: addrs}); err != nil { - return nil, fmt.Errorf("store record: %w", err) + return &api.DirectoryResponse{}, nil +} + +// directoryWithdraw removes a record: a member's own, from here and from +// the routers closest to the key, or one a peer router withdraws for a +// member it holds. Withdrawing a record that is not here is not an error: +// it may have expired, or never have been placed here. +func (r *Router) directoryWithdraw(ctx context.Context, remote peer.ID, q *api.DirectoryWithdraw) (*api.DirectoryResponse, error) { + if err := api.ValidateDirectoryKey(q.GetKey()); err != nil { + return nil, invalidDirectoryRequest("%w", err) + } + provider, placed, err := r.recordProvider(remote, q.GetProvider()) + if err != nil { + return nil, err + } + if r.records == nil { + return nil, errDirectoryNotUp + } + if !r.records.remove(q.GetKey(), provider) { + logger.Debugf("[Directory] %s withdrew a record this router did not hold", provider) } if !placed { - r.ensureFederated(ctx) - r.placeRecord(ctx, q.GetKey(), provider, q.GetAddrs()) + r.placeRecord(ctx, provider, &api.DirectoryRequest{Request: &api.DirectoryRequest_Withdraw{Withdraw: &api.DirectoryWithdraw{ + Key: q.GetKey(), Provider: provider.String(), + }}}) } return &api.DirectoryResponse{}, nil } @@ -260,26 +321,39 @@ func recordAddrs(provider peer.ID, raw []string) ([]multiaddr.Multiaddr, error) return addrs, nil } -// placeRecord puts a member's record on the routers closest to the key, -// as many as the mesh's replication, over the directory protocol with the -// provider named. Every copy is attempted; a router that fails to take it -// is logged and counted, the member's own router keeps a copy regardless. -func (r *Router) placeRecord(ctx context.Context, key []byte, provider peer.ID, addrs []string) { - targets := r.DHT.RoutingTable().NearestPeers(kb.ConvertKey(string(key)), r.directory.Replication) - req := &api.DirectoryRequest{Request: &api.DirectoryRequest_Provide{Provide: &api.DirectoryProvide{ - Key: key, Addrs: addrs, Provider: provider.String(), - }}} +// closestRouters is the peer routers closest to key, as many as the +// mesh's replication: where a record is placed and where a lookup asks. +func (r *Router) closestRouters(key []byte) []peer.ID { + if r.DHT == nil { + return nil + } + return r.DHT.RoutingTable().NearestPeers(kb.ConvertKey(string(key)), r.directory.Replication) +} + +// placeRecord puts a member's record, or its withdrawal, on the routers +// closest to the key, over the directory protocol with the provider named. +// Every copy is attempted; a router that fails to take it is logged and +// counted, the member's own router keeps its copy regardless. +func (r *Router) placeRecord(ctx context.Context, provider peer.ID, req *api.DirectoryRequest) { + var key []byte + switch q := req.GetRequest().(type) { + case *api.DirectoryRequest_Provide: + key = q.Provide.GetKey() + case *api.DirectoryRequest_Withdraw: + key = q.Withdraw.GetKey() + } + r.ensureFederated(ctx) data, err := proto.Marshal(req) if err != nil { logger.Errorf("[Directory] Placement for %s not encoded: %v", provider, err) return } var wg sync.WaitGroup - for _, target := range targets { + for _, target := range r.closestRouters(key) { wg.Add(1) go func(target peer.ID) { defer wg.Done() - if err := r.sendDirectory(ctx, target, data); err != nil { + if _, err := r.sendDirectory(ctx, target, data); err != nil { directoryPlacementsTotal.WithLabelValues(directoryResultError).Inc() logger.Warnf("[Directory] Record of %s not placed on router %s: %v", provider, target, err) return @@ -290,12 +364,12 @@ func (r *Router) placeRecord(ctx context.Context, key []byte, provider peer.ID, wg.Wait() } -// sendDirectory sends one encoded request to a peer router and reads its +// sendDirectory sends one encoded request to a peer router and returns its // answer, over the gated host so the router has authenticated first. -func (r *Router) sendDirectory(ctx context.Context, target peer.ID, data []byte) error { +func (r *Router) sendDirectory(ctx context.Context, target peer.ID, data []byte) (*api.DirectoryResponse, error) { s, err := r.gated.NewStream(ctx, target, api.DirectoryProtocolID) if err != nil { - return err + return nil, err } defer func() { _ = s.Close() }() if deadline, ok := ctx.Deadline(); ok { @@ -303,27 +377,27 @@ func (r *Router) sendDirectory(ctx context.Context, target peer.ID, data []byte) } if err := msgio.NewVarintWriter(s).WriteMsg(data); err != nil { _ = s.Reset() - return err + return nil, err } reader := msgio.NewVarintReaderSize(s, directoryMaxMessage) frame, err := reader.ReadMsg() if err != nil { _ = s.Reset() - return err + return nil, err } defer reader.ReleaseMsg(frame) var resp api.DirectoryResponse if err := proto.Unmarshal(frame, &resp); err != nil { - return err + return nil, err } if resp.GetError() != "" { - return errors.New(resp.GetError()) + return nil, errors.New(resp.GetError()) } - return nil + return &resp, nil } -// directoryFindProviders runs the lookup across the routers' DHT. -func (r *Router) directoryFindProviders(ctx context.Context, q *api.DirectoryFindProviders) (*api.DirectoryResponse, error) { +// directoryFindProviders answers who provides a key. +func (r *Router) directoryFindProviders(ctx context.Context, remote peer.ID, q *api.DirectoryFindProviders) (*api.DirectoryResponse, error) { if err := api.ValidateDirectoryKey(q.GetKey()); err != nil { return nil, invalidDirectoryRequest("%w", err) } @@ -334,7 +408,7 @@ func (r *Router) directoryFindProviders(ctx context.Context, q *api.DirectoryFin if limit > directoryMaxLimit { limit = directoryMaxLimit } - records, err := r.lookupProviders(ctx, q.GetKey(), limit) + records, err := r.lookupProviders(ctx, remote, q.GetKey(), limit) if err != nil { return nil, err } @@ -342,44 +416,134 @@ func (r *Router) directoryFindProviders(ctx context.Context, q *api.DirectoryFin } // lookupProviders is the lookup both find_providers and find_peer run. -func (r *Router) lookupProviders(ctx context.Context, key []byte, limit int) ([]*api.PeerRecord, error) { - if r.DHT == nil { - return nil, errors.New("the directory is not up") +// For a member it is this router's records and the routers' closest to +// the key, asked at once; a peer router asking is itself running such a +// lookup, and gets this router's records alone. A lookup that finds +// nothing re-reads the control plane's router list once and asks again: +// a router that joined since the last pass may hold the record. +func (r *Router) lookupProviders(ctx context.Context, remote peer.ID, key []byte, limit int) ([]*api.PeerRecord, error) { + if r.records == nil { + return nil, errDirectoryNotUp + } + if r.isAuthenticatedPeerRouter(remote) { + return r.freshRecords(r.records.providers(key, time.Now()), limit), nil } r.ensureFederated(ctx) - records, err := r.findProviders(ctx, key, limit) - if err == nil && len(records) == 0 && r.refederateIfStale() { - // A router that joined since the last pass may hold the record. - records, err = r.findProviders(ctx, key, limit) + found := r.findProviders(ctx, key, limit) + if len(found) == 0 && r.refederateIfStale() { + found = r.findProviders(ctx, key, limit) } - return records, err + if err := ctx.Err(); err != nil && len(found) == 0 { + return nil, fmt.Errorf("lookup: %w", err) + } + return r.freshRecords(found, limit), nil } -// findProviders runs one lookup across the routers' DHT. -func (r *Router) findProviders(ctx context.Context, key []byte, limit int) ([]*api.PeerRecord, error) { - c := cid.NewCidV1(cid.Raw, key) - records := make([]*api.PeerRecord, 0, limit) - seen := map[peer.ID]bool{} - for info := range r.DHT.FindProvidersAsync(ctx, c, limit) { - if seen[info.ID] { - continue +// findProviders merges this router's records under key with what the +// routers closest to it hold, each asked within directoryLookupTimeout. +func (r *Router) findProviders(ctx context.Context, key []byte, limit int) []peer.AddrInfo { + req := &api.DirectoryRequest{Request: &api.DirectoryRequest_FindProviders{FindProviders: &api.DirectoryFindProviders{Key: key, Limit: uint32(limit)}}} + data, err := proto.Marshal(req) + if err != nil { + logger.Errorf("[Directory] Lookup not encoded: %v", err) + return r.records.providers(key, time.Now()) + } + targets := r.closestRouters(key) + answers := make(chan []peer.AddrInfo, len(targets)) + var wg sync.WaitGroup + for _, target := range targets { + wg.Add(1) + go func(target peer.ID) { + defer wg.Done() + askCtx, cancel := context.WithTimeout(ctx, directoryLookupTimeout) + defer cancel() + resp, err := r.sendDirectory(askCtx, target, data) + if err != nil { + directoryLookupsTotal.WithLabelValues(directoryResultError).Inc() + logger.Warnf("[Directory] Router %s did not answer a lookup: %v", target, err) + return + } + directoryLookupsTotal.WithLabelValues(directoryResultOK).Inc() + var infos []peer.AddrInfo + for _, rec := range resp.GetProviders() { + if info, err := addrInfoOfRecord(rec); err == nil { + infos = append(infos, info) + } + } + answers <- infos + }(target) + } + wg.Wait() + close(answers) + + merged := r.records.providers(key, time.Now()) + index := map[peer.ID]int{} + for i, info := range merged { + index[info.ID] = i + } + for infos := range answers { + for _, info := range infos { + i, seen := index[info.ID] + if !seen { + index[info.ID] = len(merged) + merged = append(merged, info) + continue + } + for _, a := range info.Addrs { + if !containsAddr(merged[i].Addrs, a) { + merged[i].Addrs = append(merged[i].Addrs, a) + } + } } - seen[info.ID] = true - records = append(records, peerRecord(info)) + } + return merged +} + +// freshRecords renders up to limit providers, each at the addresses this +// router sees for it when it holds the provider, which are ahead of the +// record, and at the record's otherwise. +func (r *Router) freshRecords(infos []peer.AddrInfo, limit int) []*api.PeerRecord { + records := make([]*api.PeerRecord, 0, min(limit, len(infos))) + for _, info := range infos { if len(records) >= limit { break } + if r.isPeerAuthenticated(info.ID) && r.Host.Network().Connectedness(info.ID) == network.Connected { + if addrs := r.Host.Peerstore().Addrs(info.ID); len(addrs) > 0 { + info.Addrs = addrs + } + } + records = append(records, peerRecord(info)) } - if err := ctx.Err(); err != nil && len(records) == 0 { - return nil, fmt.Errorf("lookup: %w", err) + return records +} + +// addrInfoOfRecord parses a record another router answered with. +func addrInfoOfRecord(rec *api.PeerRecord) (peer.AddrInfo, error) { + p, err := peer.Decode(rec.GetPeerId()) + if err != nil { + return peer.AddrInfo{}, err + } + addrs, err := recordAddrs(p, rec.GetAddrs()) + if err != nil { + return peer.AddrInfo{}, err + } + return peer.AddrInfo{ID: p, Addrs: addrs}, nil +} + +func containsAddr(addrs []multiaddr.Multiaddr, a multiaddr.Multiaddr) bool { + for _, b := range addrs { + if b.Equal(a) { + return true + } } - return records, nil + return false } // directoryFindPeer answers where a peer is reached: from this router's // own peerstore for a member attached here, from the record the member // announced under its peer key otherwise. -func (r *Router) directoryFindPeer(ctx context.Context, q *api.DirectoryFindPeer) (*api.DirectoryResponse, error) { +func (r *Router) directoryFindPeer(ctx context.Context, remote peer.ID, q *api.DirectoryFindPeer) (*api.DirectoryResponse, error) { p, err := peer.Decode(q.GetPeerId()) if err != nil { return nil, invalidDirectoryRequest("peer_id: %w", err) @@ -389,7 +553,7 @@ func (r *Router) directoryFindPeer(ctx context.Context, q *api.DirectoryFindPeer return &api.DirectoryResponse{Peer: peerRecord(peer.AddrInfo{ID: p, Addrs: addrs})}, nil } } - records, err := r.lookupProviders(ctx, api.PeerKey(p), 1) + records, err := r.lookupProviders(ctx, remote, api.PeerKey(p), 1) if err != nil { return nil, err } diff --git a/internal/router/directory_store.go b/internal/router/directory_store.go new file mode 100644 index 00000000..5063a173 --- /dev/null +++ b/internal/router/directory_store.go @@ -0,0 +1,152 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package router + +import ( + "context" + "slices" + "strings" + "sync" + "time" + + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var directoryRecordsExpiredTotal = promauto.NewCounter(prometheus.CounterOpts{ + Name: "agentmesh_router_directory_records_expired_total", + Help: "Directory records dropped because their provider stopped announcing them for the mesh's record TTL", +}) + +// recordStore is the directory records this router holds: its own members' +// and the copies other routers placed on it, by key and provider. A record +// carries the addresses of its provider's last announcement, which the next +// announcement replaces, and lives for the mesh's record TTL after it; +// a withdrawal removes it at once. +type recordStore struct { + ttl time.Duration + + mu sync.Mutex + keys map[string]map[peer.ID]*directoryRecord +} + +type directoryRecord struct { + addrs []multiaddr.Multiaddr + expires time.Time +} + +func newRecordStore(ttl time.Duration) *recordStore { + return &recordStore{ttl: ttl, keys: map[string]map[peer.ID]*directoryRecord{}} +} + +// put stores or replaces the provider's record under key. +func (s *recordStore) put(key []byte, p peer.ID, addrs []multiaddr.Multiaddr, now time.Time) { + s.mu.Lock() + defer s.mu.Unlock() + providers := s.keys[string(key)] + if providers == nil { + providers = map[peer.ID]*directoryRecord{} + s.keys[string(key)] = providers + } + providers[p] = &directoryRecord{addrs: slices.Clone(addrs), expires: now.Add(s.ttl)} +} + +// remove drops the provider's record under key and reports whether there +// was one. +func (s *recordStore) remove(key []byte, p peer.ID) bool { + s.mu.Lock() + defer s.mu.Unlock() + providers := s.keys[string(key)] + if _, ok := providers[p]; !ok { + return false + } + delete(providers, p) + if len(providers) == 0 { + delete(s.keys, string(key)) + } + return true +} + +// providers is the unexpired records under key, in peer ID order so that +// two routers holding the same records answer in the same order. +func (s *recordStore) providers(key []byte, now time.Time) []peer.AddrInfo { + s.mu.Lock() + defer s.mu.Unlock() + var out []peer.AddrInfo + for p, rec := range s.keys[string(key)] { + if !now.Before(rec.expires) { + continue + } + out = append(out, peer.AddrInfo{ID: p, Addrs: slices.Clone(rec.addrs)}) + } + slices.SortFunc(out, func(a, b peer.AddrInfo) int { return strings.Compare(string(a.ID), string(b.ID)) }) + return out +} + +// sweep drops the records that expired and returns how many. +func (s *recordStore) sweep(now time.Time) int { + s.mu.Lock() + defer s.mu.Unlock() + expired := 0 + for key, providers := range s.keys { + for p, rec := range providers { + if !now.Before(rec.expires) { + delete(providers, p) + expired++ + } + } + if len(providers) == 0 { + delete(s.keys, key) + } + } + return expired +} + +// size is how many records are held, expired ones not yet swept included. +func (s *recordStore) size() int { + s.mu.Lock() + defer s.mu.Unlock() + n := 0 + for _, providers := range s.keys { + n += len(providers) + } + return n +} + +// recordSweepInterval is how often expired records are dropped: a third +// of their TTL, and at most every minute, so a record outlives its TTL by +// little. +func recordSweepInterval(ttl time.Duration) time.Duration { + return max(time.Second, min(ttl/3, time.Minute)) +} + +// runSweep drops expired records every interval until ctx ends. +func (s *recordStore) runSweep(ctx context.Context, interval time.Duration) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case now := <-ticker.C: + if expired := s.sweep(now); expired > 0 { + directoryRecordsExpiredTotal.Add(float64(expired)) + logger.Infof("[Directory] %d record(s) expired; %d held", expired, s.size()) + } + } + } +} diff --git a/internal/router/directory_test.go b/internal/router/directory_test.go index effb2dfa..530a1258 100644 --- a/internal/router/directory_test.go +++ b/internal/router/directory_test.go @@ -43,10 +43,16 @@ func newDirectoryRouter(t *testing.T) *Router { } t.Cleanup(func() { _ = kdht.Close() }) r.DHT = kdht + r.records = newRecordStore(r.directory.RecordTTL) gated.SetStreamHandler(api.DirectoryProtocolID, r.handleDirectory) return r } +// held is the providers router r holds under key. +func held(r *Router, key []byte) []peer.AddrInfo { + return r.records.providers(key, time.Now()) +} + // federate connects two routers and has each admit the other as an // authenticated peer router, as the handshake does. func federate(t *testing.T, a, b *Router) { @@ -122,9 +128,9 @@ func directoryCount(t *testing.T, op, result string) float64 { // A member announces a record to the router it is attached to; the router // keeps it and places it on the routers closest to the key, as many as the // mesh's replication. A member attached to any router then finds it: from -// that router's own store when it holds a copy, through the routers' DHT -// otherwise. A peer is found the same way, under its peer key, or from the -// router that holds it. +// that router's own records when it holds a copy, from the routers closest +// to the key otherwise. A peer is found the same way, under its peer key, +// or from the router that holds it. func TestDirectoryRecordsAreFoundFromAnyRouter(t *testing.T) { a, b, c := newDirectoryRouter(t), newDirectoryRouter(t), newDirectoryRouter(t) federate(t, a, b) @@ -154,14 +160,14 @@ func TestDirectoryRecordsAreFoundFromAnyRouter(t *testing.T) { if closest[0] == c.Host.ID() { holder, other = c, b } - if provs, _ := holder.DHT.ProviderStore().GetProviders(context.Background(), key); len(provs) != 1 || provs[0].ID != member.ID() { + if provs := held(holder, key); len(provs) != 1 || provs[0].ID != member.ID() { t.Fatalf("the closest router holds %v, want the member's record", provs) } - if provs, _ := other.DHT.ProviderStore().GetProviders(context.Background(), key); len(provs) != 0 { + if provs := held(other, key); len(provs) != 0 { t.Fatalf("the other router holds %v, want nothing: replication is one", provs) } - for name, router := range map[string]*Router{"own router": a, "holder": holder, "other router, through the DHT": other} { + for name, router := range map[string]*Router{"own router": a, "holder": holder, "other router, asking the closest": other} { t.Run("find providers from the "+name, func(t *testing.T) { asker := attach(t, router) resp := ask(t, asker, router.Host.ID(), findProviders(key)) @@ -250,7 +256,7 @@ func TestDirectoryRefusesWhatAMemberMayNotAnnounce(t *testing.T) { } }) } - if provs, _ := a.DHT.ProviderStore().GetProviders(context.Background(), key); len(provs) != 0 { + if provs := held(a, key); len(provs) != 0 { t.Fatalf("a refused record was stored: %v", provs) } } @@ -267,10 +273,7 @@ func TestDirectoryPlacementNamesTheProvider(t *testing.T) { if resp := ask(t, member, a.Host.ID(), provide(key, memberAddr)); resp.GetError() != "" { t.Fatalf("provide refused: %s", resp.GetError()) } - provs, err := b.DHT.ProviderStore().GetProviders(context.Background(), key) - if err != nil { - t.Fatal(err) - } + provs := held(b, key) if len(provs) != 1 || provs[0].ID != member.ID() { t.Fatalf("router b holds %v, want the member's record placed by router a", provs) } @@ -278,3 +281,94 @@ func TestDirectoryPlacementNamesTheProvider(t *testing.T) { t.Fatalf("router b holds addrs %v, want the member's %v without the peer suffix", provs[0].Addrs, member.Addrs()[0]) } } + +func withdraw(key []byte) *api.DirectoryRequest { + return &api.DirectoryRequest{Request: &api.DirectoryRequest_Withdraw{Withdraw: &api.DirectoryWithdraw{Key: key}}} +} + +// A member that stops a service withdraws its record: it is gone from the +// router it announced to and from the routers the record was placed on, +// and a lookup from any router no longer names the member. Announcing a +// record again replaces its addresses rather than adding to them, so a +// circuit through a router the member left does not linger. +func TestDirectoryWithdrawAndReplace(t *testing.T) { + a, b := newDirectoryRouter(t), newDirectoryRouter(t) + federate(t, a, b) + member := attach(t, a) + key, _ := api.ServiceKey(api.ServiceType_SERVICE_TYPE_MCP, "calculator") + oldAddr := "/ip4/10.0.0.1/tcp/4001/p2p/" + member.ID().String() + newAddr := "/ip4/10.0.0.2/tcp/4001/p2p/" + member.ID().String() + + if resp := ask(t, member, a.Host.ID(), provide(key, oldAddr)); resp.GetError() != "" { + t.Fatalf("provide refused: %s", resp.GetError()) + } + if resp := ask(t, member, a.Host.ID(), provide(key, newAddr)); resp.GetError() != "" { + t.Fatalf("second provide refused: %s", resp.GetError()) + } + for name, router := range map[string]*Router{"own router": a, "placed on": b} { + provs := held(router, key) + if len(provs) != 1 || len(provs[0].Addrs) != 1 || provs[0].Addrs[0].String() != "/ip4/10.0.0.2/tcp/4001" { + t.Fatalf("%s holds %v, want the member at its new address only", name, provs) + } + } + + before := directoryCount(t, directoryOpWithdraw, directoryResultOK) + if resp := ask(t, member, a.Host.ID(), withdraw(key)); resp.GetError() != "" { + t.Fatalf("withdraw refused: %s", resp.GetError()) + } + if got := directoryCount(t, directoryOpWithdraw, directoryResultOK); got != before+1 { + t.Fatalf("withdrawals counted = %v, want %v", got, before+1) + } + for name, router := range map[string]*Router{"own router": a, "placed on": b} { + if provs := held(router, key); len(provs) != 0 { + t.Fatalf("%s still holds %v after the withdrawal", name, provs) + } + } + asker := attach(t, b) + if resp := ask(t, asker, b.Host.ID(), findProviders(key)); resp.GetError() != "" || len(resp.GetProviders()) != 0 { + t.Fatalf("find_providers after the withdrawal = %v, want nobody", resp) + } + // Withdrawing again is not an error: the record may have expired. + if resp := ask(t, member, a.Host.ID(), withdraw(key)); resp.GetError() != "" { + t.Fatalf("withdrawing a record that is gone: %s", resp.GetError()) + } + // A member withdraws its own records only. + stranger := newClient(t, a) + named := &api.DirectoryRequest{Request: &api.DirectoryRequest_Withdraw{Withdraw: &api.DirectoryWithdraw{Key: key, Provider: stranger.ID().String()}}} + if resp := ask(t, member, a.Host.ID(), named); resp.GetError() == "" { + t.Fatal("a member withdrew a record naming another provider") + } +} + +// A record lives for the mesh's record TTL after its last announcement: +// announcing it again extends it, and the sweep drops what expired. +func TestRecordStoreExpiry(t *testing.T) { + store := newRecordStore(time.Minute) + p1, p2 := peer.ID("p1"), peer.ID("p2") + key := []byte("k") + t0 := time.Unix(1000, 0) + store.put(key, p1, nil, t0) + store.put(key, p2, nil, t0.Add(30*time.Second)) + if got := store.providers(key, t0.Add(59*time.Second)); len(got) != 2 { + t.Fatalf("providers before expiry = %v, want both", got) + } + if got := store.providers(key, t0.Add(time.Minute)); len(got) != 1 || got[0].ID != p2 { + t.Fatalf("providers at the first record's expiry = %v, want the second only", got) + } + store.put(key, p1, nil, t0.Add(time.Minute)) + if got := store.sweep(t0.Add(90 * time.Second)); got != 1 { + t.Fatalf("sweep dropped %d records, want 1: the second, not the first that was announced again", got) + } + if got := store.providers(key, t0.Add(90*time.Second)); len(got) != 1 || got[0].ID != p1 { + t.Fatalf("providers after the sweep = %v, want the first", got) + } + if store.sweep(t0.Add(3*time.Minute)) != 1 || store.size() != 0 { + t.Fatalf("the store holds %d records after everything expired, want 0", store.size()) + } + if got := recordSweepInterval(15 * time.Minute); got != time.Minute { + t.Fatalf("sweep interval for a 15m TTL = %s, want 1m", got) + } + if got := recordSweepInterval(6 * time.Second); got != 2*time.Second { + t.Fatalf("sweep interval for a 6s TTL = %s, want 2s", got) + } +} diff --git a/internal/router/metrics.go b/internal/router/metrics.go index 42bc1f7f..fef7a1db 100644 --- a/internal/router/metrics.go +++ b/internal/router/metrics.go @@ -120,6 +120,7 @@ type routerStateCollector struct { relayLimitDesc *prometheus.Desc bannedDesc *prometheus.Desc dhtDesc *prometheus.Desc + recordsDesc *prometheus.Desc biscuitExpiryDesc *prometheus.Desc } @@ -170,6 +171,10 @@ func newRouterStateCollector(r *Router) *routerStateCollector { "agentmesh_router_dht_routing_table_size", "Peers in the Kademlia routing table", nil, nil), + recordsDesc: prometheus.NewDesc( + "agentmesh_router_directory_records", + "Directory records this router holds: its members' and the copies placed on it", + nil, nil), biscuitExpiryDesc: prometheus.NewDesc( "agentmesh_router_biscuit_expiry_timestamp_seconds", "Unix time the router's own mesh credential expires", @@ -180,7 +185,7 @@ func newRouterStateCollector(r *Router) *routerStateCollector { func (c *routerStateCollector) Describe(ch chan<- *prometheus.Desc) { for _, d := range []*prometheus.Desc{ c.infoDesc, c.readyDesc, c.drainingDesc, c.authenticatedDesc, c.peerRoutersDesc, c.connectedDesc, c.connectionsDesc, c.watermarkDesc, c.relayLimitDesc, - c.bannedDesc, c.dhtDesc, c.biscuitExpiryDesc, connsPerSourceIPDesc, + c.bannedDesc, c.dhtDesc, c.recordsDesc, c.biscuitExpiryDesc, connsPerSourceIPDesc, } { ch <- d } @@ -224,6 +229,7 @@ func (c *routerStateCollector) Collect(ch chan<- prometheus.Metric) { ch <- prometheus.MustNewConstMetric(c.connectionsDesc, prometheus.GaugeValue, float64(outbound), "outbound") ch <- prometheus.MustNewConstMetric(c.bannedDesc, prometheus.GaugeValue, float64(syncMapLen(&r.bannedPeers))) ch <- prometheus.MustNewConstMetric(c.dhtDesc, prometheus.GaugeValue, float64(r.DHT.RoutingTable().Size())) + ch <- prometheus.MustNewConstMetric(c.recordsDesc, prometheus.GaugeValue, float64(r.records.size())) r.keysMu.RLock() expiry := r.credential.expiration diff --git a/internal/router/metrics_test.go b/internal/router/metrics_test.go index e28fe5ff..7eeab451 100644 --- a/internal/router/metrics_test.go +++ b/internal/router/metrics_test.go @@ -84,6 +84,8 @@ func TestRouterMetricsListener(t *testing.T) { r.Host = host r.DHT = kad + r.records = newRecordStore(time.Minute) + r.records.put([]byte("k"), peer.ID("peer-a"), nil, time.Now()) r.authenticatedPeers.Store(peer.ID("peer-a"), true) r.authenticatedPeers.Store(peer.ID("peer-b"), true) r.bannedPeers.Store(peer.ID("peer-x"), true) @@ -119,6 +121,7 @@ func TestRouterMetricsListener(t *testing.T) { "agentmesh_router_banned_peers 1", "agentmesh_router_connected_peers 0", "agentmesh_router_dht_routing_table_size 0", + "agentmesh_router_directory_records 1", // The text format renders values with strconv 'g', so a unix time // comes out in scientific notation. "agentmesh_router_biscuit_expiry_timestamp_seconds " + strconv.FormatFloat(float64(expiry.Unix()), 'g', -1, 64), diff --git a/internal/router/router.go b/internal/router/router.go index b9822d81..4d610081 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -332,9 +332,10 @@ type Router struct { EventTopic *pubsub.Topic authenticatedPeers sync.Map bannedPeers sync.Map - // directory is how the mesh keeps its DHT, read from /info at start - // (syncDirectory). + // directory is how the mesh keeps its directory, read from /info at + // start (syncDirectory); records is the records this router holds. directory api.DirectorySettings + records *recordStore // peerRouters is the other routers of the mesh, as /info lists them or as // a handshake proved them: held whatever the load and never sent away. peerRouters sync.Map @@ -524,6 +525,7 @@ func (r *Router) Start() (retErr error) { return err } r.DHT = kadDHT + r.records = newRecordStore(r.directory.RecordTTL) if err = kadDHT.Bootstrap(r.ctx); err != nil { return err @@ -579,13 +581,17 @@ func (r *Router) Start() (retErr error) { }) // 5. Start background routines - r.wg.Add(7) + r.wg.Add(8) go r.runLeaseRenewalLoop() go r.runKeysSyncLoop() go r.runFederationLoop() go r.runBiscuitRenewalLoop() go r.runShedLoop() go r.listenForControlPlaneEvents(r.ctx) + go func() { + defer r.wg.Done() + r.records.runSweep(r.ctx, recordSweepInterval(r.directory.RecordTTL)) + }() go func() { defer r.wg.Done() p2plimits.ObserveStreamPressure(hostNode.Network().Conns, busiestConnStreams, logger.Warnf).Run(r.ctx, streamPressureInterval) diff --git a/sdk/README.md b/sdk/README.md index e654e3cc..08a537ce 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -127,9 +127,9 @@ pinned by a test: (libp2p/py-libp2p#1550); the host gives it the system roots. - A member keeps no DHT. Both SDKs ask a router they hold over `/mesh/directory/1.0.0` (`discovery.ts`, `agent_mesh/discovery.py`), one - `DirectoryRequest` per stream, and the router runs the lookup in the DHT - the routers keep among themselves. `sdk/testdata/service_keys.json` pins - the keys both SDKs derive against `api/directory_keys.go`. + `DirectoryRequest` per stream, and the router answers from its own + records and the routers closest to the key. `sdk/testdata/service_keys.json` + pins the keys both SDKs derive against `api/directory_keys.go`. - go-libp2p-http is plain HTTP/1.1 on a stream, one request per stream, with `Host` set to the peer ID. The JS SDK bridges the stream to a Node duplex and runs Node's HTTP server and client on it; the Python SDK diff --git a/sdk/js/src/gen/agentmesh_pb.ts b/sdk/js/src/gen/agentmesh_pb.ts index e8cbe917..c2f5a251 100644 --- a/sdk/js/src/gen/agentmesh_pb.ts +++ b/sdk/js/src/gen/agentmesh_pb.ts @@ -26,7 +26,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file agentmesh.proto. */ export const file_agentmesh: GenFile = /*@__PURE__*/ - fileDesc("Cg9hZ2VudG1lc2gucHJvdG8SDGFnZW50bWVzaC52MSI0CglBdXRoRnJhbWUSDwoHYmlzY3VpdBgBIAEoDBIWCg50YXJnZXRfc2VydmljZRgCIAEoCSI/CgxBdXRoUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCBINCgVlcnJvchgCIAEoCRIPCgdiaXNjdWl0GAMgASgMIrEBCgxSb3V0ZXJHb0F3YXkSMQoGcmVhc29uGAEgASgOMiEuYWdlbnRtZXNoLnYxLlJvdXRlckdvQXdheS5SZWFzb24SLgoLcmV0cnlfYWZ0ZXIYAiABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24iPgoGUmVhc29uEhYKElJFQVNPTl9VTlNQRUNJRklFRBAAEgwKCERSQUlOSU5HEAESDgoKT1ZFUkxPQURFRBACItwBCglNZXNoRXZlbnQSKgoEdHlwZRgBIAEoDjIcLmFnZW50bWVzaC52MS5NZXNoRXZlbnQuVHlwZRIPCgdwZWVyX2lkGAIgASgJEi4KCmV2ZW50X3RpbWUYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhYKDm5ld19wdWJsaWNfa2V5GAQgASgMEhEKCXNpZ25hdHVyZRgFIAEoDCI3CgRUeXBlEgoKBkJBTk5FRBAAEhAKDEtFWV9ST1RBVElPThABEhEKDVBPTElDWV9VUERBVEUQAiL5AQoNRW5yb2xsUmVxdWVzdBILCgNqd3QYASABKAkSDwoHcGVlcl9pZBgCIAEoCRISCgpwdWJsaWNfa2V5GAMgASgMEhYKDnJlcXVlc3RlZF9yb2xlGAQgASgJEjcKBmxhYmVscxgFIAMoCzInLmFnZW50bWVzaC52MS5FbnJvbGxSZXF1ZXN0LkxhYmVsc0VudHJ5EhkKEWNoYWxsZW5nZV91bml4X21zGAYgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYByABKAwaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKrAQoORW5yb2xsUmVzcG9uc2USFQoNYmlzY3VpdF90b2tlbhgBIAEoDBIVCg1lcnJvcl9tZXNzYWdlGAIgASgJEiAKGGNvbnRyb2xfcGxhbmVfcHVibGljX2tleRgDIAEoDBIYChByb3V0ZXJfYWRkcmVzc2VzGAQgAygJEi8KC2V4cGlyZV90aW1lGAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCKXAgoWQm9vdHN0cmFwRW5yb2xsUmVxdWVzdBIXCg9ib290c3RyYXBfdG9rZW4YASABKAkSDwoHcGVlcl9pZBgCIAEoCRISCgpwdWJsaWNfa2V5GAMgASgMEhYKDnJlcXVlc3RlZF9yb2xlGAQgASgJEkAKBmxhYmVscxgFIAMoCzIwLmFnZW50bWVzaC52MS5Cb290c3RyYXBFbnJvbGxSZXF1ZXN0LkxhYmVsc0VudHJ5EhkKEWNoYWxsZW5nZV91bml4X21zGAYgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYByABKAwaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKDAgoXQm9vdHN0cmFwRW5yb2xsUmVzcG9uc2USLgoGc3RhdHVzGAEgASgOMh4uYWdlbnRtZXNoLnYxLkVucm9sbG1lbnRTdGF0dXMSFQoNYmlzY3VpdF90b2tlbhgCIAEoDBIdChVwb2xsX2ludGVydmFsX3NlY29uZHMYAyABKAUSFQoNZXJyb3JfbWVzc2FnZRgEIAEoCRIgChhjb250cm9sX3BsYW5lX3B1YmxpY19rZXkYBSABKAwSGAoQcm91dGVyX2FkZHJlc3NlcxgGIAMoCRIvCgtleHBpcmVfdGltZRgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiWQoLU2VydmljZUluZm8SJwoEdHlwZRgBIAEoDjIZLmFnZW50bWVzaC52MS5TZXJ2aWNlVHlwZRIMCgRuYW1lGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJIoEBCg5Db21tYW5kQmFja2VuZBIPCgdjb21tYW5kGAEgAygJEjIKA2VudhgCIAMoCzIlLmFnZW50bWVzaC52MS5Db21tYW5kQmFja2VuZC5FbnZFbnRyeRoqCghFbnZFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIpYBChZSZWdpc3RlclNlcnZpY2VSZXF1ZXN0EioKB3NlcnZpY2UYASABKAsyGS5hZ2VudG1lc2gudjEuU2VydmljZUluZm8SFAoKdGFyZ2V0X3VybBgCIAEoCUgAEi8KB2NvbW1hbmQYAyABKAsyHC5hZ2VudG1lc2gudjEuQ29tbWFuZEJhY2tlbmRIAEIJCgdiYWNrZW5kImkKEkRpc2NvdmVyZWRQcm92aWRlchIPCgdwZWVyX2lkGAEgASgJEhcKD2xvY2FsX3Byb3h5X3VybBgCIAEoCRIQCghzcnZfbmFtZRgDIAEoCRIXCg9zcnZfZGVzY3JpcHRpb24YBCABKAki6AEKGENvbnRyb2xQbGFuZUluZm9SZXNwb25zZRITCgtvaWRjX2lzc3VlchgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSEAoIYXVkaWVuY2UYAyABKAkSGAoQcm91dGVyX2FkZHJlc3NlcxgEIAMoCRIXCg9iYW5uZWRfcGVlcl9pZHMYBSADKAkSKQoHcm91dGVycxgGIAMoCzIYLmFnZW50bWVzaC52MS5Sb3V0ZXJJbmZvEjQKCWRpcmVjdG9yeRgHIAEoCzIhLmFnZW50bWVzaC52MS5EaXJlY3RvcnlQYXJhbWV0ZXJzIpABChNEaXJlY3RvcnlQYXJhbWV0ZXJzEhMKC3JlcGxpY2F0aW9uGAEgASgFEjUKEnJlcHJvdmlkZV9pbnRlcnZhbBgCIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbhItCgpyZWNvcmRfdHRsGAMgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uIsYBChBEaXJlY3RvcnlSZXF1ZXN0EjEKB3Byb3ZpZGUYASABKAsyHi5hZ2VudG1lc2gudjEuRGlyZWN0b3J5UHJvdmlkZUgAEj4KDmZpbmRfcHJvdmlkZXJzGAIgASgLMiQuYWdlbnRtZXNoLnYxLkRpcmVjdG9yeUZpbmRQcm92aWRlcnNIABI0CglmaW5kX3BlZXIYAyABKAsyHy5hZ2VudG1lc2gudjEuRGlyZWN0b3J5RmluZFBlZXJIAEIJCgdyZXF1ZXN0IkAKEERpcmVjdG9yeVByb3ZpZGUSCwoDa2V5GAEgASgMEg0KBWFkZHJzGAIgAygJEhAKCHByb3ZpZGVyGAMgASgJIjQKFkRpcmVjdG9yeUZpbmRQcm92aWRlcnMSCwoDa2V5GAEgASgMEg0KBWxpbWl0GAIgASgNIiQKEURpcmVjdG9yeUZpbmRQZWVyEg8KB3BlZXJfaWQYASABKAkidwoRRGlyZWN0b3J5UmVzcG9uc2USKwoJcHJvdmlkZXJzGAEgAygLMhguYWdlbnRtZXNoLnYxLlBlZXJSZWNvcmQSJgoEcGVlchgCIAEoCzIYLmFnZW50bWVzaC52MS5QZWVyUmVjb3JkEg0KBWVycm9yGAMgASgJIiwKClBlZXJSZWNvcmQSDwoHcGVlcl9pZBgBIAEoCRINCgVhZGRycxgCIAMoCSLEAQoKUm91dGVySW5mbxIPCgdwZWVyX2lkGAEgASgJEhEKCWFkZHJlc3NlcxgCIAMoCRI0CgZsYWJlbHMYAyADKAsyJC5hZ2VudG1lc2gudjEuUm91dGVySW5mby5MYWJlbHNFbnRyeRITCgtjb25uZWN0aW9ucxgEIAEoBRIYChBjb25uZWN0aW9uX2xpbWl0GAUgASgFGi0KC0xhYmVsc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEigwIKElJvdXRlckxlYXNlUmVxdWVzdBIPCgdwZWVyX2lkGAEgASgJEhEKCWFkZHJlc3NlcxgCIAMoCRIPCgdiaXNjdWl0GAMgASgMEhcKD2Nvbm5lY3RlZF9wZWVycxgEIAMoCRIQCghkaHRfc2l6ZRgFIAEoBRIZChFjaGFsbGVuZ2VfdW5peF9tcxgGIAEoAxIbChNjaGFsbGVuZ2Vfc2lnbmF0dXJlGAcgASgMEiYKA3R0bBgIIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbhITCgtjb25uZWN0aW9ucxgJIAEoBRIYChBjb25uZWN0aW9uX2xpbWl0GAogASgFImYKE1JvdXRlckxlYXNlUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCBINCgVlcnJvchgCIAEoCRIvCgtleHBpcmVfdGltZRgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAipAEKClBvbGljeVJvbGUSDAoEbmFtZRgBIAEoCRIXCg9hbGxvd2VkX3RhcmdldHMYAiADKAkSGAoQYWxsb3dlZF9zZXJ2aWNlcxgDIAMoCRIWCg5jdXN0b21fZGF0YWxvZxgEIAMoCRIWCg5hbGxvd2VkX2xhYmVscxgFIAMoCRIlCgRodHRwGAYgAygLMhcuYWdlbnRtZXNoLnYxLkhUVFBHcmFudCI8CglIVFRQR3JhbnQSDwoHc2VydmljZRgBIAEoCRIPCgdtZXRob2RzGAIgAygJEg0KBXBhdGhzGAMgAygJIqECChFFZ3Jlc3NEZXN0aW5hdGlvbhIMCgRuYW1lGAEgASgJEhIKCnRhcmdldF91cmwYAiABKAkSEgoKY3JlZGVudGlhbBgDIAEoCRIRCglzZXJ2ZWRfYnkYBCADKAkSLgoGYnJva2VyGAUgASgLMh4uYWdlbnRtZXNoLnYxLkNyZWRlbnRpYWxCcm9rZXISLAoKaW5zcGVjdGlvbhgGIAEoCzIYLmFnZW50bWVzaC52MS5JbnNwZWN0aW9uEiYKBG1vZGUYByABKA4yGC5hZ2VudG1lc2gudjEuRWdyZXNzTW9kZRINCgVwb3J0cxgIIAMoDRIVCg1wcmVzZXJ2ZV9ob3N0GAkgASgIEhcKD2ZvcndhcmRfY29udGV4dBgKIAEoCCI5CgpJbnNwZWN0aW9uEisKCmluc3BlY3RvcnMYASADKAsyFy5hZ2VudG1lc2gudjEuSW5zcGVjdG9yIm8KCUluc3BlY3RvchIvCgttb2RlbF9hcm1vchgBIAEoCzIYLmFnZW50bWVzaC52MS5Nb2RlbEFybW9ySAASKQoIZXh0X3Byb2MYAiABKAsyFS5hZ2VudG1lc2gudjEuRXh0UHJvY0gAQgYKBGtpbmQikQEKCk1vZGVsQXJtb3ISEAoIdGVtcGxhdGUYASABKAkSMgoIcmVzcG9uc2UYAiABKA4yIC5hZ2VudG1lc2gudjEuUmVzcG9uc2VJbnNwZWN0aW9uEhEKCWZhaWxfb3BlbhgDIAEoCBIqCgd0aW1lb3V0GAQgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uIogCCgdFeHRQcm9jEg4KBnRhcmdldBgBIAEoCRIKCgJjYRgCIAEoCRIaChJjbGllbnRfY2VydGlmaWNhdGUYAyABKAkSPAoPcHJvY2Vzc2luZ19tb2RlGAQgASgLMiMuYWdlbnRtZXNoLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZRIbChNhbGxvd19tb2RlX292ZXJyaWRlGAUgASgIEjIKD21lc3NhZ2VfdGltZW91dBgGIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbhIaChJmYWlsdXJlX21vZGVfYWxsb3cYByABKAgSGgoSbWF4X2J1ZmZlcmVkX2J5dGVzGAggASgNIv8EChVFeHRQcm9jUHJvY2Vzc2luZ01vZGUSSwoTcmVxdWVzdF9oZWFkZXJfbW9kZRgBIAEoDjIuLmFnZW50bWVzaC52MS5FeHRQcm9jUHJvY2Vzc2luZ01vZGUuSGVhZGVyTW9kZRJMChRyZXNwb25zZV9oZWFkZXJfbW9kZRgCIAEoDjIuLmFnZW50bWVzaC52MS5FeHRQcm9jUHJvY2Vzc2luZ01vZGUuSGVhZGVyTW9kZRJHChFyZXF1ZXN0X2JvZHlfbW9kZRgDIAEoDjIsLmFnZW50bWVzaC52MS5FeHRQcm9jUHJvY2Vzc2luZ01vZGUuQm9keU1vZGUSSAoScmVzcG9uc2VfYm9keV9tb2RlGAQgASgOMiwuYWdlbnRtZXNoLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZS5Cb2R5TW9kZRJMChRyZXF1ZXN0X3RyYWlsZXJfbW9kZRgFIAEoDjIuLmFnZW50bWVzaC52MS5FeHRQcm9jUHJvY2Vzc2luZ01vZGUuSGVhZGVyTW9kZRJNChVyZXNwb25zZV90cmFpbGVyX21vZGUYBiABKA4yLi5hZ2VudG1lc2gudjEuRXh0UHJvY1Byb2Nlc3NpbmdNb2RlLkhlYWRlck1vZGUiOQoKSGVhZGVyTW9kZRIXChNIRUFERVJfTU9ERV9ERUZBVUxUEAASCAoEU0VORBABEggKBFNLSVAQAiJgCghCb2R5TW9kZRIICgROT05FEAASDAoIU1RSRUFNRUQQARIMCghCVUZGRVJFRBACEhQKEEJVRkZFUkVEX1BBUlRJQUwQAxIYChRGVUxMX0RVUExFWF9TVFJFQU1FRBAEIuEBChBDcmVkZW50aWFsQnJva2VyEhcKDXN0YXRpY19zZWNyZXQYASABKAlIABI3Cg9vaWRjX2ZlZGVyYXRpb24YAiABKAsyHC5hZ2VudG1lc2gudjEuT0lEQ0ZlZGVyYXRpb25IABI2Cg9hd3NfYXNzdW1lX3JvbGUYAyABKAsyGy5hZ2VudG1lc2gudjEuQVdTQXNzdW1lUm9sZUgAEjsKEXBsYXRmb3JtX2lkZW50aXR5GAQgASgLMh4uYWdlbnRtZXNoLnYxLlBsYXRmb3JtSWRlbnRpdHlIAEIGCgRraW5kIl8KDk9JRENGZWRlcmF0aW9uEhYKDnRva2VuX2VuZHBvaW50GAEgASgJEhAKCGF1ZGllbmNlGAIgASgJEhMKC2ltcGVyc29uYXRlGAMgASgJEg4KBnNjb3BlcxgEIAMoCSI5Cg1BV1NBc3N1bWVSb2xlEhAKCHJvbGVfYXJuGAEgASgJEhYKDnNlc3Npb25fcG9saWN5GAIgASgJIiIKEFBsYXRmb3JtSWRlbnRpdHkSDgoGc2NvcGVzGAEgAygJIi4KDVBvbGljeUJpbmRpbmcSDAoEcm9sZRgBIAEoCRIPCgdtZW1iZXJzGAIgAygJIpcBCgxQb2xpY3lDb25maWcSJwoFcm9sZXMYASADKAsyGC5hZ2VudG1lc2gudjEuUG9saWN5Um9sZRItCghiaW5kaW5ncxgCIAMoCzIbLmFnZW50bWVzaC52MS5Qb2xpY3lCaW5kaW5nEi8KBmVncmVzcxgDIAMoCzIfLmFnZW50bWVzaC52MS5FZ3Jlc3NEZXN0aW5hdGlvbiIYChZQb2xpY3lDb25maWdHZXRSZXF1ZXN0IjAKF1BvbGljeUNvbmZpZ0dldFJlc3BvbnNlEhUKDWRhdGFsb2dfcnVsZXMYASADKAkiPAoaUG9saWN5Q29uZmlnVXBkYXRlUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCBINCgVlcnJvchgCIAEoCSIaChhFZ3Jlc3NBc3NpZ25tZW50c1JlcXVlc3QiTAoZRWdyZXNzQXNzaWdubWVudHNSZXNwb25zZRIvCgZlZ3Jlc3MYASADKAsyHy5hZ2VudG1lc2gudjEuRWdyZXNzRGVzdGluYXRpb24iZgoMS2V5c1Jlc3BvbnNlEhMKC3B1YmxpY19rZXlzGAEgAygMEi0KCXNpZ25fdGltZRgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASEgoKc2lnbmF0dXJlcxgDIAMoDCJrChNUb2tlblJlZnJlc2hSZXF1ZXN0EhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYASABKAwSGQoRY2hhbGxlbmdlX3VuaXhfbXMYAiABKAMSDwoHcGVlcl9pZBgDIAEoCRILCgNqd3QYBCABKAkidQoUVG9rZW5SZWZyZXNoUmVzcG9uc2USFQoNYmlzY3VpdF90b2tlbhgBIAEoDBIvCgtleHBpcmVfdGltZRgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASFQoNZXJyb3JfbWVzc2FnZRgDIAEoCSJAChFOb2RlQ2F0YWxvZ1JlcG9ydBIrCghzZXJ2aWNlcxgBIAMoCzIZLmFnZW50bWVzaC52MS5TZXJ2aWNlSW5mbyIlChJUb2tlblJldm9rZVJlcXVlc3QSDwoHcGVlcl9pZBgBIAEoCSI1ChNUb2tlblJldm9rZVJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgSDQoFZXJyb3IYAiABKAkilgEKG0Jvb3RzdHJhcFRva2VuQ3JlYXRlUmVxdWVzdBIMCgRyb2xlGAEgASgJEhAKCG93bmVyX2lkGAIgASgJEhEKCXR0bF9ob3VycxgDIAEoBRISCgptYXhfdXNhZ2VzGAQgASgFEhMKC2Rlc2NyaXB0aW9uGAUgASgJEhsKE2F1dG9ub21vdXNfcmVjb3ZlcnkYBiABKAgiigEKHEJvb3RzdHJhcFRva2VuQ3JlYXRlUmVzcG9uc2USCgoCaWQYASABKAkSDQoFdG9rZW4YAiABKAkSDAoEcm9sZRgDIAEoCRIQCghvd25lcl9pZBgEIAEoCRIvCgtleHBpcmVfdGltZRgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiqwIKDkJvb3RzdHJhcFRva2VuEgoKAmlkGAEgASgJEgwKBHJvbGUYAiABKAkSEAoIb3duZXJfaWQYAyABKAkSEgoKbWF4X3VzYWdlcxgEIAEoBRIUCgx1c2FnZXNfY291bnQYBSABKAUSEwoLZGVzY3JpcHRpb24YBiABKAkSLwoLY3JlYXRlX3RpbWUYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2V4cGlyZV90aW1lGAggASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIvCgtyZXZva2VfdGltZRgJIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGwoTYXV0b25vbW91c19yZWNvdmVyeRgKIAEoCCJKChpCb290c3RyYXBUb2tlbkxpc3RSZXNwb25zZRIsCgZ0b2tlbnMYASADKAsyHC5hZ2VudG1lc2gudjEuQm9vdHN0cmFwVG9rZW4i1gIKEUVucm9sbG1lbnRSZXF1ZXN0EgoKAmlkGAEgASgJEg8KB3BlZXJfaWQYAiABKAkSEAoIdG9rZW5faWQYAyABKAkSLgoGc3RhdHVzGAQgASgOMh4uYWdlbnRtZXNoLnYxLkVucm9sbG1lbnRTdGF0dXMSOwoGbGFiZWxzGAUgAygLMisuYWdlbnRtZXNoLnYxLkVucm9sbG1lbnRSZXF1ZXN0LkxhYmVsc0VudHJ5Ei8KC2NyZWF0ZV90aW1lGAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIwCgxyZXNvbHZlX3RpbWUYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhMKC3Jlc29sdmVkX2J5GAggASgJGi0KC0xhYmVsc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiUgodRW5yb2xsbWVudFJlcXVlc3RMaXN0UmVzcG9uc2USMQoIcmVxdWVzdHMYASADKAsyHy5hZ2VudG1lc2gudjEuRW5yb2xsbWVudFJlcXVlc3QicAoEVXNlchIKCgJpZBgBIAEoCRIOCgZpc3N1ZXIYAiABKAkSDQoFZW1haWwYAyABKAkSDAoEcm9sZRgEIAEoCRIvCgtjcmVhdGVfdGltZRgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAi4wIKDEVucm9sbGVkTm9kZRIPCgdwZWVyX2lkGAEgASgJEgwKBHJvbGUYAiABKAkSFwoPZW5yb2xsbWVudF90eXBlGAMgASgJEhMKC2NsYWltc19qc29uGAQgASgJEhAKCG93bmVyX2lkGAUgASgJEjYKBmxhYmVscxgGIAMoCzImLmFnZW50bWVzaC52MS5FbnJvbGxlZE5vZGUuTGFiZWxzRW50cnkSLwoLZW5yb2xsX3RpbWUYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2V4cGlyZV90aW1lGAggASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIOCgZiYW5uZWQYCSABKAgSGwoTYXV0b25vbW91c19yZWNvdmVyeRgKIAEoCBotCgtMYWJlbHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIsQBCgtSb3V0ZXJMZWFzZRIPCgdwZWVyX2lkGAEgASgJEhEKCWFkZHJlc3NlcxgCIAMoCRI1ChFsYXN0X3JlbmV3YWxfdGltZRgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLwoLZXhwaXJlX3RpbWUYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhcKD2Nvbm5lY3RlZF9wZWVycxgFIAMoCRIQCghkaHRfc2l6ZRgGIAEoBSJsCgxOb2RlU2VydmljZXMSKwoIc2VydmljZXMYASADKAsyGS5hZ2VudG1lc2gudjEuU2VydmljZUluZm8SLwoLcmVwb3J0X3RpbWUYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wItsDChNBZG1pblN0YXR1c1Jlc3BvbnNlEiEKBXVzZXJzGAEgAygLMhIuYWdlbnRtZXNoLnYxLlVzZXISMQoOYWN0aXZlX3JvdXRlcnMYAiADKAsyGS5hZ2VudG1lc2gudjEuUm91dGVyTGVhc2USMgoOZW5yb2xsZWRfbm9kZXMYAyADKAsyGi5hZ2VudG1lc2gudjEuRW5yb2xsZWROb2RlEjwKE2Vucm9sbG1lbnRfcmVxdWVzdHMYBCADKAsyHy5hZ2VudG1lc2gudjEuRW5yb2xsbWVudFJlcXVlc3QSNgoQYm9vdHN0cmFwX3Rva2VucxgFIAMoCzIcLmFnZW50bWVzaC52MS5Cb290c3RyYXBUb2tlbhIqCgZwb2xpY3kYBiABKAsyGi5hZ2VudG1lc2gudjEuUG9saWN5Q29uZmlnEkgKDG5vZGVfY2F0YWxvZxgHIAMoCzIyLmFnZW50bWVzaC52MS5BZG1pblN0YXR1c1Jlc3BvbnNlLk5vZGVDYXRhbG9nRW50cnkaTgoQTm9kZUNhdGFsb2dFbnRyeRILCgNrZXkYASABKAkSKQoFdmFsdWUYAiABKAsyGi5hZ2VudG1lc2gudjEuTm9kZVNlcnZpY2VzOgI4ASKBAgoSVXNlclN0YXR1c1Jlc3BvbnNlEiAKBHVzZXIYASABKAsyEi5hZ2VudG1lc2gudjEuVXNlchIyCg5lbnJvbGxlZF9ub2RlcxgCIAMoCzIaLmFnZW50bWVzaC52MS5FbnJvbGxlZE5vZGUSNgoQYm9vdHN0cmFwX3Rva2VucxgDIAMoCzIcLmFnZW50bWVzaC52MS5Cb290c3RyYXBUb2tlbhIxCg5hY3RpdmVfcm91dGVycxgEIAMoCzIZLmFnZW50bWVzaC52MS5Sb3V0ZXJMZWFzZRIqCgZwb2xpY3kYBSABKAsyGi5hZ2VudG1lc2gudjEuUG9saWN5Q29uZmlnIuQBChhJZGVudGl0eUV2aWRlbmNlUmVzcG9uc2USDwoHcGVlcl9pZBgBIAEoCRIPCgdiaXNjdWl0GAIgASgMEjcKE2Jpc2N1aXRfZXhwaXJlX3RpbWUYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhkKEWNvbnRyb2xfcGxhbmVfdXJsGAQgASgJEiIKGnRydXN0ZWRfY29udHJvbF9wbGFuZV9rZXlzGAUgAygMEi4KCmNoZWNrX3RpbWUYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIsYCChRQZWVyRXZpZGVuY2VSZXNwb25zZRIPCgdwZWVyX2lkGAEgASgJEg8KB2Jpc2N1aXQYAiABKAwSFQoNdmVyaWZ5aW5nX2tleRgDIAEoDBINCgVyb2xlcxgEIAMoCRI+CgZsYWJlbHMYBSADKAsyLi5hZ2VudG1lc2gudjEuUGVlckV2aWRlbmNlUmVzcG9uc2UuTGFiZWxzRW50cnkSLwoLZXhwaXJlX3RpbWUYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhYKDnJldm9jYXRpb25faWRzGAcgAygJEi4KCmNoZWNrX3RpbWUYCCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wGi0KC0xhYmVsc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEijAIKEE1lbWJlckNyZWRlbnRpYWwSGQoRY29udHJvbF9wbGFuZV91cmwYASABKAkSDwoHYmlzY3VpdBgCIAEoDBIvCgtleHBpcmVfdGltZRgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASNQoMdHJ1c3RlZF9rZXlzGAQgAygLMh8uYWdlbnRtZXNoLnYxLlRydXN0ZWRTaWduaW5nS2V5EhkKEWlzc3VlZF91bmRlcl9rZXlzGAUgAygMEhgKEHJvdXRlcl9hZGRyZXNzZXMYBiADKAkSLwoMb2lkY19zZXNzaW9uGAcgASgLMhkuYWdlbnRtZXNoLnYxLk9JRENTZXNzaW9uIlkKEVRydXN0ZWRTaWduaW5nS2V5EhIKCnB1YmxpY19rZXkYASABKAwSMAoMcmVjZWl2ZV90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJZCgtPSURDU2Vzc2lvbhIOCgZpc3N1ZXIYASABKAkSEQoJY2xpZW50X2lkGAIgASgJEhAKCGF1ZGllbmNlGAMgASgJEhUKDXJlZnJlc2hfdG9rZW4YBCABKAkikwEKFVRhc2tBdXRob3JpemF0aW9uUnVsZRIMCgRuYW1lGAEgASgJEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRIlCgVydWxlcxgDIAMoCzIWLmFnZW50bWVzaC52MS5UYXNrUnVsZRIvCgtleHBpcmVfdGltZRgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAihAEKCFRhc2tSdWxlEhMKC2Rlc2NyaXB0aW9uGAEgASgJEhgKEGFsbG93ZWRfc2VydmljZXMYAiADKAkSLgoJb3BlcmF0aW9uGAMgASgLMhsuYWdlbnRtZXNoLnYxLlRhc2tPcGVyYXRpb24SGQoRYWxsb3dlZF9yZXNvdXJjZXMYBCADKAkicwoNVGFza09wZXJhdGlvbhIVCg1hbGxvd2VkX3Rvb2xzGAEgAygJEhcKD2FsbG93ZWRfbWV0aG9kcxgCIAMoCRIVCg1hbGxvd2VkX3BhdGhzGAMgAygJEhsKE2FsbG93ZWRfcGVybWlzc2lvbnMYBCADKAkiqwEKFFRva2VuRXhjaGFuZ2VSZXF1ZXN0EhUKDXN1YmplY3RfdG9rZW4YASABKAkSNgoJdGFza19ydWxlGAIgASgLMiMuYWdlbnRtZXNoLnYxLlRhc2tBdXRob3JpemF0aW9uUnVsZRIMCgRzZWFsGAMgASgIEhkKEWNoYWxsZW5nZV91bml4X21zGAQgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYBSABKAwifwoVVG9rZW5FeGNoYW5nZVJlc3BvbnNlEhUKDWJpc2N1aXRfdG9rZW4YASABKAwSLwoLZXhwaXJlX3RpbWUYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEg0KBXJvbGVzGAMgAygJEg8KB3N1YmplY3QYBCABKAkigQEKD1NUU1Rva2VuUmVxdWVzdBIPCgdiaXNjdWl0GAEgASgMEhMKC2Rlc3RpbmF0aW9uGAIgASgJEhAKCGF1ZGllbmNlGAMgASgJEhkKEWNoYWxsZW5nZV91bml4X21zGAQgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYBSABKAwigwEKEFNUU1Rva2VuUmVzcG9uc2USCwoDand0GAEgASgJEi8KC2V4cGlyZV90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIPCgdzdWJqZWN0GAMgASgJEg0KBXJvbGVzGAQgAygJEhEKCXRhc2tfbmFtZRgFIAEoCSJGChNSZXZvY2F0aW9uc1Jlc3BvbnNlEhYKDnJldm9jYXRpb25faWRzGAEgAygJEhcKD2Jhbm5lZF9wZWVyX2lkcxgCIAMoCSqUAQoQRW5yb2xsbWVudFN0YXR1cxIhCh1FTlJPTExNRU5UX1NUQVRVU19VTlNQRUNJRklFRBAAEh0KGUVOUk9MTE1FTlRfU1RBVFVTX1BFTkRJTkcQARIeChpFTlJPTExNRU5UX1NUQVRVU19BUFBST1ZFRBACEh4KGkVOUk9MTE1FTlRfU1RBVFVTX1JFSkVDVEVEEAMqjAEKC1NlcnZpY2VUeXBlEhwKGFNFUlZJQ0VfVFlQRV9VTlNQRUNJRklFRBAAEhQKEFNFUlZJQ0VfVFlQRV9NQ1AQARIaChZTRVJWSUNFX1RZUEVfSU5GRVJFTkNFEAISFAoQU0VSVklDRV9UWVBFX0EyQRADEhcKE1NFUlZJQ0VfVFlQRV9FR1JFU1MQBCo3CgpFZ3Jlc3NNb2RlEhQKEEVHUkVTU19NT0RFX0hUVFAQABITCg9FR1JFU1NfTU9ERV9UQ1AQASpcChJSZXNwb25zZUluc3BlY3Rpb24SIAocUkVTUE9OU0VfSU5TUEVDVElPTl9CVUZGRVJFRBAAEiQKIFJFU1BPTlNFX0lOU1BFQ1RJT05fUkVRVUVTVF9PTkxZEAFCIVofZ2l0aHViLmNvbS9nb29nbGUvYWdlbnRtZXNoL2FwaWIGcHJvdG8z", [file_google_protobuf_duration, file_google_protobuf_timestamp]); + fileDesc("Cg9hZ2VudG1lc2gucHJvdG8SDGFnZW50bWVzaC52MSI0CglBdXRoRnJhbWUSDwoHYmlzY3VpdBgBIAEoDBIWCg50YXJnZXRfc2VydmljZRgCIAEoCSI/CgxBdXRoUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCBINCgVlcnJvchgCIAEoCRIPCgdiaXNjdWl0GAMgASgMIrEBCgxSb3V0ZXJHb0F3YXkSMQoGcmVhc29uGAEgASgOMiEuYWdlbnRtZXNoLnYxLlJvdXRlckdvQXdheS5SZWFzb24SLgoLcmV0cnlfYWZ0ZXIYAiABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24iPgoGUmVhc29uEhYKElJFQVNPTl9VTlNQRUNJRklFRBAAEgwKCERSQUlOSU5HEAESDgoKT1ZFUkxPQURFRBACItwBCglNZXNoRXZlbnQSKgoEdHlwZRgBIAEoDjIcLmFnZW50bWVzaC52MS5NZXNoRXZlbnQuVHlwZRIPCgdwZWVyX2lkGAIgASgJEi4KCmV2ZW50X3RpbWUYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhYKDm5ld19wdWJsaWNfa2V5GAQgASgMEhEKCXNpZ25hdHVyZRgFIAEoDCI3CgRUeXBlEgoKBkJBTk5FRBAAEhAKDEtFWV9ST1RBVElPThABEhEKDVBPTElDWV9VUERBVEUQAiL5AQoNRW5yb2xsUmVxdWVzdBILCgNqd3QYASABKAkSDwoHcGVlcl9pZBgCIAEoCRISCgpwdWJsaWNfa2V5GAMgASgMEhYKDnJlcXVlc3RlZF9yb2xlGAQgASgJEjcKBmxhYmVscxgFIAMoCzInLmFnZW50bWVzaC52MS5FbnJvbGxSZXF1ZXN0LkxhYmVsc0VudHJ5EhkKEWNoYWxsZW5nZV91bml4X21zGAYgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYByABKAwaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKrAQoORW5yb2xsUmVzcG9uc2USFQoNYmlzY3VpdF90b2tlbhgBIAEoDBIVCg1lcnJvcl9tZXNzYWdlGAIgASgJEiAKGGNvbnRyb2xfcGxhbmVfcHVibGljX2tleRgDIAEoDBIYChByb3V0ZXJfYWRkcmVzc2VzGAQgAygJEi8KC2V4cGlyZV90aW1lGAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCKXAgoWQm9vdHN0cmFwRW5yb2xsUmVxdWVzdBIXCg9ib290c3RyYXBfdG9rZW4YASABKAkSDwoHcGVlcl9pZBgCIAEoCRISCgpwdWJsaWNfa2V5GAMgASgMEhYKDnJlcXVlc3RlZF9yb2xlGAQgASgJEkAKBmxhYmVscxgFIAMoCzIwLmFnZW50bWVzaC52MS5Cb290c3RyYXBFbnJvbGxSZXF1ZXN0LkxhYmVsc0VudHJ5EhkKEWNoYWxsZW5nZV91bml4X21zGAYgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYByABKAwaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKDAgoXQm9vdHN0cmFwRW5yb2xsUmVzcG9uc2USLgoGc3RhdHVzGAEgASgOMh4uYWdlbnRtZXNoLnYxLkVucm9sbG1lbnRTdGF0dXMSFQoNYmlzY3VpdF90b2tlbhgCIAEoDBIdChVwb2xsX2ludGVydmFsX3NlY29uZHMYAyABKAUSFQoNZXJyb3JfbWVzc2FnZRgEIAEoCRIgChhjb250cm9sX3BsYW5lX3B1YmxpY19rZXkYBSABKAwSGAoQcm91dGVyX2FkZHJlc3NlcxgGIAMoCRIvCgtleHBpcmVfdGltZRgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiWQoLU2VydmljZUluZm8SJwoEdHlwZRgBIAEoDjIZLmFnZW50bWVzaC52MS5TZXJ2aWNlVHlwZRIMCgRuYW1lGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJIoEBCg5Db21tYW5kQmFja2VuZBIPCgdjb21tYW5kGAEgAygJEjIKA2VudhgCIAMoCzIlLmFnZW50bWVzaC52MS5Db21tYW5kQmFja2VuZC5FbnZFbnRyeRoqCghFbnZFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIpYBChZSZWdpc3RlclNlcnZpY2VSZXF1ZXN0EioKB3NlcnZpY2UYASABKAsyGS5hZ2VudG1lc2gudjEuU2VydmljZUluZm8SFAoKdGFyZ2V0X3VybBgCIAEoCUgAEi8KB2NvbW1hbmQYAyABKAsyHC5hZ2VudG1lc2gudjEuQ29tbWFuZEJhY2tlbmRIAEIJCgdiYWNrZW5kImkKEkRpc2NvdmVyZWRQcm92aWRlchIPCgdwZWVyX2lkGAEgASgJEhcKD2xvY2FsX3Byb3h5X3VybBgCIAEoCRIQCghzcnZfbmFtZRgDIAEoCRIXCg9zcnZfZGVzY3JpcHRpb24YBCABKAki6AEKGENvbnRyb2xQbGFuZUluZm9SZXNwb25zZRITCgtvaWRjX2lzc3VlchgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSEAoIYXVkaWVuY2UYAyABKAkSGAoQcm91dGVyX2FkZHJlc3NlcxgEIAMoCRIXCg9iYW5uZWRfcGVlcl9pZHMYBSADKAkSKQoHcm91dGVycxgGIAMoCzIYLmFnZW50bWVzaC52MS5Sb3V0ZXJJbmZvEjQKCWRpcmVjdG9yeRgHIAEoCzIhLmFnZW50bWVzaC52MS5EaXJlY3RvcnlQYXJhbWV0ZXJzIpABChNEaXJlY3RvcnlQYXJhbWV0ZXJzEhMKC3JlcGxpY2F0aW9uGAEgASgFEjUKEnJlcHJvdmlkZV9pbnRlcnZhbBgCIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbhItCgpyZWNvcmRfdHRsGAMgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uIvsBChBEaXJlY3RvcnlSZXF1ZXN0EjEKB3Byb3ZpZGUYASABKAsyHi5hZ2VudG1lc2gudjEuRGlyZWN0b3J5UHJvdmlkZUgAEj4KDmZpbmRfcHJvdmlkZXJzGAIgASgLMiQuYWdlbnRtZXNoLnYxLkRpcmVjdG9yeUZpbmRQcm92aWRlcnNIABI0CglmaW5kX3BlZXIYAyABKAsyHy5hZ2VudG1lc2gudjEuRGlyZWN0b3J5RmluZFBlZXJIABIzCgh3aXRoZHJhdxgEIAEoCzIfLmFnZW50bWVzaC52MS5EaXJlY3RvcnlXaXRoZHJhd0gAQgkKB3JlcXVlc3QiQAoQRGlyZWN0b3J5UHJvdmlkZRILCgNrZXkYASABKAwSDQoFYWRkcnMYAiADKAkSEAoIcHJvdmlkZXIYAyABKAkiMgoRRGlyZWN0b3J5V2l0aGRyYXcSCwoDa2V5GAEgASgMEhAKCHByb3ZpZGVyGAIgASgJIjQKFkRpcmVjdG9yeUZpbmRQcm92aWRlcnMSCwoDa2V5GAEgASgMEg0KBWxpbWl0GAIgASgNIiQKEURpcmVjdG9yeUZpbmRQZWVyEg8KB3BlZXJfaWQYASABKAkidwoRRGlyZWN0b3J5UmVzcG9uc2USKwoJcHJvdmlkZXJzGAEgAygLMhguYWdlbnRtZXNoLnYxLlBlZXJSZWNvcmQSJgoEcGVlchgCIAEoCzIYLmFnZW50bWVzaC52MS5QZWVyUmVjb3JkEg0KBWVycm9yGAMgASgJIiwKClBlZXJSZWNvcmQSDwoHcGVlcl9pZBgBIAEoCRINCgVhZGRycxgCIAMoCSLEAQoKUm91dGVySW5mbxIPCgdwZWVyX2lkGAEgASgJEhEKCWFkZHJlc3NlcxgCIAMoCRI0CgZsYWJlbHMYAyADKAsyJC5hZ2VudG1lc2gudjEuUm91dGVySW5mby5MYWJlbHNFbnRyeRITCgtjb25uZWN0aW9ucxgEIAEoBRIYChBjb25uZWN0aW9uX2xpbWl0GAUgASgFGi0KC0xhYmVsc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEigwIKElJvdXRlckxlYXNlUmVxdWVzdBIPCgdwZWVyX2lkGAEgASgJEhEKCWFkZHJlc3NlcxgCIAMoCRIPCgdiaXNjdWl0GAMgASgMEhcKD2Nvbm5lY3RlZF9wZWVycxgEIAMoCRIQCghkaHRfc2l6ZRgFIAEoBRIZChFjaGFsbGVuZ2VfdW5peF9tcxgGIAEoAxIbChNjaGFsbGVuZ2Vfc2lnbmF0dXJlGAcgASgMEiYKA3R0bBgIIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbhITCgtjb25uZWN0aW9ucxgJIAEoBRIYChBjb25uZWN0aW9uX2xpbWl0GAogASgFImYKE1JvdXRlckxlYXNlUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCBINCgVlcnJvchgCIAEoCRIvCgtleHBpcmVfdGltZRgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAipAEKClBvbGljeVJvbGUSDAoEbmFtZRgBIAEoCRIXCg9hbGxvd2VkX3RhcmdldHMYAiADKAkSGAoQYWxsb3dlZF9zZXJ2aWNlcxgDIAMoCRIWCg5jdXN0b21fZGF0YWxvZxgEIAMoCRIWCg5hbGxvd2VkX2xhYmVscxgFIAMoCRIlCgRodHRwGAYgAygLMhcuYWdlbnRtZXNoLnYxLkhUVFBHcmFudCI8CglIVFRQR3JhbnQSDwoHc2VydmljZRgBIAEoCRIPCgdtZXRob2RzGAIgAygJEg0KBXBhdGhzGAMgAygJIqECChFFZ3Jlc3NEZXN0aW5hdGlvbhIMCgRuYW1lGAEgASgJEhIKCnRhcmdldF91cmwYAiABKAkSEgoKY3JlZGVudGlhbBgDIAEoCRIRCglzZXJ2ZWRfYnkYBCADKAkSLgoGYnJva2VyGAUgASgLMh4uYWdlbnRtZXNoLnYxLkNyZWRlbnRpYWxCcm9rZXISLAoKaW5zcGVjdGlvbhgGIAEoCzIYLmFnZW50bWVzaC52MS5JbnNwZWN0aW9uEiYKBG1vZGUYByABKA4yGC5hZ2VudG1lc2gudjEuRWdyZXNzTW9kZRINCgVwb3J0cxgIIAMoDRIVCg1wcmVzZXJ2ZV9ob3N0GAkgASgIEhcKD2ZvcndhcmRfY29udGV4dBgKIAEoCCI5CgpJbnNwZWN0aW9uEisKCmluc3BlY3RvcnMYASADKAsyFy5hZ2VudG1lc2gudjEuSW5zcGVjdG9yIm8KCUluc3BlY3RvchIvCgttb2RlbF9hcm1vchgBIAEoCzIYLmFnZW50bWVzaC52MS5Nb2RlbEFybW9ySAASKQoIZXh0X3Byb2MYAiABKAsyFS5hZ2VudG1lc2gudjEuRXh0UHJvY0gAQgYKBGtpbmQikQEKCk1vZGVsQXJtb3ISEAoIdGVtcGxhdGUYASABKAkSMgoIcmVzcG9uc2UYAiABKA4yIC5hZ2VudG1lc2gudjEuUmVzcG9uc2VJbnNwZWN0aW9uEhEKCWZhaWxfb3BlbhgDIAEoCBIqCgd0aW1lb3V0GAQgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uIogCCgdFeHRQcm9jEg4KBnRhcmdldBgBIAEoCRIKCgJjYRgCIAEoCRIaChJjbGllbnRfY2VydGlmaWNhdGUYAyABKAkSPAoPcHJvY2Vzc2luZ19tb2RlGAQgASgLMiMuYWdlbnRtZXNoLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZRIbChNhbGxvd19tb2RlX292ZXJyaWRlGAUgASgIEjIKD21lc3NhZ2VfdGltZW91dBgGIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbhIaChJmYWlsdXJlX21vZGVfYWxsb3cYByABKAgSGgoSbWF4X2J1ZmZlcmVkX2J5dGVzGAggASgNIv8EChVFeHRQcm9jUHJvY2Vzc2luZ01vZGUSSwoTcmVxdWVzdF9oZWFkZXJfbW9kZRgBIAEoDjIuLmFnZW50bWVzaC52MS5FeHRQcm9jUHJvY2Vzc2luZ01vZGUuSGVhZGVyTW9kZRJMChRyZXNwb25zZV9oZWFkZXJfbW9kZRgCIAEoDjIuLmFnZW50bWVzaC52MS5FeHRQcm9jUHJvY2Vzc2luZ01vZGUuSGVhZGVyTW9kZRJHChFyZXF1ZXN0X2JvZHlfbW9kZRgDIAEoDjIsLmFnZW50bWVzaC52MS5FeHRQcm9jUHJvY2Vzc2luZ01vZGUuQm9keU1vZGUSSAoScmVzcG9uc2VfYm9keV9tb2RlGAQgASgOMiwuYWdlbnRtZXNoLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZS5Cb2R5TW9kZRJMChRyZXF1ZXN0X3RyYWlsZXJfbW9kZRgFIAEoDjIuLmFnZW50bWVzaC52MS5FeHRQcm9jUHJvY2Vzc2luZ01vZGUuSGVhZGVyTW9kZRJNChVyZXNwb25zZV90cmFpbGVyX21vZGUYBiABKA4yLi5hZ2VudG1lc2gudjEuRXh0UHJvY1Byb2Nlc3NpbmdNb2RlLkhlYWRlck1vZGUiOQoKSGVhZGVyTW9kZRIXChNIRUFERVJfTU9ERV9ERUZBVUxUEAASCAoEU0VORBABEggKBFNLSVAQAiJgCghCb2R5TW9kZRIICgROT05FEAASDAoIU1RSRUFNRUQQARIMCghCVUZGRVJFRBACEhQKEEJVRkZFUkVEX1BBUlRJQUwQAxIYChRGVUxMX0RVUExFWF9TVFJFQU1FRBAEIuEBChBDcmVkZW50aWFsQnJva2VyEhcKDXN0YXRpY19zZWNyZXQYASABKAlIABI3Cg9vaWRjX2ZlZGVyYXRpb24YAiABKAsyHC5hZ2VudG1lc2gudjEuT0lEQ0ZlZGVyYXRpb25IABI2Cg9hd3NfYXNzdW1lX3JvbGUYAyABKAsyGy5hZ2VudG1lc2gudjEuQVdTQXNzdW1lUm9sZUgAEjsKEXBsYXRmb3JtX2lkZW50aXR5GAQgASgLMh4uYWdlbnRtZXNoLnYxLlBsYXRmb3JtSWRlbnRpdHlIAEIGCgRraW5kIl8KDk9JRENGZWRlcmF0aW9uEhYKDnRva2VuX2VuZHBvaW50GAEgASgJEhAKCGF1ZGllbmNlGAIgASgJEhMKC2ltcGVyc29uYXRlGAMgASgJEg4KBnNjb3BlcxgEIAMoCSI5Cg1BV1NBc3N1bWVSb2xlEhAKCHJvbGVfYXJuGAEgASgJEhYKDnNlc3Npb25fcG9saWN5GAIgASgJIiIKEFBsYXRmb3JtSWRlbnRpdHkSDgoGc2NvcGVzGAEgAygJIi4KDVBvbGljeUJpbmRpbmcSDAoEcm9sZRgBIAEoCRIPCgdtZW1iZXJzGAIgAygJIpcBCgxQb2xpY3lDb25maWcSJwoFcm9sZXMYASADKAsyGC5hZ2VudG1lc2gudjEuUG9saWN5Um9sZRItCghiaW5kaW5ncxgCIAMoCzIbLmFnZW50bWVzaC52MS5Qb2xpY3lCaW5kaW5nEi8KBmVncmVzcxgDIAMoCzIfLmFnZW50bWVzaC52MS5FZ3Jlc3NEZXN0aW5hdGlvbiIYChZQb2xpY3lDb25maWdHZXRSZXF1ZXN0IjAKF1BvbGljeUNvbmZpZ0dldFJlc3BvbnNlEhUKDWRhdGFsb2dfcnVsZXMYASADKAkiPAoaUG9saWN5Q29uZmlnVXBkYXRlUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCBINCgVlcnJvchgCIAEoCSIaChhFZ3Jlc3NBc3NpZ25tZW50c1JlcXVlc3QiTAoZRWdyZXNzQXNzaWdubWVudHNSZXNwb25zZRIvCgZlZ3Jlc3MYASADKAsyHy5hZ2VudG1lc2gudjEuRWdyZXNzRGVzdGluYXRpb24iZgoMS2V5c1Jlc3BvbnNlEhMKC3B1YmxpY19rZXlzGAEgAygMEi0KCXNpZ25fdGltZRgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASEgoKc2lnbmF0dXJlcxgDIAMoDCJrChNUb2tlblJlZnJlc2hSZXF1ZXN0EhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYASABKAwSGQoRY2hhbGxlbmdlX3VuaXhfbXMYAiABKAMSDwoHcGVlcl9pZBgDIAEoCRILCgNqd3QYBCABKAkidQoUVG9rZW5SZWZyZXNoUmVzcG9uc2USFQoNYmlzY3VpdF90b2tlbhgBIAEoDBIvCgtleHBpcmVfdGltZRgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASFQoNZXJyb3JfbWVzc2FnZRgDIAEoCSJAChFOb2RlQ2F0YWxvZ1JlcG9ydBIrCghzZXJ2aWNlcxgBIAMoCzIZLmFnZW50bWVzaC52MS5TZXJ2aWNlSW5mbyIlChJUb2tlblJldm9rZVJlcXVlc3QSDwoHcGVlcl9pZBgBIAEoCSI1ChNUb2tlblJldm9rZVJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgSDQoFZXJyb3IYAiABKAkilgEKG0Jvb3RzdHJhcFRva2VuQ3JlYXRlUmVxdWVzdBIMCgRyb2xlGAEgASgJEhAKCG93bmVyX2lkGAIgASgJEhEKCXR0bF9ob3VycxgDIAEoBRISCgptYXhfdXNhZ2VzGAQgASgFEhMKC2Rlc2NyaXB0aW9uGAUgASgJEhsKE2F1dG9ub21vdXNfcmVjb3ZlcnkYBiABKAgiigEKHEJvb3RzdHJhcFRva2VuQ3JlYXRlUmVzcG9uc2USCgoCaWQYASABKAkSDQoFdG9rZW4YAiABKAkSDAoEcm9sZRgDIAEoCRIQCghvd25lcl9pZBgEIAEoCRIvCgtleHBpcmVfdGltZRgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiqwIKDkJvb3RzdHJhcFRva2VuEgoKAmlkGAEgASgJEgwKBHJvbGUYAiABKAkSEAoIb3duZXJfaWQYAyABKAkSEgoKbWF4X3VzYWdlcxgEIAEoBRIUCgx1c2FnZXNfY291bnQYBSABKAUSEwoLZGVzY3JpcHRpb24YBiABKAkSLwoLY3JlYXRlX3RpbWUYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2V4cGlyZV90aW1lGAggASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIvCgtyZXZva2VfdGltZRgJIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGwoTYXV0b25vbW91c19yZWNvdmVyeRgKIAEoCCJKChpCb290c3RyYXBUb2tlbkxpc3RSZXNwb25zZRIsCgZ0b2tlbnMYASADKAsyHC5hZ2VudG1lc2gudjEuQm9vdHN0cmFwVG9rZW4i1gIKEUVucm9sbG1lbnRSZXF1ZXN0EgoKAmlkGAEgASgJEg8KB3BlZXJfaWQYAiABKAkSEAoIdG9rZW5faWQYAyABKAkSLgoGc3RhdHVzGAQgASgOMh4uYWdlbnRtZXNoLnYxLkVucm9sbG1lbnRTdGF0dXMSOwoGbGFiZWxzGAUgAygLMisuYWdlbnRtZXNoLnYxLkVucm9sbG1lbnRSZXF1ZXN0LkxhYmVsc0VudHJ5Ei8KC2NyZWF0ZV90aW1lGAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIwCgxyZXNvbHZlX3RpbWUYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhMKC3Jlc29sdmVkX2J5GAggASgJGi0KC0xhYmVsc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiUgodRW5yb2xsbWVudFJlcXVlc3RMaXN0UmVzcG9uc2USMQoIcmVxdWVzdHMYASADKAsyHy5hZ2VudG1lc2gudjEuRW5yb2xsbWVudFJlcXVlc3QicAoEVXNlchIKCgJpZBgBIAEoCRIOCgZpc3N1ZXIYAiABKAkSDQoFZW1haWwYAyABKAkSDAoEcm9sZRgEIAEoCRIvCgtjcmVhdGVfdGltZRgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAi4wIKDEVucm9sbGVkTm9kZRIPCgdwZWVyX2lkGAEgASgJEgwKBHJvbGUYAiABKAkSFwoPZW5yb2xsbWVudF90eXBlGAMgASgJEhMKC2NsYWltc19qc29uGAQgASgJEhAKCG93bmVyX2lkGAUgASgJEjYKBmxhYmVscxgGIAMoCzImLmFnZW50bWVzaC52MS5FbnJvbGxlZE5vZGUuTGFiZWxzRW50cnkSLwoLZW5yb2xsX3RpbWUYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2V4cGlyZV90aW1lGAggASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIOCgZiYW5uZWQYCSABKAgSGwoTYXV0b25vbW91c19yZWNvdmVyeRgKIAEoCBotCgtMYWJlbHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIsQBCgtSb3V0ZXJMZWFzZRIPCgdwZWVyX2lkGAEgASgJEhEKCWFkZHJlc3NlcxgCIAMoCRI1ChFsYXN0X3JlbmV3YWxfdGltZRgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLwoLZXhwaXJlX3RpbWUYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhcKD2Nvbm5lY3RlZF9wZWVycxgFIAMoCRIQCghkaHRfc2l6ZRgGIAEoBSJsCgxOb2RlU2VydmljZXMSKwoIc2VydmljZXMYASADKAsyGS5hZ2VudG1lc2gudjEuU2VydmljZUluZm8SLwoLcmVwb3J0X3RpbWUYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wItsDChNBZG1pblN0YXR1c1Jlc3BvbnNlEiEKBXVzZXJzGAEgAygLMhIuYWdlbnRtZXNoLnYxLlVzZXISMQoOYWN0aXZlX3JvdXRlcnMYAiADKAsyGS5hZ2VudG1lc2gudjEuUm91dGVyTGVhc2USMgoOZW5yb2xsZWRfbm9kZXMYAyADKAsyGi5hZ2VudG1lc2gudjEuRW5yb2xsZWROb2RlEjwKE2Vucm9sbG1lbnRfcmVxdWVzdHMYBCADKAsyHy5hZ2VudG1lc2gudjEuRW5yb2xsbWVudFJlcXVlc3QSNgoQYm9vdHN0cmFwX3Rva2VucxgFIAMoCzIcLmFnZW50bWVzaC52MS5Cb290c3RyYXBUb2tlbhIqCgZwb2xpY3kYBiABKAsyGi5hZ2VudG1lc2gudjEuUG9saWN5Q29uZmlnEkgKDG5vZGVfY2F0YWxvZxgHIAMoCzIyLmFnZW50bWVzaC52MS5BZG1pblN0YXR1c1Jlc3BvbnNlLk5vZGVDYXRhbG9nRW50cnkaTgoQTm9kZUNhdGFsb2dFbnRyeRILCgNrZXkYASABKAkSKQoFdmFsdWUYAiABKAsyGi5hZ2VudG1lc2gudjEuTm9kZVNlcnZpY2VzOgI4ASKBAgoSVXNlclN0YXR1c1Jlc3BvbnNlEiAKBHVzZXIYASABKAsyEi5hZ2VudG1lc2gudjEuVXNlchIyCg5lbnJvbGxlZF9ub2RlcxgCIAMoCzIaLmFnZW50bWVzaC52MS5FbnJvbGxlZE5vZGUSNgoQYm9vdHN0cmFwX3Rva2VucxgDIAMoCzIcLmFnZW50bWVzaC52MS5Cb290c3RyYXBUb2tlbhIxCg5hY3RpdmVfcm91dGVycxgEIAMoCzIZLmFnZW50bWVzaC52MS5Sb3V0ZXJMZWFzZRIqCgZwb2xpY3kYBSABKAsyGi5hZ2VudG1lc2gudjEuUG9saWN5Q29uZmlnIuQBChhJZGVudGl0eUV2aWRlbmNlUmVzcG9uc2USDwoHcGVlcl9pZBgBIAEoCRIPCgdiaXNjdWl0GAIgASgMEjcKE2Jpc2N1aXRfZXhwaXJlX3RpbWUYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhkKEWNvbnRyb2xfcGxhbmVfdXJsGAQgASgJEiIKGnRydXN0ZWRfY29udHJvbF9wbGFuZV9rZXlzGAUgAygMEi4KCmNoZWNrX3RpbWUYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIsYCChRQZWVyRXZpZGVuY2VSZXNwb25zZRIPCgdwZWVyX2lkGAEgASgJEg8KB2Jpc2N1aXQYAiABKAwSFQoNdmVyaWZ5aW5nX2tleRgDIAEoDBINCgVyb2xlcxgEIAMoCRI+CgZsYWJlbHMYBSADKAsyLi5hZ2VudG1lc2gudjEuUGVlckV2aWRlbmNlUmVzcG9uc2UuTGFiZWxzRW50cnkSLwoLZXhwaXJlX3RpbWUYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhYKDnJldm9jYXRpb25faWRzGAcgAygJEi4KCmNoZWNrX3RpbWUYCCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wGi0KC0xhYmVsc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEijAIKEE1lbWJlckNyZWRlbnRpYWwSGQoRY29udHJvbF9wbGFuZV91cmwYASABKAkSDwoHYmlzY3VpdBgCIAEoDBIvCgtleHBpcmVfdGltZRgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASNQoMdHJ1c3RlZF9rZXlzGAQgAygLMh8uYWdlbnRtZXNoLnYxLlRydXN0ZWRTaWduaW5nS2V5EhkKEWlzc3VlZF91bmRlcl9rZXlzGAUgAygMEhgKEHJvdXRlcl9hZGRyZXNzZXMYBiADKAkSLwoMb2lkY19zZXNzaW9uGAcgASgLMhkuYWdlbnRtZXNoLnYxLk9JRENTZXNzaW9uIlkKEVRydXN0ZWRTaWduaW5nS2V5EhIKCnB1YmxpY19rZXkYASABKAwSMAoMcmVjZWl2ZV90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJZCgtPSURDU2Vzc2lvbhIOCgZpc3N1ZXIYASABKAkSEQoJY2xpZW50X2lkGAIgASgJEhAKCGF1ZGllbmNlGAMgASgJEhUKDXJlZnJlc2hfdG9rZW4YBCABKAkikwEKFVRhc2tBdXRob3JpemF0aW9uUnVsZRIMCgRuYW1lGAEgASgJEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRIlCgVydWxlcxgDIAMoCzIWLmFnZW50bWVzaC52MS5UYXNrUnVsZRIvCgtleHBpcmVfdGltZRgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAihAEKCFRhc2tSdWxlEhMKC2Rlc2NyaXB0aW9uGAEgASgJEhgKEGFsbG93ZWRfc2VydmljZXMYAiADKAkSLgoJb3BlcmF0aW9uGAMgASgLMhsuYWdlbnRtZXNoLnYxLlRhc2tPcGVyYXRpb24SGQoRYWxsb3dlZF9yZXNvdXJjZXMYBCADKAkicwoNVGFza09wZXJhdGlvbhIVCg1hbGxvd2VkX3Rvb2xzGAEgAygJEhcKD2FsbG93ZWRfbWV0aG9kcxgCIAMoCRIVCg1hbGxvd2VkX3BhdGhzGAMgAygJEhsKE2FsbG93ZWRfcGVybWlzc2lvbnMYBCADKAkiqwEKFFRva2VuRXhjaGFuZ2VSZXF1ZXN0EhUKDXN1YmplY3RfdG9rZW4YASABKAkSNgoJdGFza19ydWxlGAIgASgLMiMuYWdlbnRtZXNoLnYxLlRhc2tBdXRob3JpemF0aW9uUnVsZRIMCgRzZWFsGAMgASgIEhkKEWNoYWxsZW5nZV91bml4X21zGAQgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYBSABKAwifwoVVG9rZW5FeGNoYW5nZVJlc3BvbnNlEhUKDWJpc2N1aXRfdG9rZW4YASABKAwSLwoLZXhwaXJlX3RpbWUYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEg0KBXJvbGVzGAMgAygJEg8KB3N1YmplY3QYBCABKAkigQEKD1NUU1Rva2VuUmVxdWVzdBIPCgdiaXNjdWl0GAEgASgMEhMKC2Rlc3RpbmF0aW9uGAIgASgJEhAKCGF1ZGllbmNlGAMgASgJEhkKEWNoYWxsZW5nZV91bml4X21zGAQgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYBSABKAwigwEKEFNUU1Rva2VuUmVzcG9uc2USCwoDand0GAEgASgJEi8KC2V4cGlyZV90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIPCgdzdWJqZWN0GAMgASgJEg0KBXJvbGVzGAQgAygJEhEKCXRhc2tfbmFtZRgFIAEoCSJGChNSZXZvY2F0aW9uc1Jlc3BvbnNlEhYKDnJldm9jYXRpb25faWRzGAEgAygJEhcKD2Jhbm5lZF9wZWVyX2lkcxgCIAMoCSqUAQoQRW5yb2xsbWVudFN0YXR1cxIhCh1FTlJPTExNRU5UX1NUQVRVU19VTlNQRUNJRklFRBAAEh0KGUVOUk9MTE1FTlRfU1RBVFVTX1BFTkRJTkcQARIeChpFTlJPTExNRU5UX1NUQVRVU19BUFBST1ZFRBACEh4KGkVOUk9MTE1FTlRfU1RBVFVTX1JFSkVDVEVEEAMqjAEKC1NlcnZpY2VUeXBlEhwKGFNFUlZJQ0VfVFlQRV9VTlNQRUNJRklFRBAAEhQKEFNFUlZJQ0VfVFlQRV9NQ1AQARIaChZTRVJWSUNFX1RZUEVfSU5GRVJFTkNFEAISFAoQU0VSVklDRV9UWVBFX0EyQRADEhcKE1NFUlZJQ0VfVFlQRV9FR1JFU1MQBCo3CgpFZ3Jlc3NNb2RlEhQKEEVHUkVTU19NT0RFX0hUVFAQABITCg9FR1JFU1NfTU9ERV9UQ1AQASpcChJSZXNwb25zZUluc3BlY3Rpb24SIAocUkVTUE9OU0VfSU5TUEVDVElPTl9CVUZGRVJFRBAAEiQKIFJFU1BPTlNFX0lOU1BFQ1RJT05fUkVRVUVTVF9PTkxZEAFCIVofZ2l0aHViLmNvbS9nb29nbGUvYWdlbnRtZXNoL2FwaWIGcHJvdG8z", [file_google_protobuf_duration, file_google_protobuf_timestamp]); /** * @generated from message agentmesh.v1.AuthFrame @@ -655,9 +655,11 @@ export const DirectoryParametersSchema: GenMessage = /*@__P /** * DirectoryRequest is what a member asks a router it is attached to over * /mesh/directory/1.0.0: one request per stream, answered with one - * DirectoryResponse. The member keeps no DHT of its own; the router runs - * the lookup across the routers' DHT and answers from it. A router sends - * the same message to another router to place a member's record on it. + * DirectoryResponse. The member keeps no DHT of its own; the router keeps + * its records, places them on the routers closest to each key, and answers + * a lookup from its own records and those routers'. A router sends the + * same message to another router to place a member's record, or its + * withdrawal, on it. * * @generated from message agentmesh.v1.DirectoryRequest */ @@ -683,6 +685,12 @@ export type DirectoryRequest = Message<"agentmesh.v1.DirectoryRequest"> & { */ value: DirectoryFindPeer; case: "findPeer"; + } | { + /** + * @generated from field: agentmesh.v1.DirectoryWithdraw withdraw = 4; + */ + value: DirectoryWithdraw; + case: "withdraw"; } | { case: undefined; value?: undefined }; }; @@ -696,9 +704,11 @@ export const DirectoryRequestSchema: GenMessage = /*@__PURE__* /** * DirectoryProvide announces the sender as a provider of key, reached at * addrs. A key is a sha256 multihash: of "mesh:service:[:]" for - * a service, of "mesh:peer:" for the member itself. provider is set - * only between routers, by the router that holds the member, and refused - * from anyone else: a member can only announce itself. + * a service, of "mesh:peer:" for the member itself. A record lives + * for the mesh's record TTL after its last announcement; announcing it + * again replaces its addresses. provider is set only between routers, by + * the router that holds the member, and refused from anyone else: a member + * can only announce itself. * * @generated from message agentmesh.v1.DirectoryProvide */ @@ -726,6 +736,33 @@ export type DirectoryProvide = Message<"agentmesh.v1.DirectoryProvide"> & { export const DirectoryProvideSchema: GenMessage = /*@__PURE__*/ messageDesc(file_agentmesh, 15); +/** + * DirectoryWithdraw removes the sender's record under key, from the router + * asked and from the routers closest to the key: what a member sends when + * it stops a service or leaves the mesh, so that callers do not find it + * until the record would have expired. provider is as in DirectoryProvide. + * + * @generated from message agentmesh.v1.DirectoryWithdraw + */ +export type DirectoryWithdraw = Message<"agentmesh.v1.DirectoryWithdraw"> & { + /** + * @generated from field: bytes key = 1; + */ + key: Uint8Array; + + /** + * @generated from field: string provider = 2; + */ + provider: string; +}; + +/** + * Describes the message agentmesh.v1.DirectoryWithdraw. + * Use `create(DirectoryWithdrawSchema)` to create a new message. + */ +export const DirectoryWithdrawSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_agentmesh, 16); + /** * DirectoryFindProviders asks for up to limit providers of key. * @@ -748,7 +785,7 @@ export type DirectoryFindProviders = Message<"agentmesh.v1.DirectoryFindProvider * Use `create(DirectoryFindProvidersSchema)` to create a new message. */ export const DirectoryFindProvidersSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 16); + messageDesc(file_agentmesh, 17); /** * DirectoryFindPeer asks where peer_id is reached. @@ -767,7 +804,7 @@ export type DirectoryFindPeer = Message<"agentmesh.v1.DirectoryFindPeer"> & { * Use `create(DirectoryFindPeerSchema)` to create a new message. */ export const DirectoryFindPeerSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 17); + messageDesc(file_agentmesh, 18); /** * DirectoryResponse answers one DirectoryRequest. providers answers @@ -798,7 +835,7 @@ export type DirectoryResponse = Message<"agentmesh.v1.DirectoryResponse"> & { * Use `create(DirectoryResponseSchema)` to create a new message. */ export const DirectoryResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 18); + messageDesc(file_agentmesh, 19); /** * PeerRecord is a peer and the addresses it is reached at, relayed ones @@ -823,7 +860,7 @@ export type PeerRecord = Message<"agentmesh.v1.PeerRecord"> & { * Use `create(PeerRecordSchema)` to create a new message. */ export const PeerRecordSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 19); + messageDesc(file_agentmesh, 20); /** * RouterInfo is one router as the control plane lists it on /info. @@ -876,7 +913,7 @@ export type RouterInfo = Message<"agentmesh.v1.RouterInfo"> & { * Use `create(RouterInfoSchema)` to create a new message. */ export const RouterInfoSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 20); + messageDesc(file_agentmesh, 21); /** * @generated from message agentmesh.v1.RouterLeaseRequest @@ -953,7 +990,7 @@ export type RouterLeaseRequest = Message<"agentmesh.v1.RouterLeaseRequest"> & { * Use `create(RouterLeaseRequestSchema)` to create a new message. */ export const RouterLeaseRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 21); + messageDesc(file_agentmesh, 22); /** * @generated from message agentmesh.v1.RouterLeaseResponse @@ -980,7 +1017,7 @@ export type RouterLeaseResponse = Message<"agentmesh.v1.RouterLeaseResponse"> & * Use `create(RouterLeaseResponseSchema)` to create a new message. */ export const RouterLeaseResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 22); + messageDesc(file_agentmesh, 23); /** * @generated from message agentmesh.v1.PolicyRole @@ -1028,7 +1065,7 @@ export type PolicyRole = Message<"agentmesh.v1.PolicyRole"> & { * Use `create(PolicyRoleSchema)` to create a new message. */ export const PolicyRoleSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 23); + messageDesc(file_agentmesh, 24); /** * HTTPGrant narrows one allowed_services entry to HTTP methods and paths. @@ -1070,7 +1107,7 @@ export type HTTPGrant = Message<"agentmesh.v1.HTTPGrant"> & { * Use `create(HTTPGrantSchema)` to create a new message. */ export const HTTPGrantSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 24); + messageDesc(file_agentmesh, 25); /** * EgressDestination is a destination outside the mesh that selected nodes @@ -1169,7 +1206,7 @@ export type EgressDestination = Message<"agentmesh.v1.EgressDestination"> & { * Use `create(EgressDestinationSchema)` to create a new message. */ export const EgressDestinationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 25); + messageDesc(file_agentmesh, 26); /** * Inspection lists the inspectors the egress node runs, in order; the first @@ -1190,7 +1227,7 @@ export type Inspection = Message<"agentmesh.v1.Inspection"> & { * Use `create(InspectionSchema)` to create a new message. */ export const InspectionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 26); + messageDesc(file_agentmesh, 27); /** * @generated from message agentmesh.v1.Inspector @@ -1219,7 +1256,7 @@ export type Inspector = Message<"agentmesh.v1.Inspector"> & { * Use `create(InspectorSchema)` to create a new message. */ export const InspectorSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 27); + messageDesc(file_agentmesh, 28); /** * ModelArmor calls sanitizeUserPrompt / sanitizeModelResponse directly over @@ -1264,7 +1301,7 @@ export type ModelArmor = Message<"agentmesh.v1.ModelArmor"> & { * Use `create(ModelArmorSchema)` to create a new message. */ export const ModelArmorSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 28); + messageDesc(file_agentmesh, 29); /** * ExtProc runs an Envoy external processor (envoy.service.ext_proc.v3 @@ -1334,7 +1371,7 @@ export type ExtProc = Message<"agentmesh.v1.ExtProc"> & { * Use `create(ExtProcSchema)` to create a new message. */ export const ExtProcSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 29); + messageDesc(file_agentmesh, 30); /** * @generated from message agentmesh.v1.ExtProcProcessingMode @@ -1376,7 +1413,7 @@ export type ExtProcProcessingMode = Message<"agentmesh.v1.ExtProcProcessingMode" * Use `create(ExtProcProcessingModeSchema)` to create a new message. */ export const ExtProcProcessingModeSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 30); + messageDesc(file_agentmesh, 31); /** * @generated from enum agentmesh.v1.ExtProcProcessingMode.HeaderMode @@ -1402,7 +1439,7 @@ export enum ExtProcProcessingMode_HeaderMode { * Describes the enum agentmesh.v1.ExtProcProcessingMode.HeaderMode. */ export const ExtProcProcessingMode_HeaderModeSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_agentmesh, 30, 0); + enumDesc(file_agentmesh, 31, 0); /** * @generated from enum agentmesh.v1.ExtProcProcessingMode.BodyMode @@ -1438,7 +1475,7 @@ export enum ExtProcProcessingMode_BodyMode { * Describes the enum agentmesh.v1.ExtProcProcessingMode.BodyMode. */ export const ExtProcProcessingMode_BodyModeSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_agentmesh, 30, 1); + enumDesc(file_agentmesh, 31, 1); /** * @generated from message agentmesh.v1.CredentialBroker @@ -1484,7 +1521,7 @@ export type CredentialBroker = Message<"agentmesh.v1.CredentialBroker"> & { * Use `create(CredentialBrokerSchema)` to create a new message. */ export const CredentialBrokerSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 31); + messageDesc(file_agentmesh, 32); /** * OIDCFederation exchanges the control plane's JWT at a provider STS. @@ -1529,7 +1566,7 @@ export type OIDCFederation = Message<"agentmesh.v1.OIDCFederation"> & { * Use `create(OIDCFederationSchema)` to create a new message. */ export const OIDCFederationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 32); + messageDesc(file_agentmesh, 33); /** * @generated from message agentmesh.v1.AWSAssumeRole @@ -1553,7 +1590,7 @@ export type AWSAssumeRole = Message<"agentmesh.v1.AWSAssumeRole"> & { * Use `create(AWSAssumeRoleSchema)` to create a new message. */ export const AWSAssumeRoleSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 33); + messageDesc(file_agentmesh, 34); /** * @generated from message agentmesh.v1.PlatformIdentity @@ -1570,7 +1607,7 @@ export type PlatformIdentity = Message<"agentmesh.v1.PlatformIdentity"> & { * Use `create(PlatformIdentitySchema)` to create a new message. */ export const PlatformIdentitySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 34); + messageDesc(file_agentmesh, 35); /** * @generated from message agentmesh.v1.PolicyBinding @@ -1592,7 +1629,7 @@ export type PolicyBinding = Message<"agentmesh.v1.PolicyBinding"> & { * Use `create(PolicyBindingSchema)` to create a new message. */ export const PolicyBindingSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 35); + messageDesc(file_agentmesh, 36); /** * PolicyConfig is the mesh policy as the operator writes it: roles and @@ -1624,7 +1661,7 @@ export type PolicyConfig = Message<"agentmesh.v1.PolicyConfig"> & { * Use `create(PolicyConfigSchema)` to create a new message. */ export const PolicyConfigSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 36); + messageDesc(file_agentmesh, 37); /** * @generated from message agentmesh.v1.PolicyConfigGetRequest @@ -1637,7 +1674,7 @@ export type PolicyConfigGetRequest = Message<"agentmesh.v1.PolicyConfigGetReques * Use `create(PolicyConfigGetRequestSchema)` to create a new message. */ export const PolicyConfigGetRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 37); + messageDesc(file_agentmesh, 38); /** * PolicyConfigGetResponse answers GET /policies for a mesh member holding a @@ -1660,7 +1697,7 @@ export type PolicyConfigGetResponse = Message<"agentmesh.v1.PolicyConfigGetRespo * Use `create(PolicyConfigGetResponseSchema)` to create a new message. */ export const PolicyConfigGetResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 38); + messageDesc(file_agentmesh, 39); /** * @generated from message agentmesh.v1.PolicyConfigUpdateResponse @@ -1682,7 +1719,7 @@ export type PolicyConfigUpdateResponse = Message<"agentmesh.v1.PolicyConfigUpdat * Use `create(PolicyConfigUpdateResponseSchema)` to create a new message. */ export const PolicyConfigUpdateResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 39); + messageDesc(file_agentmesh, 40); /** * @generated from message agentmesh.v1.EgressAssignmentsRequest @@ -1695,7 +1732,7 @@ export type EgressAssignmentsRequest = Message<"agentmesh.v1.EgressAssignmentsRe * Use `create(EgressAssignmentsRequestSchema)` to create a new message. */ export const EgressAssignmentsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 40); + messageDesc(file_agentmesh, 41); /** * EgressAssignmentsResponse answers GET /egress for a mesh member holding a @@ -1717,7 +1754,7 @@ export type EgressAssignmentsResponse = Message<"agentmesh.v1.EgressAssignmentsR * Use `create(EgressAssignmentsResponseSchema)` to create a new message. */ export const EgressAssignmentsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 41); + messageDesc(file_agentmesh, 42); /** * @generated from message agentmesh.v1.KeysResponse @@ -1752,7 +1789,7 @@ export type KeysResponse = Message<"agentmesh.v1.KeysResponse"> & { * Use `create(KeysResponseSchema)` to create a new message. */ export const KeysResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 42); + messageDesc(file_agentmesh, 43); /** * @generated from message agentmesh.v1.TokenRefreshRequest @@ -1805,7 +1842,7 @@ export type TokenRefreshRequest = Message<"agentmesh.v1.TokenRefreshRequest"> & * Use `create(TokenRefreshRequestSchema)` to create a new message. */ export const TokenRefreshRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 43); + messageDesc(file_agentmesh, 44); /** * @generated from message agentmesh.v1.TokenRefreshResponse @@ -1832,7 +1869,7 @@ export type TokenRefreshResponse = Message<"agentmesh.v1.TokenRefreshResponse"> * Use `create(TokenRefreshResponseSchema)` to create a new message. */ export const TokenRefreshResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 44); + messageDesc(file_agentmesh, 45); /** * NodeCatalogReport is the body of POST /nodes/catalog: a node's @@ -1854,7 +1891,7 @@ export type NodeCatalogReport = Message<"agentmesh.v1.NodeCatalogReport"> & { * Use `create(NodeCatalogReportSchema)` to create a new message. */ export const NodeCatalogReportSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 45); + messageDesc(file_agentmesh, 46); /** * @generated from message agentmesh.v1.TokenRevokeRequest @@ -1871,7 +1908,7 @@ export type TokenRevokeRequest = Message<"agentmesh.v1.TokenRevokeRequest"> & { * Use `create(TokenRevokeRequestSchema)` to create a new message. */ export const TokenRevokeRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 46); + messageDesc(file_agentmesh, 47); /** * @generated from message agentmesh.v1.TokenRevokeResponse @@ -1893,7 +1930,7 @@ export type TokenRevokeResponse = Message<"agentmesh.v1.TokenRevokeResponse"> & * Use `create(TokenRevokeResponseSchema)` to create a new message. */ export const TokenRevokeResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 47); + messageDesc(file_agentmesh, 48); /** * BootstrapTokenCreateRequest is the body of POST /admin/bootstrap-tokens @@ -1956,7 +1993,7 @@ export type BootstrapTokenCreateRequest = Message<"agentmesh.v1.BootstrapTokenCr * Use `create(BootstrapTokenCreateRequestSchema)` to create a new message. */ export const BootstrapTokenCreateRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 48); + messageDesc(file_agentmesh, 49); /** * BootstrapTokenCreateResponse is returned (201) when a token is minted. @@ -1997,7 +2034,7 @@ export type BootstrapTokenCreateResponse = Message<"agentmesh.v1.BootstrapTokenC * Use `create(BootstrapTokenCreateResponseSchema)` to create a new message. */ export const BootstrapTokenCreateResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 49); + messageDesc(file_agentmesh, 50); /** * BootstrapToken is a minted token as operators list it. @@ -2064,7 +2101,7 @@ export type BootstrapToken = Message<"agentmesh.v1.BootstrapToken"> & { * Use `create(BootstrapTokenSchema)` to create a new message. */ export const BootstrapTokenSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 50); + messageDesc(file_agentmesh, 51); /** * @generated from message agentmesh.v1.BootstrapTokenListResponse @@ -2081,7 +2118,7 @@ export type BootstrapTokenListResponse = Message<"agentmesh.v1.BootstrapTokenLis * Use `create(BootstrapTokenListResponseSchema)` to create a new message. */ export const BootstrapTokenListResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 51); + messageDesc(file_agentmesh, 52); /** * EnrollmentRequest is a bootstrap enrollment awaiting or past an operator @@ -2142,7 +2179,7 @@ export type EnrollmentRequest = Message<"agentmesh.v1.EnrollmentRequest"> & { * Use `create(EnrollmentRequestSchema)` to create a new message. */ export const EnrollmentRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 52); + messageDesc(file_agentmesh, 53); /** * @generated from message agentmesh.v1.EnrollmentRequestListResponse @@ -2159,7 +2196,7 @@ export type EnrollmentRequestListResponse = Message<"agentmesh.v1.EnrollmentRequ * Use `create(EnrollmentRequestListResponseSchema)` to create a new message. */ export const EnrollmentRequestListResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 53); + messageDesc(file_agentmesh, 54); /** * User is a human identity known to the mesh. @@ -2202,7 +2239,7 @@ export type User = Message<"agentmesh.v1.User"> & { * Use `create(UserSchema)` to create a new message. */ export const UserSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 54); + messageDesc(file_agentmesh, 55); /** * EnrolledNode is a member's enrollment record without its credential. @@ -2272,7 +2309,7 @@ export type EnrolledNode = Message<"agentmesh.v1.EnrolledNode"> & { * Use `create(EnrolledNodeSchema)` to create a new message. */ export const EnrolledNodeSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 55); + messageDesc(file_agentmesh, 56); /** * RouterLease is a router's current registration with the control plane. @@ -2318,7 +2355,7 @@ export type RouterLease = Message<"agentmesh.v1.RouterLease"> & { * Use `create(RouterLeaseSchema)` to create a new message. */ export const RouterLeaseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 56); + messageDesc(file_agentmesh, 57); /** * NodeServices is the services one node last reported (see @@ -2343,7 +2380,7 @@ export type NodeServices = Message<"agentmesh.v1.NodeServices"> & { * Use `create(NodeServicesSchema)` to create a new message. */ export const NodeServicesSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 57); + messageDesc(file_agentmesh, 58); /** * AdminStatusResponse answers GET /admin/status: everything the console @@ -2395,7 +2432,7 @@ export type AdminStatusResponse = Message<"agentmesh.v1.AdminStatusResponse"> & * Use `create(AdminStatusResponseSchema)` to create a new message. */ export const AdminStatusResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 58); + messageDesc(file_agentmesh, 59); /** * UserStatusResponse answers GET /user/status: the caller and what it owns. @@ -2436,7 +2473,7 @@ export type UserStatusResponse = Message<"agentmesh.v1.UserStatusResponse"> & { * Use `create(UserStatusResponseSchema)` to create a new message. */ export const UserStatusResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 59); + messageDesc(file_agentmesh, 60); /** * @generated from message agentmesh.v1.IdentityEvidenceResponse @@ -2480,7 +2517,7 @@ export type IdentityEvidenceResponse = Message<"agentmesh.v1.IdentityEvidenceRes * Use `create(IdentityEvidenceResponseSchema)` to create a new message. */ export const IdentityEvidenceResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 60); + messageDesc(file_agentmesh, 61); /** * @generated from message agentmesh.v1.PeerEvidenceResponse @@ -2536,7 +2573,7 @@ export type PeerEvidenceResponse = Message<"agentmesh.v1.PeerEvidenceResponse"> * Use `create(PeerEvidenceResponseSchema)` to create a new message. */ export const PeerEvidenceResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 61); + messageDesc(file_agentmesh, 62); /** * @generated from message agentmesh.v1.MemberCredential @@ -2601,7 +2638,7 @@ export type MemberCredential = Message<"agentmesh.v1.MemberCredential"> & { * Use `create(MemberCredentialSchema)` to create a new message. */ export const MemberCredentialSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 62); + messageDesc(file_agentmesh, 63); /** * @generated from message agentmesh.v1.TrustedSigningKey @@ -2628,7 +2665,7 @@ export type TrustedSigningKey = Message<"agentmesh.v1.TrustedSigningKey"> & { * Use `create(TrustedSigningKeySchema)` to create a new message. */ export const TrustedSigningKeySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 63); + messageDesc(file_agentmesh, 64); /** * @generated from message agentmesh.v1.OIDCSession @@ -2660,7 +2697,7 @@ export type OIDCSession = Message<"agentmesh.v1.OIDCSession"> & { * Use `create(OIDCSessionSchema)` to create a new message. */ export const OIDCSessionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 64); + messageDesc(file_agentmesh, 65); /** * TaskAuthorizationRule narrows a credential's authority for a specific task or @@ -2704,7 +2741,7 @@ export type TaskAuthorizationRule = Message<"agentmesh.v1.TaskAuthorizationRule" * Use `create(TaskAuthorizationRuleSchema)` to create a new message. */ export const TaskAuthorizationRuleSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 65); + messageDesc(file_agentmesh, 66); /** * @generated from message agentmesh.v1.TaskRule @@ -2748,7 +2785,7 @@ export type TaskRule = Message<"agentmesh.v1.TaskRule"> & { * Use `create(TaskRuleSchema)` to create a new message. */ export const TaskRuleSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 66); + messageDesc(file_agentmesh, 67); /** * @generated from message agentmesh.v1.TaskOperation @@ -2790,7 +2827,7 @@ export type TaskOperation = Message<"agentmesh.v1.TaskOperation"> & { * Use `create(TaskOperationSchema)` to create a new message. */ export const TaskOperationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 67); + messageDesc(file_agentmesh, 68); /** * TokenExchangeRequest is the body of POST /token/exchange on the control @@ -2848,7 +2885,7 @@ export type TokenExchangeRequest = Message<"agentmesh.v1.TokenExchangeRequest"> * Use `create(TokenExchangeRequestSchema)` to create a new message. */ export const TokenExchangeRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 68); + messageDesc(file_agentmesh, 69); /** * @generated from message agentmesh.v1.TokenExchangeResponse @@ -2880,7 +2917,7 @@ export type TokenExchangeResponse = Message<"agentmesh.v1.TokenExchangeResponse" * Use `create(TokenExchangeResponseSchema)` to create a new message. */ export const TokenExchangeResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 69); + messageDesc(file_agentmesh, 70); /** * STSTokenRequest is the body of POST /sts/token on the control plane: an @@ -2934,7 +2971,7 @@ export type STSTokenRequest = Message<"agentmesh.v1.STSTokenRequest"> & { * Use `create(STSTokenRequestSchema)` to create a new message. */ export const STSTokenRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 70); + messageDesc(file_agentmesh, 71); /** * @generated from message agentmesh.v1.STSTokenResponse @@ -2971,7 +3008,7 @@ export type STSTokenResponse = Message<"agentmesh.v1.STSTokenResponse"> & { * Use `create(STSTokenResponseSchema)` to create a new message. */ export const STSTokenResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 71); + messageDesc(file_agentmesh, 72); /** * RevocationsResponse answers GET /revocations on the control plane: the set of @@ -2997,7 +3034,7 @@ export type RevocationsResponse = Message<"agentmesh.v1.RevocationsResponse"> & * Use `create(RevocationsResponseSchema)` to create a new message. */ export const RevocationsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_agentmesh, 72); + messageDesc(file_agentmesh, 73); /** * @generated from enum agentmesh.v1.EnrollmentStatus diff --git a/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.py b/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.py index 38dbd2d4..3c8dd1e9 100644 --- a/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.py +++ b/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.py @@ -15,7 +15,7 @@ from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0f\x61gentmesh.proto\x12\x0c\x61gentmesh.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"4\n\tAuthFrame\x12\x0f\n\x07\x62iscuit\x18\x01 \x01(\x0c\x12\x16\n\x0etarget_service\x18\x02 \x01(\t\"?\n\x0c\x41uthResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x03 \x01(\x0c\"\xb1\x01\n\x0cRouterGoAway\x12\x31\n\x06reason\x18\x01 \x01(\x0e\x32!.agentmesh.v1.RouterGoAway.Reason\x12.\n\x0bretry_after\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\">\n\x06Reason\x12\x16\n\x12REASON_UNSPECIFIED\x10\x00\x12\x0c\n\x08\x44RAINING\x10\x01\x12\x0e\n\nOVERLOADED\x10\x02\"\xdc\x01\n\tMeshEvent\x12*\n\x04type\x18\x01 \x01(\x0e\x32\x1c.agentmesh.v1.MeshEvent.Type\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12.\n\nevent_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0enew_public_key\x18\x04 \x01(\x0c\x12\x11\n\tsignature\x18\x05 \x01(\x0c\"7\n\x04Type\x12\n\n\x06\x42\x41NNED\x10\x00\x12\x10\n\x0cKEY_ROTATION\x10\x01\x12\x11\n\rPOLICY_UPDATE\x10\x02\"\xf9\x01\n\rEnrollRequest\x12\x0b\n\x03jwt\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x12\n\npublic_key\x18\x03 \x01(\x0c\x12\x16\n\x0erequested_role\x18\x04 \x01(\t\x12\x37\n\x06labels\x18\x05 \x03(\x0b\x32\'.agentmesh.v1.EnrollRequest.LabelsEntry\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xab\x01\n\x0e\x45nrollResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12 \n\x18\x63ontrol_plane_public_key\x18\x03 \x01(\x0c\x12\x18\n\x10router_addresses\x18\x04 \x03(\t\x12/\n\x0b\x65xpire_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x97\x02\n\x16\x42ootstrapEnrollRequest\x12\x17\n\x0f\x62ootstrap_token\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x12\n\npublic_key\x18\x03 \x01(\x0c\x12\x16\n\x0erequested_role\x18\x04 \x01(\t\x12@\n\x06labels\x18\x05 \x03(\x0b\x32\x30.agentmesh.v1.BootstrapEnrollRequest.LabelsEntry\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x83\x02\n\x17\x42ootstrapEnrollResponse\x12.\n\x06status\x18\x01 \x01(\x0e\x32\x1e.agentmesh.v1.EnrollmentStatus\x12\x15\n\rbiscuit_token\x18\x02 \x01(\x0c\x12\x1d\n\x15poll_interval_seconds\x18\x03 \x01(\x05\x12\x15\n\rerror_message\x18\x04 \x01(\t\x12 \n\x18\x63ontrol_plane_public_key\x18\x05 \x01(\x0c\x12\x18\n\x10router_addresses\x18\x06 \x03(\t\x12/\n\x0b\x65xpire_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"Y\n\x0bServiceInfo\x12\'\n\x04type\x18\x01 \x01(\x0e\x32\x19.agentmesh.v1.ServiceType\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\"\x81\x01\n\x0e\x43ommandBackend\x12\x0f\n\x07\x63ommand\x18\x01 \x03(\t\x12\x32\n\x03\x65nv\x18\x02 \x03(\x0b\x32%.agentmesh.v1.CommandBackend.EnvEntry\x1a*\n\x08\x45nvEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x96\x01\n\x16RegisterServiceRequest\x12*\n\x07service\x18\x01 \x01(\x0b\x32\x19.agentmesh.v1.ServiceInfo\x12\x14\n\ntarget_url\x18\x02 \x01(\tH\x00\x12/\n\x07\x63ommand\x18\x03 \x01(\x0b\x32\x1c.agentmesh.v1.CommandBackendH\x00\x42\t\n\x07\x62\x61\x63kend\"i\n\x12\x44iscoveredProvider\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x17\n\x0flocal_proxy_url\x18\x02 \x01(\t\x12\x10\n\x08srv_name\x18\x03 \x01(\t\x12\x17\n\x0fsrv_description\x18\x04 \x01(\t\"\xe8\x01\n\x18\x43ontrolPlaneInfoResponse\x12\x13\n\x0boidc_issuer\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x18\n\x10router_addresses\x18\x04 \x03(\t\x12\x17\n\x0f\x62\x61nned_peer_ids\x18\x05 \x03(\t\x12)\n\x07routers\x18\x06 \x03(\x0b\x32\x18.agentmesh.v1.RouterInfo\x12\x34\n\tdirectory\x18\x07 \x01(\x0b\x32!.agentmesh.v1.DirectoryParameters\"\x90\x01\n\x13\x44irectoryParameters\x12\x13\n\x0breplication\x18\x01 \x01(\x05\x12\x35\n\x12reprovide_interval\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\x12-\n\nrecord_ttl\x18\x03 \x01(\x0b\x32\x19.google.protobuf.Duration\"\xc6\x01\n\x10\x44irectoryRequest\x12\x31\n\x07provide\x18\x01 \x01(\x0b\x32\x1e.agentmesh.v1.DirectoryProvideH\x00\x12>\n\x0e\x66ind_providers\x18\x02 \x01(\x0b\x32$.agentmesh.v1.DirectoryFindProvidersH\x00\x12\x34\n\tfind_peer\x18\x03 \x01(\x0b\x32\x1f.agentmesh.v1.DirectoryFindPeerH\x00\x42\t\n\x07request\"@\n\x10\x44irectoryProvide\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\r\n\x05\x61\x64\x64rs\x18\x02 \x03(\t\x12\x10\n\x08provider\x18\x03 \x01(\t\"4\n\x16\x44irectoryFindProviders\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\r\n\x05limit\x18\x02 \x01(\r\"$\n\x11\x44irectoryFindPeer\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\"w\n\x11\x44irectoryResponse\x12+\n\tproviders\x18\x01 \x03(\x0b\x32\x18.agentmesh.v1.PeerRecord\x12&\n\x04peer\x18\x02 \x01(\x0b\x32\x18.agentmesh.v1.PeerRecord\x12\r\n\x05\x65rror\x18\x03 \x01(\t\",\n\nPeerRecord\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\r\n\x05\x61\x64\x64rs\x18\x02 \x03(\t\"\xc4\x01\n\nRouterInfo\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x11\n\taddresses\x18\x02 \x03(\t\x12\x34\n\x06labels\x18\x03 \x03(\x0b\x32$.agentmesh.v1.RouterInfo.LabelsEntry\x12\x13\n\x0b\x63onnections\x18\x04 \x01(\x05\x12\x18\n\x10\x63onnection_limit\x18\x05 \x01(\x05\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x83\x02\n\x12RouterLeaseRequest\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x11\n\taddresses\x18\x02 \x03(\t\x12\x0f\n\x07\x62iscuit\x18\x03 \x01(\x0c\x12\x17\n\x0f\x63onnected_peers\x18\x04 \x03(\t\x12\x10\n\x08\x64ht_size\x18\x05 \x01(\x05\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\x12&\n\x03ttl\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x13\n\x0b\x63onnections\x18\t \x01(\x05\x12\x18\n\x10\x63onnection_limit\x18\n \x01(\x05\"f\n\x13RouterLeaseResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xa4\x01\n\nPolicyRole\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x17\n\x0f\x61llowed_targets\x18\x02 \x03(\t\x12\x18\n\x10\x61llowed_services\x18\x03 \x03(\t\x12\x16\n\x0e\x63ustom_datalog\x18\x04 \x03(\t\x12\x16\n\x0e\x61llowed_labels\x18\x05 \x03(\t\x12%\n\x04http\x18\x06 \x03(\x0b\x32\x17.agentmesh.v1.HTTPGrant\"<\n\tHTTPGrant\x12\x0f\n\x07service\x18\x01 \x01(\t\x12\x0f\n\x07methods\x18\x02 \x03(\t\x12\r\n\x05paths\x18\x03 \x03(\t\"\xa1\x02\n\x11\x45gressDestination\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x12\n\ntarget_url\x18\x02 \x01(\t\x12\x12\n\ncredential\x18\x03 \x01(\t\x12\x11\n\tserved_by\x18\x04 \x03(\t\x12.\n\x06\x62roker\x18\x05 \x01(\x0b\x32\x1e.agentmesh.v1.CredentialBroker\x12,\n\ninspection\x18\x06 \x01(\x0b\x32\x18.agentmesh.v1.Inspection\x12&\n\x04mode\x18\x07 \x01(\x0e\x32\x18.agentmesh.v1.EgressMode\x12\r\n\x05ports\x18\x08 \x03(\r\x12\x15\n\rpreserve_host\x18\t \x01(\x08\x12\x17\n\x0f\x66orward_context\x18\n \x01(\x08\"9\n\nInspection\x12+\n\ninspectors\x18\x01 \x03(\x0b\x32\x17.agentmesh.v1.Inspector\"o\n\tInspector\x12/\n\x0bmodel_armor\x18\x01 \x01(\x0b\x32\x18.agentmesh.v1.ModelArmorH\x00\x12)\n\x08\x65xt_proc\x18\x02 \x01(\x0b\x32\x15.agentmesh.v1.ExtProcH\x00\x42\x06\n\x04kind\"\x91\x01\n\nModelArmor\x12\x10\n\x08template\x18\x01 \x01(\t\x12\x32\n\x08response\x18\x02 \x01(\x0e\x32 .agentmesh.v1.ResponseInspection\x12\x11\n\tfail_open\x18\x03 \x01(\x08\x12*\n\x07timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\"\x88\x02\n\x07\x45xtProc\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\n\n\x02\x63\x61\x18\x02 \x01(\t\x12\x1a\n\x12\x63lient_certificate\x18\x03 \x01(\t\x12<\n\x0fprocessing_mode\x18\x04 \x01(\x0b\x32#.agentmesh.v1.ExtProcProcessingMode\x12\x1b\n\x13\x61llow_mode_override\x18\x05 \x01(\x08\x12\x32\n\x0fmessage_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x1a\n\x12\x66\x61ilure_mode_allow\x18\x07 \x01(\x08\x12\x1a\n\x12max_buffered_bytes\x18\x08 \x01(\r\"\xff\x04\n\x15\x45xtProcProcessingMode\x12K\n\x13request_header_mode\x18\x01 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\x12L\n\x14response_header_mode\x18\x02 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\x12G\n\x11request_body_mode\x18\x03 \x01(\x0e\x32,.agentmesh.v1.ExtProcProcessingMode.BodyMode\x12H\n\x12response_body_mode\x18\x04 \x01(\x0e\x32,.agentmesh.v1.ExtProcProcessingMode.BodyMode\x12L\n\x14request_trailer_mode\x18\x05 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\x12M\n\x15response_trailer_mode\x18\x06 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\"9\n\nHeaderMode\x12\x17\n\x13HEADER_MODE_DEFAULT\x10\x00\x12\x08\n\x04SEND\x10\x01\x12\x08\n\x04SKIP\x10\x02\"`\n\x08\x42odyMode\x12\x08\n\x04NONE\x10\x00\x12\x0c\n\x08STREAMED\x10\x01\x12\x0c\n\x08\x42UFFERED\x10\x02\x12\x14\n\x10\x42UFFERED_PARTIAL\x10\x03\x12\x18\n\x14\x46ULL_DUPLEX_STREAMED\x10\x04\"\xe1\x01\n\x10\x43redentialBroker\x12\x17\n\rstatic_secret\x18\x01 \x01(\tH\x00\x12\x37\n\x0foidc_federation\x18\x02 \x01(\x0b\x32\x1c.agentmesh.v1.OIDCFederationH\x00\x12\x36\n\x0f\x61ws_assume_role\x18\x03 \x01(\x0b\x32\x1b.agentmesh.v1.AWSAssumeRoleH\x00\x12;\n\x11platform_identity\x18\x04 \x01(\x0b\x32\x1e.agentmesh.v1.PlatformIdentityH\x00\x42\x06\n\x04kind\"_\n\x0eOIDCFederation\x12\x16\n\x0etoken_endpoint\x18\x01 \x01(\t\x12\x10\n\x08\x61udience\x18\x02 \x01(\t\x12\x13\n\x0bimpersonate\x18\x03 \x01(\t\x12\x0e\n\x06scopes\x18\x04 \x03(\t\"9\n\rAWSAssumeRole\x12\x10\n\x08role_arn\x18\x01 \x01(\t\x12\x16\n\x0esession_policy\x18\x02 \x01(\t\"\"\n\x10PlatformIdentity\x12\x0e\n\x06scopes\x18\x01 \x03(\t\".\n\rPolicyBinding\x12\x0c\n\x04role\x18\x01 \x01(\t\x12\x0f\n\x07members\x18\x02 \x03(\t\"\x97\x01\n\x0cPolicyConfig\x12\'\n\x05roles\x18\x01 \x03(\x0b\x32\x18.agentmesh.v1.PolicyRole\x12-\n\x08\x62indings\x18\x02 \x03(\x0b\x32\x1b.agentmesh.v1.PolicyBinding\x12/\n\x06\x65gress\x18\x03 \x03(\x0b\x32\x1f.agentmesh.v1.EgressDestination\"\x18\n\x16PolicyConfigGetRequest\"0\n\x17PolicyConfigGetResponse\x12\x15\n\rdatalog_rules\x18\x01 \x03(\t\"<\n\x1aPolicyConfigUpdateResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x1a\n\x18\x45gressAssignmentsRequest\"L\n\x19\x45gressAssignmentsResponse\x12/\n\x06\x65gress\x18\x01 \x03(\x0b\x32\x1f.agentmesh.v1.EgressDestination\"f\n\x0cKeysResponse\x12\x13\n\x0bpublic_keys\x18\x01 \x03(\x0c\x12-\n\tsign_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x12\n\nsignatures\x18\x03 \x03(\x0c\"k\n\x13TokenRefreshRequest\x12\x1b\n\x13\x63hallenge_signature\x18\x01 \x01(\x0c\x12\x19\n\x11\x63hallenge_unix_ms\x18\x02 \x01(\x03\x12\x0f\n\x07peer_id\x18\x03 \x01(\t\x12\x0b\n\x03jwt\x18\x04 \x01(\t\"u\n\x14TokenRefreshResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x15\n\rerror_message\x18\x03 \x01(\t\"@\n\x11NodeCatalogReport\x12+\n\x08services\x18\x01 \x03(\x0b\x32\x19.agentmesh.v1.ServiceInfo\"%\n\x12TokenRevokeRequest\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\"5\n\x13TokenRevokeResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x96\x01\n\x1b\x42ootstrapTokenCreateRequest\x12\x0c\n\x04role\x18\x01 \x01(\t\x12\x10\n\x08owner_id\x18\x02 \x01(\t\x12\x11\n\tttl_hours\x18\x03 \x01(\x05\x12\x12\n\nmax_usages\x18\x04 \x01(\x05\x12\x13\n\x0b\x64\x65scription\x18\x05 \x01(\t\x12\x1b\n\x13\x61utonomous_recovery\x18\x06 \x01(\x08\"\x8a\x01\n\x1c\x42ootstrapTokenCreateResponse\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\t\x12\x0c\n\x04role\x18\x03 \x01(\t\x12\x10\n\x08owner_id\x18\x04 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xab\x02\n\x0e\x42ootstrapToken\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04role\x18\x02 \x01(\t\x12\x10\n\x08owner_id\x18\x03 \x01(\t\x12\x12\n\nmax_usages\x18\x04 \x01(\x05\x12\x14\n\x0cusages_count\x18\x05 \x01(\x05\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12/\n\x0b\x63reate_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0brevoke_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x1b\n\x13\x61utonomous_recovery\x18\n \x01(\x08\"J\n\x1a\x42ootstrapTokenListResponse\x12,\n\x06tokens\x18\x01 \x03(\x0b\x32\x1c.agentmesh.v1.BootstrapToken\"\xd6\x02\n\x11\x45nrollmentRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x10\n\x08token_id\x18\x03 \x01(\t\x12.\n\x06status\x18\x04 \x01(\x0e\x32\x1e.agentmesh.v1.EnrollmentStatus\x12;\n\x06labels\x18\x05 \x03(\x0b\x32+.agentmesh.v1.EnrollmentRequest.LabelsEntry\x12/\n\x0b\x63reate_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0cresolve_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x13\n\x0bresolved_by\x18\x08 \x01(\t\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"R\n\x1d\x45nrollmentRequestListResponse\x12\x31\n\x08requests\x18\x01 \x03(\x0b\x32\x1f.agentmesh.v1.EnrollmentRequest\"p\n\x04User\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0e\n\x06issuer\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12\x0c\n\x04role\x18\x04 \x01(\t\x12/\n\x0b\x63reate_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xe3\x02\n\x0c\x45nrolledNode\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0c\n\x04role\x18\x02 \x01(\t\x12\x17\n\x0f\x65nrollment_type\x18\x03 \x01(\t\x12\x13\n\x0b\x63laims_json\x18\x04 \x01(\t\x12\x10\n\x08owner_id\x18\x05 \x01(\t\x12\x36\n\x06labels\x18\x06 \x03(\x0b\x32&.agentmesh.v1.EnrolledNode.LabelsEntry\x12/\n\x0b\x65nroll_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06\x62\x61nned\x18\t \x01(\x08\x12\x1b\n\x13\x61utonomous_recovery\x18\n \x01(\x08\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc4\x01\n\x0bRouterLease\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x11\n\taddresses\x18\x02 \x03(\t\x12\x35\n\x11last_renewal_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x17\n\x0f\x63onnected_peers\x18\x05 \x03(\t\x12\x10\n\x08\x64ht_size\x18\x06 \x01(\x05\"l\n\x0cNodeServices\x12+\n\x08services\x18\x01 \x03(\x0b\x32\x19.agentmesh.v1.ServiceInfo\x12/\n\x0breport_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xdb\x03\n\x13\x41\x64minStatusResponse\x12!\n\x05users\x18\x01 \x03(\x0b\x32\x12.agentmesh.v1.User\x12\x31\n\x0e\x61\x63tive_routers\x18\x02 \x03(\x0b\x32\x19.agentmesh.v1.RouterLease\x12\x32\n\x0e\x65nrolled_nodes\x18\x03 \x03(\x0b\x32\x1a.agentmesh.v1.EnrolledNode\x12<\n\x13\x65nrollment_requests\x18\x04 \x03(\x0b\x32\x1f.agentmesh.v1.EnrollmentRequest\x12\x36\n\x10\x62ootstrap_tokens\x18\x05 \x03(\x0b\x32\x1c.agentmesh.v1.BootstrapToken\x12*\n\x06policy\x18\x06 \x01(\x0b\x32\x1a.agentmesh.v1.PolicyConfig\x12H\n\x0cnode_catalog\x18\x07 \x03(\x0b\x32\x32.agentmesh.v1.AdminStatusResponse.NodeCatalogEntry\x1aN\n\x10NodeCatalogEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.agentmesh.v1.NodeServices:\x02\x38\x01\"\x81\x02\n\x12UserStatusResponse\x12 \n\x04user\x18\x01 \x01(\x0b\x32\x12.agentmesh.v1.User\x12\x32\n\x0e\x65nrolled_nodes\x18\x02 \x03(\x0b\x32\x1a.agentmesh.v1.EnrolledNode\x12\x36\n\x10\x62ootstrap_tokens\x18\x03 \x03(\x0b\x32\x1c.agentmesh.v1.BootstrapToken\x12\x31\n\x0e\x61\x63tive_routers\x18\x04 \x03(\x0b\x32\x19.agentmesh.v1.RouterLease\x12*\n\x06policy\x18\x05 \x01(\x0b\x32\x1a.agentmesh.v1.PolicyConfig\"\xe4\x01\n\x18IdentityEvidenceResponse\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12\x37\n\x13\x62iscuit_expire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x19\n\x11\x63ontrol_plane_url\x18\x04 \x01(\t\x12\"\n\x1atrusted_control_plane_keys\x18\x05 \x03(\x0c\x12.\n\ncheck_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xc6\x02\n\x14PeerEvidenceResponse\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12\x15\n\rverifying_key\x18\x03 \x01(\x0c\x12\r\n\x05roles\x18\x04 \x03(\t\x12>\n\x06labels\x18\x05 \x03(\x0b\x32..agentmesh.v1.PeerEvidenceResponse.LabelsEntry\x12/\n\x0b\x65xpire_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0erevocation_ids\x18\x07 \x03(\t\x12.\n\ncheck_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8c\x02\n\x10MemberCredential\x12\x19\n\x11\x63ontrol_plane_url\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x35\n\x0ctrusted_keys\x18\x04 \x03(\x0b\x32\x1f.agentmesh.v1.TrustedSigningKey\x12\x19\n\x11issued_under_keys\x18\x05 \x03(\x0c\x12\x18\n\x10router_addresses\x18\x06 \x03(\t\x12/\n\x0coidc_session\x18\x07 \x01(\x0b\x32\x19.agentmesh.v1.OIDCSession\"Y\n\x11TrustedSigningKey\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\x12\x30\n\x0creceive_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"Y\n\x0bOIDCSession\x12\x0e\n\x06issuer\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x15\n\rrefresh_token\x18\x04 \x01(\t\"\x93\x01\n\x15TaskAuthorizationRule\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12%\n\x05rules\x18\x03 \x03(\x0b\x32\x16.agentmesh.v1.TaskRule\x12/\n\x0b\x65xpire_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x84\x01\n\x08TaskRule\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x18\n\x10\x61llowed_services\x18\x02 \x03(\t\x12.\n\toperation\x18\x03 \x01(\x0b\x32\x1b.agentmesh.v1.TaskOperation\x12\x19\n\x11\x61llowed_resources\x18\x04 \x03(\t\"s\n\rTaskOperation\x12\x15\n\rallowed_tools\x18\x01 \x03(\t\x12\x17\n\x0f\x61llowed_methods\x18\x02 \x03(\t\x12\x15\n\rallowed_paths\x18\x03 \x03(\t\x12\x1b\n\x13\x61llowed_permissions\x18\x04 \x03(\t\"\xab\x01\n\x14TokenExchangeRequest\x12\x15\n\rsubject_token\x18\x01 \x01(\t\x12\x36\n\ttask_rule\x18\x02 \x01(\x0b\x32#.agentmesh.v1.TaskAuthorizationRule\x12\x0c\n\x04seal\x18\x03 \x01(\x08\x12\x19\n\x11\x63hallenge_unix_ms\x18\x04 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x05 \x01(\x0c\"\x7f\n\x15TokenExchangeResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\r\n\x05roles\x18\x03 \x03(\t\x12\x0f\n\x07subject\x18\x04 \x01(\t\"\x81\x01\n\x0fSTSTokenRequest\x12\x0f\n\x07\x62iscuit\x18\x01 \x01(\x0c\x12\x13\n\x0b\x64\x65stination\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x19\n\x11\x63hallenge_unix_ms\x18\x04 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x05 \x01(\x0c\"\x83\x01\n\x10STSTokenResponse\x12\x0b\n\x03jwt\x18\x01 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0f\n\x07subject\x18\x03 \x01(\t\x12\r\n\x05roles\x18\x04 \x03(\t\x12\x11\n\ttask_name\x18\x05 \x01(\t\"F\n\x13RevocationsResponse\x12\x16\n\x0erevocation_ids\x18\x01 \x03(\t\x12\x17\n\x0f\x62\x61nned_peer_ids\x18\x02 \x03(\t*\x94\x01\n\x10\x45nrollmentStatus\x12!\n\x1d\x45NROLLMENT_STATUS_UNSPECIFIED\x10\x00\x12\x1d\n\x19\x45NROLLMENT_STATUS_PENDING\x10\x01\x12\x1e\n\x1a\x45NROLLMENT_STATUS_APPROVED\x10\x02\x12\x1e\n\x1a\x45NROLLMENT_STATUS_REJECTED\x10\x03*\x8c\x01\n\x0bServiceType\x12\x1c\n\x18SERVICE_TYPE_UNSPECIFIED\x10\x00\x12\x14\n\x10SERVICE_TYPE_MCP\x10\x01\x12\x1a\n\x16SERVICE_TYPE_INFERENCE\x10\x02\x12\x14\n\x10SERVICE_TYPE_A2A\x10\x03\x12\x17\n\x13SERVICE_TYPE_EGRESS\x10\x04*7\n\nEgressMode\x12\x14\n\x10\x45GRESS_MODE_HTTP\x10\x00\x12\x13\n\x0f\x45GRESS_MODE_TCP\x10\x01*\\\n\x12ResponseInspection\x12 \n\x1cRESPONSE_INSPECTION_BUFFERED\x10\x00\x12$\n RESPONSE_INSPECTION_REQUEST_ONLY\x10\x01\x42!Z\x1fgithub.com/google/agentmesh/apib\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0f\x61gentmesh.proto\x12\x0c\x61gentmesh.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"4\n\tAuthFrame\x12\x0f\n\x07\x62iscuit\x18\x01 \x01(\x0c\x12\x16\n\x0etarget_service\x18\x02 \x01(\t\"?\n\x0c\x41uthResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x03 \x01(\x0c\"\xb1\x01\n\x0cRouterGoAway\x12\x31\n\x06reason\x18\x01 \x01(\x0e\x32!.agentmesh.v1.RouterGoAway.Reason\x12.\n\x0bretry_after\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\">\n\x06Reason\x12\x16\n\x12REASON_UNSPECIFIED\x10\x00\x12\x0c\n\x08\x44RAINING\x10\x01\x12\x0e\n\nOVERLOADED\x10\x02\"\xdc\x01\n\tMeshEvent\x12*\n\x04type\x18\x01 \x01(\x0e\x32\x1c.agentmesh.v1.MeshEvent.Type\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12.\n\nevent_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0enew_public_key\x18\x04 \x01(\x0c\x12\x11\n\tsignature\x18\x05 \x01(\x0c\"7\n\x04Type\x12\n\n\x06\x42\x41NNED\x10\x00\x12\x10\n\x0cKEY_ROTATION\x10\x01\x12\x11\n\rPOLICY_UPDATE\x10\x02\"\xf9\x01\n\rEnrollRequest\x12\x0b\n\x03jwt\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x12\n\npublic_key\x18\x03 \x01(\x0c\x12\x16\n\x0erequested_role\x18\x04 \x01(\t\x12\x37\n\x06labels\x18\x05 \x03(\x0b\x32\'.agentmesh.v1.EnrollRequest.LabelsEntry\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xab\x01\n\x0e\x45nrollResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12 \n\x18\x63ontrol_plane_public_key\x18\x03 \x01(\x0c\x12\x18\n\x10router_addresses\x18\x04 \x03(\t\x12/\n\x0b\x65xpire_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x97\x02\n\x16\x42ootstrapEnrollRequest\x12\x17\n\x0f\x62ootstrap_token\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x12\n\npublic_key\x18\x03 \x01(\x0c\x12\x16\n\x0erequested_role\x18\x04 \x01(\t\x12@\n\x06labels\x18\x05 \x03(\x0b\x32\x30.agentmesh.v1.BootstrapEnrollRequest.LabelsEntry\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x83\x02\n\x17\x42ootstrapEnrollResponse\x12.\n\x06status\x18\x01 \x01(\x0e\x32\x1e.agentmesh.v1.EnrollmentStatus\x12\x15\n\rbiscuit_token\x18\x02 \x01(\x0c\x12\x1d\n\x15poll_interval_seconds\x18\x03 \x01(\x05\x12\x15\n\rerror_message\x18\x04 \x01(\t\x12 \n\x18\x63ontrol_plane_public_key\x18\x05 \x01(\x0c\x12\x18\n\x10router_addresses\x18\x06 \x03(\t\x12/\n\x0b\x65xpire_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"Y\n\x0bServiceInfo\x12\'\n\x04type\x18\x01 \x01(\x0e\x32\x19.agentmesh.v1.ServiceType\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\"\x81\x01\n\x0e\x43ommandBackend\x12\x0f\n\x07\x63ommand\x18\x01 \x03(\t\x12\x32\n\x03\x65nv\x18\x02 \x03(\x0b\x32%.agentmesh.v1.CommandBackend.EnvEntry\x1a*\n\x08\x45nvEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x96\x01\n\x16RegisterServiceRequest\x12*\n\x07service\x18\x01 \x01(\x0b\x32\x19.agentmesh.v1.ServiceInfo\x12\x14\n\ntarget_url\x18\x02 \x01(\tH\x00\x12/\n\x07\x63ommand\x18\x03 \x01(\x0b\x32\x1c.agentmesh.v1.CommandBackendH\x00\x42\t\n\x07\x62\x61\x63kend\"i\n\x12\x44iscoveredProvider\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x17\n\x0flocal_proxy_url\x18\x02 \x01(\t\x12\x10\n\x08srv_name\x18\x03 \x01(\t\x12\x17\n\x0fsrv_description\x18\x04 \x01(\t\"\xe8\x01\n\x18\x43ontrolPlaneInfoResponse\x12\x13\n\x0boidc_issuer\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x18\n\x10router_addresses\x18\x04 \x03(\t\x12\x17\n\x0f\x62\x61nned_peer_ids\x18\x05 \x03(\t\x12)\n\x07routers\x18\x06 \x03(\x0b\x32\x18.agentmesh.v1.RouterInfo\x12\x34\n\tdirectory\x18\x07 \x01(\x0b\x32!.agentmesh.v1.DirectoryParameters\"\x90\x01\n\x13\x44irectoryParameters\x12\x13\n\x0breplication\x18\x01 \x01(\x05\x12\x35\n\x12reprovide_interval\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\x12-\n\nrecord_ttl\x18\x03 \x01(\x0b\x32\x19.google.protobuf.Duration\"\xfb\x01\n\x10\x44irectoryRequest\x12\x31\n\x07provide\x18\x01 \x01(\x0b\x32\x1e.agentmesh.v1.DirectoryProvideH\x00\x12>\n\x0e\x66ind_providers\x18\x02 \x01(\x0b\x32$.agentmesh.v1.DirectoryFindProvidersH\x00\x12\x34\n\tfind_peer\x18\x03 \x01(\x0b\x32\x1f.agentmesh.v1.DirectoryFindPeerH\x00\x12\x33\n\x08withdraw\x18\x04 \x01(\x0b\x32\x1f.agentmesh.v1.DirectoryWithdrawH\x00\x42\t\n\x07request\"@\n\x10\x44irectoryProvide\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\r\n\x05\x61\x64\x64rs\x18\x02 \x03(\t\x12\x10\n\x08provider\x18\x03 \x01(\t\"2\n\x11\x44irectoryWithdraw\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\x10\n\x08provider\x18\x02 \x01(\t\"4\n\x16\x44irectoryFindProviders\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\r\n\x05limit\x18\x02 \x01(\r\"$\n\x11\x44irectoryFindPeer\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\"w\n\x11\x44irectoryResponse\x12+\n\tproviders\x18\x01 \x03(\x0b\x32\x18.agentmesh.v1.PeerRecord\x12&\n\x04peer\x18\x02 \x01(\x0b\x32\x18.agentmesh.v1.PeerRecord\x12\r\n\x05\x65rror\x18\x03 \x01(\t\",\n\nPeerRecord\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\r\n\x05\x61\x64\x64rs\x18\x02 \x03(\t\"\xc4\x01\n\nRouterInfo\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x11\n\taddresses\x18\x02 \x03(\t\x12\x34\n\x06labels\x18\x03 \x03(\x0b\x32$.agentmesh.v1.RouterInfo.LabelsEntry\x12\x13\n\x0b\x63onnections\x18\x04 \x01(\x05\x12\x18\n\x10\x63onnection_limit\x18\x05 \x01(\x05\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x83\x02\n\x12RouterLeaseRequest\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x11\n\taddresses\x18\x02 \x03(\t\x12\x0f\n\x07\x62iscuit\x18\x03 \x01(\x0c\x12\x17\n\x0f\x63onnected_peers\x18\x04 \x03(\t\x12\x10\n\x08\x64ht_size\x18\x05 \x01(\x05\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\x12&\n\x03ttl\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x13\n\x0b\x63onnections\x18\t \x01(\x05\x12\x18\n\x10\x63onnection_limit\x18\n \x01(\x05\"f\n\x13RouterLeaseResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xa4\x01\n\nPolicyRole\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x17\n\x0f\x61llowed_targets\x18\x02 \x03(\t\x12\x18\n\x10\x61llowed_services\x18\x03 \x03(\t\x12\x16\n\x0e\x63ustom_datalog\x18\x04 \x03(\t\x12\x16\n\x0e\x61llowed_labels\x18\x05 \x03(\t\x12%\n\x04http\x18\x06 \x03(\x0b\x32\x17.agentmesh.v1.HTTPGrant\"<\n\tHTTPGrant\x12\x0f\n\x07service\x18\x01 \x01(\t\x12\x0f\n\x07methods\x18\x02 \x03(\t\x12\r\n\x05paths\x18\x03 \x03(\t\"\xa1\x02\n\x11\x45gressDestination\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x12\n\ntarget_url\x18\x02 \x01(\t\x12\x12\n\ncredential\x18\x03 \x01(\t\x12\x11\n\tserved_by\x18\x04 \x03(\t\x12.\n\x06\x62roker\x18\x05 \x01(\x0b\x32\x1e.agentmesh.v1.CredentialBroker\x12,\n\ninspection\x18\x06 \x01(\x0b\x32\x18.agentmesh.v1.Inspection\x12&\n\x04mode\x18\x07 \x01(\x0e\x32\x18.agentmesh.v1.EgressMode\x12\r\n\x05ports\x18\x08 \x03(\r\x12\x15\n\rpreserve_host\x18\t \x01(\x08\x12\x17\n\x0f\x66orward_context\x18\n \x01(\x08\"9\n\nInspection\x12+\n\ninspectors\x18\x01 \x03(\x0b\x32\x17.agentmesh.v1.Inspector\"o\n\tInspector\x12/\n\x0bmodel_armor\x18\x01 \x01(\x0b\x32\x18.agentmesh.v1.ModelArmorH\x00\x12)\n\x08\x65xt_proc\x18\x02 \x01(\x0b\x32\x15.agentmesh.v1.ExtProcH\x00\x42\x06\n\x04kind\"\x91\x01\n\nModelArmor\x12\x10\n\x08template\x18\x01 \x01(\t\x12\x32\n\x08response\x18\x02 \x01(\x0e\x32 .agentmesh.v1.ResponseInspection\x12\x11\n\tfail_open\x18\x03 \x01(\x08\x12*\n\x07timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\"\x88\x02\n\x07\x45xtProc\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\n\n\x02\x63\x61\x18\x02 \x01(\t\x12\x1a\n\x12\x63lient_certificate\x18\x03 \x01(\t\x12<\n\x0fprocessing_mode\x18\x04 \x01(\x0b\x32#.agentmesh.v1.ExtProcProcessingMode\x12\x1b\n\x13\x61llow_mode_override\x18\x05 \x01(\x08\x12\x32\n\x0fmessage_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x1a\n\x12\x66\x61ilure_mode_allow\x18\x07 \x01(\x08\x12\x1a\n\x12max_buffered_bytes\x18\x08 \x01(\r\"\xff\x04\n\x15\x45xtProcProcessingMode\x12K\n\x13request_header_mode\x18\x01 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\x12L\n\x14response_header_mode\x18\x02 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\x12G\n\x11request_body_mode\x18\x03 \x01(\x0e\x32,.agentmesh.v1.ExtProcProcessingMode.BodyMode\x12H\n\x12response_body_mode\x18\x04 \x01(\x0e\x32,.agentmesh.v1.ExtProcProcessingMode.BodyMode\x12L\n\x14request_trailer_mode\x18\x05 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\x12M\n\x15response_trailer_mode\x18\x06 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\"9\n\nHeaderMode\x12\x17\n\x13HEADER_MODE_DEFAULT\x10\x00\x12\x08\n\x04SEND\x10\x01\x12\x08\n\x04SKIP\x10\x02\"`\n\x08\x42odyMode\x12\x08\n\x04NONE\x10\x00\x12\x0c\n\x08STREAMED\x10\x01\x12\x0c\n\x08\x42UFFERED\x10\x02\x12\x14\n\x10\x42UFFERED_PARTIAL\x10\x03\x12\x18\n\x14\x46ULL_DUPLEX_STREAMED\x10\x04\"\xe1\x01\n\x10\x43redentialBroker\x12\x17\n\rstatic_secret\x18\x01 \x01(\tH\x00\x12\x37\n\x0foidc_federation\x18\x02 \x01(\x0b\x32\x1c.agentmesh.v1.OIDCFederationH\x00\x12\x36\n\x0f\x61ws_assume_role\x18\x03 \x01(\x0b\x32\x1b.agentmesh.v1.AWSAssumeRoleH\x00\x12;\n\x11platform_identity\x18\x04 \x01(\x0b\x32\x1e.agentmesh.v1.PlatformIdentityH\x00\x42\x06\n\x04kind\"_\n\x0eOIDCFederation\x12\x16\n\x0etoken_endpoint\x18\x01 \x01(\t\x12\x10\n\x08\x61udience\x18\x02 \x01(\t\x12\x13\n\x0bimpersonate\x18\x03 \x01(\t\x12\x0e\n\x06scopes\x18\x04 \x03(\t\"9\n\rAWSAssumeRole\x12\x10\n\x08role_arn\x18\x01 \x01(\t\x12\x16\n\x0esession_policy\x18\x02 \x01(\t\"\"\n\x10PlatformIdentity\x12\x0e\n\x06scopes\x18\x01 \x03(\t\".\n\rPolicyBinding\x12\x0c\n\x04role\x18\x01 \x01(\t\x12\x0f\n\x07members\x18\x02 \x03(\t\"\x97\x01\n\x0cPolicyConfig\x12\'\n\x05roles\x18\x01 \x03(\x0b\x32\x18.agentmesh.v1.PolicyRole\x12-\n\x08\x62indings\x18\x02 \x03(\x0b\x32\x1b.agentmesh.v1.PolicyBinding\x12/\n\x06\x65gress\x18\x03 \x03(\x0b\x32\x1f.agentmesh.v1.EgressDestination\"\x18\n\x16PolicyConfigGetRequest\"0\n\x17PolicyConfigGetResponse\x12\x15\n\rdatalog_rules\x18\x01 \x03(\t\"<\n\x1aPolicyConfigUpdateResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x1a\n\x18\x45gressAssignmentsRequest\"L\n\x19\x45gressAssignmentsResponse\x12/\n\x06\x65gress\x18\x01 \x03(\x0b\x32\x1f.agentmesh.v1.EgressDestination\"f\n\x0cKeysResponse\x12\x13\n\x0bpublic_keys\x18\x01 \x03(\x0c\x12-\n\tsign_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x12\n\nsignatures\x18\x03 \x03(\x0c\"k\n\x13TokenRefreshRequest\x12\x1b\n\x13\x63hallenge_signature\x18\x01 \x01(\x0c\x12\x19\n\x11\x63hallenge_unix_ms\x18\x02 \x01(\x03\x12\x0f\n\x07peer_id\x18\x03 \x01(\t\x12\x0b\n\x03jwt\x18\x04 \x01(\t\"u\n\x14TokenRefreshResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x15\n\rerror_message\x18\x03 \x01(\t\"@\n\x11NodeCatalogReport\x12+\n\x08services\x18\x01 \x03(\x0b\x32\x19.agentmesh.v1.ServiceInfo\"%\n\x12TokenRevokeRequest\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\"5\n\x13TokenRevokeResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x96\x01\n\x1b\x42ootstrapTokenCreateRequest\x12\x0c\n\x04role\x18\x01 \x01(\t\x12\x10\n\x08owner_id\x18\x02 \x01(\t\x12\x11\n\tttl_hours\x18\x03 \x01(\x05\x12\x12\n\nmax_usages\x18\x04 \x01(\x05\x12\x13\n\x0b\x64\x65scription\x18\x05 \x01(\t\x12\x1b\n\x13\x61utonomous_recovery\x18\x06 \x01(\x08\"\x8a\x01\n\x1c\x42ootstrapTokenCreateResponse\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\t\x12\x0c\n\x04role\x18\x03 \x01(\t\x12\x10\n\x08owner_id\x18\x04 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xab\x02\n\x0e\x42ootstrapToken\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04role\x18\x02 \x01(\t\x12\x10\n\x08owner_id\x18\x03 \x01(\t\x12\x12\n\nmax_usages\x18\x04 \x01(\x05\x12\x14\n\x0cusages_count\x18\x05 \x01(\x05\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12/\n\x0b\x63reate_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0brevoke_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x1b\n\x13\x61utonomous_recovery\x18\n \x01(\x08\"J\n\x1a\x42ootstrapTokenListResponse\x12,\n\x06tokens\x18\x01 \x03(\x0b\x32\x1c.agentmesh.v1.BootstrapToken\"\xd6\x02\n\x11\x45nrollmentRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x10\n\x08token_id\x18\x03 \x01(\t\x12.\n\x06status\x18\x04 \x01(\x0e\x32\x1e.agentmesh.v1.EnrollmentStatus\x12;\n\x06labels\x18\x05 \x03(\x0b\x32+.agentmesh.v1.EnrollmentRequest.LabelsEntry\x12/\n\x0b\x63reate_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0cresolve_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x13\n\x0bresolved_by\x18\x08 \x01(\t\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"R\n\x1d\x45nrollmentRequestListResponse\x12\x31\n\x08requests\x18\x01 \x03(\x0b\x32\x1f.agentmesh.v1.EnrollmentRequest\"p\n\x04User\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0e\n\x06issuer\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12\x0c\n\x04role\x18\x04 \x01(\t\x12/\n\x0b\x63reate_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xe3\x02\n\x0c\x45nrolledNode\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0c\n\x04role\x18\x02 \x01(\t\x12\x17\n\x0f\x65nrollment_type\x18\x03 \x01(\t\x12\x13\n\x0b\x63laims_json\x18\x04 \x01(\t\x12\x10\n\x08owner_id\x18\x05 \x01(\t\x12\x36\n\x06labels\x18\x06 \x03(\x0b\x32&.agentmesh.v1.EnrolledNode.LabelsEntry\x12/\n\x0b\x65nroll_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06\x62\x61nned\x18\t \x01(\x08\x12\x1b\n\x13\x61utonomous_recovery\x18\n \x01(\x08\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc4\x01\n\x0bRouterLease\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x11\n\taddresses\x18\x02 \x03(\t\x12\x35\n\x11last_renewal_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x17\n\x0f\x63onnected_peers\x18\x05 \x03(\t\x12\x10\n\x08\x64ht_size\x18\x06 \x01(\x05\"l\n\x0cNodeServices\x12+\n\x08services\x18\x01 \x03(\x0b\x32\x19.agentmesh.v1.ServiceInfo\x12/\n\x0breport_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xdb\x03\n\x13\x41\x64minStatusResponse\x12!\n\x05users\x18\x01 \x03(\x0b\x32\x12.agentmesh.v1.User\x12\x31\n\x0e\x61\x63tive_routers\x18\x02 \x03(\x0b\x32\x19.agentmesh.v1.RouterLease\x12\x32\n\x0e\x65nrolled_nodes\x18\x03 \x03(\x0b\x32\x1a.agentmesh.v1.EnrolledNode\x12<\n\x13\x65nrollment_requests\x18\x04 \x03(\x0b\x32\x1f.agentmesh.v1.EnrollmentRequest\x12\x36\n\x10\x62ootstrap_tokens\x18\x05 \x03(\x0b\x32\x1c.agentmesh.v1.BootstrapToken\x12*\n\x06policy\x18\x06 \x01(\x0b\x32\x1a.agentmesh.v1.PolicyConfig\x12H\n\x0cnode_catalog\x18\x07 \x03(\x0b\x32\x32.agentmesh.v1.AdminStatusResponse.NodeCatalogEntry\x1aN\n\x10NodeCatalogEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.agentmesh.v1.NodeServices:\x02\x38\x01\"\x81\x02\n\x12UserStatusResponse\x12 \n\x04user\x18\x01 \x01(\x0b\x32\x12.agentmesh.v1.User\x12\x32\n\x0e\x65nrolled_nodes\x18\x02 \x03(\x0b\x32\x1a.agentmesh.v1.EnrolledNode\x12\x36\n\x10\x62ootstrap_tokens\x18\x03 \x03(\x0b\x32\x1c.agentmesh.v1.BootstrapToken\x12\x31\n\x0e\x61\x63tive_routers\x18\x04 \x03(\x0b\x32\x19.agentmesh.v1.RouterLease\x12*\n\x06policy\x18\x05 \x01(\x0b\x32\x1a.agentmesh.v1.PolicyConfig\"\xe4\x01\n\x18IdentityEvidenceResponse\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12\x37\n\x13\x62iscuit_expire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x19\n\x11\x63ontrol_plane_url\x18\x04 \x01(\t\x12\"\n\x1atrusted_control_plane_keys\x18\x05 \x03(\x0c\x12.\n\ncheck_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xc6\x02\n\x14PeerEvidenceResponse\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12\x15\n\rverifying_key\x18\x03 \x01(\x0c\x12\r\n\x05roles\x18\x04 \x03(\t\x12>\n\x06labels\x18\x05 \x03(\x0b\x32..agentmesh.v1.PeerEvidenceResponse.LabelsEntry\x12/\n\x0b\x65xpire_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0erevocation_ids\x18\x07 \x03(\t\x12.\n\ncheck_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8c\x02\n\x10MemberCredential\x12\x19\n\x11\x63ontrol_plane_url\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x35\n\x0ctrusted_keys\x18\x04 \x03(\x0b\x32\x1f.agentmesh.v1.TrustedSigningKey\x12\x19\n\x11issued_under_keys\x18\x05 \x03(\x0c\x12\x18\n\x10router_addresses\x18\x06 \x03(\t\x12/\n\x0coidc_session\x18\x07 \x01(\x0b\x32\x19.agentmesh.v1.OIDCSession\"Y\n\x11TrustedSigningKey\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\x12\x30\n\x0creceive_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"Y\n\x0bOIDCSession\x12\x0e\n\x06issuer\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x15\n\rrefresh_token\x18\x04 \x01(\t\"\x93\x01\n\x15TaskAuthorizationRule\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12%\n\x05rules\x18\x03 \x03(\x0b\x32\x16.agentmesh.v1.TaskRule\x12/\n\x0b\x65xpire_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x84\x01\n\x08TaskRule\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x18\n\x10\x61llowed_services\x18\x02 \x03(\t\x12.\n\toperation\x18\x03 \x01(\x0b\x32\x1b.agentmesh.v1.TaskOperation\x12\x19\n\x11\x61llowed_resources\x18\x04 \x03(\t\"s\n\rTaskOperation\x12\x15\n\rallowed_tools\x18\x01 \x03(\t\x12\x17\n\x0f\x61llowed_methods\x18\x02 \x03(\t\x12\x15\n\rallowed_paths\x18\x03 \x03(\t\x12\x1b\n\x13\x61llowed_permissions\x18\x04 \x03(\t\"\xab\x01\n\x14TokenExchangeRequest\x12\x15\n\rsubject_token\x18\x01 \x01(\t\x12\x36\n\ttask_rule\x18\x02 \x01(\x0b\x32#.agentmesh.v1.TaskAuthorizationRule\x12\x0c\n\x04seal\x18\x03 \x01(\x08\x12\x19\n\x11\x63hallenge_unix_ms\x18\x04 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x05 \x01(\x0c\"\x7f\n\x15TokenExchangeResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\r\n\x05roles\x18\x03 \x03(\t\x12\x0f\n\x07subject\x18\x04 \x01(\t\"\x81\x01\n\x0fSTSTokenRequest\x12\x0f\n\x07\x62iscuit\x18\x01 \x01(\x0c\x12\x13\n\x0b\x64\x65stination\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x19\n\x11\x63hallenge_unix_ms\x18\x04 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x05 \x01(\x0c\"\x83\x01\n\x10STSTokenResponse\x12\x0b\n\x03jwt\x18\x01 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0f\n\x07subject\x18\x03 \x01(\t\x12\r\n\x05roles\x18\x04 \x03(\t\x12\x11\n\ttask_name\x18\x05 \x01(\t\"F\n\x13RevocationsResponse\x12\x16\n\x0erevocation_ids\x18\x01 \x03(\t\x12\x17\n\x0f\x62\x61nned_peer_ids\x18\x02 \x03(\t*\x94\x01\n\x10\x45nrollmentStatus\x12!\n\x1d\x45NROLLMENT_STATUS_UNSPECIFIED\x10\x00\x12\x1d\n\x19\x45NROLLMENT_STATUS_PENDING\x10\x01\x12\x1e\n\x1a\x45NROLLMENT_STATUS_APPROVED\x10\x02\x12\x1e\n\x1a\x45NROLLMENT_STATUS_REJECTED\x10\x03*\x8c\x01\n\x0bServiceType\x12\x1c\n\x18SERVICE_TYPE_UNSPECIFIED\x10\x00\x12\x14\n\x10SERVICE_TYPE_MCP\x10\x01\x12\x1a\n\x16SERVICE_TYPE_INFERENCE\x10\x02\x12\x14\n\x10SERVICE_TYPE_A2A\x10\x03\x12\x17\n\x13SERVICE_TYPE_EGRESS\x10\x04*7\n\nEgressMode\x12\x14\n\x10\x45GRESS_MODE_HTTP\x10\x00\x12\x13\n\x0f\x45GRESS_MODE_TCP\x10\x01*\\\n\x12ResponseInspection\x12 \n\x1cRESPONSE_INSPECTION_BUFFERED\x10\x00\x12$\n RESPONSE_INSPECTION_REQUEST_ONLY\x10\x01\x42!Z\x1fgithub.com/google/agentmesh/apib\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'agentmesh_pb2', globals()) @@ -39,14 +39,14 @@ _ADMINSTATUSRESPONSE_NODECATALOGENTRY._serialized_options = b'8\001' _PEEREVIDENCERESPONSE_LABELSENTRY._options = None _PEEREVIDENCERESPONSE_LABELSENTRY._serialized_options = b'8\001' - _ENROLLMENTSTATUS._serialized_start=11331 - _ENROLLMENTSTATUS._serialized_end=11479 - _SERVICETYPE._serialized_start=11482 - _SERVICETYPE._serialized_end=11622 - _EGRESSMODE._serialized_start=11624 - _EGRESSMODE._serialized_end=11679 - _RESPONSEINSPECTION._serialized_start=11681 - _RESPONSEINSPECTION._serialized_end=11773 + _ENROLLMENTSTATUS._serialized_start=11436 + _ENROLLMENTSTATUS._serialized_end=11584 + _SERVICETYPE._serialized_start=11587 + _SERVICETYPE._serialized_end=11727 + _EGRESSMODE._serialized_start=11729 + _EGRESSMODE._serialized_end=11784 + _RESPONSEINSPECTION._serialized_start=11786 + _RESPONSEINSPECTION._serialized_end=11878 _AUTHFRAME._serialized_start=98 _AUTHFRAME._serialized_end=150 _AUTHRESPONSE._serialized_start=152 @@ -86,135 +86,137 @@ _DIRECTORYPARAMETERS._serialized_start=2309 _DIRECTORYPARAMETERS._serialized_end=2453 _DIRECTORYREQUEST._serialized_start=2456 - _DIRECTORYREQUEST._serialized_end=2654 - _DIRECTORYPROVIDE._serialized_start=2656 - _DIRECTORYPROVIDE._serialized_end=2720 - _DIRECTORYFINDPROVIDERS._serialized_start=2722 - _DIRECTORYFINDPROVIDERS._serialized_end=2774 - _DIRECTORYFINDPEER._serialized_start=2776 - _DIRECTORYFINDPEER._serialized_end=2812 - _DIRECTORYRESPONSE._serialized_start=2814 - _DIRECTORYRESPONSE._serialized_end=2933 - _PEERRECORD._serialized_start=2935 - _PEERRECORD._serialized_end=2979 - _ROUTERINFO._serialized_start=2982 - _ROUTERINFO._serialized_end=3178 + _DIRECTORYREQUEST._serialized_end=2707 + _DIRECTORYPROVIDE._serialized_start=2709 + _DIRECTORYPROVIDE._serialized_end=2773 + _DIRECTORYWITHDRAW._serialized_start=2775 + _DIRECTORYWITHDRAW._serialized_end=2825 + _DIRECTORYFINDPROVIDERS._serialized_start=2827 + _DIRECTORYFINDPROVIDERS._serialized_end=2879 + _DIRECTORYFINDPEER._serialized_start=2881 + _DIRECTORYFINDPEER._serialized_end=2917 + _DIRECTORYRESPONSE._serialized_start=2919 + _DIRECTORYRESPONSE._serialized_end=3038 + _PEERRECORD._serialized_start=3040 + _PEERRECORD._serialized_end=3084 + _ROUTERINFO._serialized_start=3087 + _ROUTERINFO._serialized_end=3283 _ROUTERINFO_LABELSENTRY._serialized_start=825 _ROUTERINFO_LABELSENTRY._serialized_end=870 - _ROUTERLEASEREQUEST._serialized_start=3181 - _ROUTERLEASEREQUEST._serialized_end=3440 - _ROUTERLEASERESPONSE._serialized_start=3442 - _ROUTERLEASERESPONSE._serialized_end=3544 - _POLICYROLE._serialized_start=3547 - _POLICYROLE._serialized_end=3711 - _HTTPGRANT._serialized_start=3713 - _HTTPGRANT._serialized_end=3773 - _EGRESSDESTINATION._serialized_start=3776 - _EGRESSDESTINATION._serialized_end=4065 - _INSPECTION._serialized_start=4067 - _INSPECTION._serialized_end=4124 - _INSPECTOR._serialized_start=4126 - _INSPECTOR._serialized_end=4237 - _MODELARMOR._serialized_start=4240 - _MODELARMOR._serialized_end=4385 - _EXTPROC._serialized_start=4388 - _EXTPROC._serialized_end=4652 - _EXTPROCPROCESSINGMODE._serialized_start=4655 - _EXTPROCPROCESSINGMODE._serialized_end=5294 - _EXTPROCPROCESSINGMODE_HEADERMODE._serialized_start=5139 - _EXTPROCPROCESSINGMODE_HEADERMODE._serialized_end=5196 - _EXTPROCPROCESSINGMODE_BODYMODE._serialized_start=5198 - _EXTPROCPROCESSINGMODE_BODYMODE._serialized_end=5294 - _CREDENTIALBROKER._serialized_start=5297 - _CREDENTIALBROKER._serialized_end=5522 - _OIDCFEDERATION._serialized_start=5524 - _OIDCFEDERATION._serialized_end=5619 - _AWSASSUMEROLE._serialized_start=5621 - _AWSASSUMEROLE._serialized_end=5678 - _PLATFORMIDENTITY._serialized_start=5680 - _PLATFORMIDENTITY._serialized_end=5714 - _POLICYBINDING._serialized_start=5716 - _POLICYBINDING._serialized_end=5762 - _POLICYCONFIG._serialized_start=5765 - _POLICYCONFIG._serialized_end=5916 - _POLICYCONFIGGETREQUEST._serialized_start=5918 - _POLICYCONFIGGETREQUEST._serialized_end=5942 - _POLICYCONFIGGETRESPONSE._serialized_start=5944 - _POLICYCONFIGGETRESPONSE._serialized_end=5992 - _POLICYCONFIGUPDATERESPONSE._serialized_start=5994 - _POLICYCONFIGUPDATERESPONSE._serialized_end=6054 - _EGRESSASSIGNMENTSREQUEST._serialized_start=6056 - _EGRESSASSIGNMENTSREQUEST._serialized_end=6082 - _EGRESSASSIGNMENTSRESPONSE._serialized_start=6084 - _EGRESSASSIGNMENTSRESPONSE._serialized_end=6160 - _KEYSRESPONSE._serialized_start=6162 - _KEYSRESPONSE._serialized_end=6264 - _TOKENREFRESHREQUEST._serialized_start=6266 - _TOKENREFRESHREQUEST._serialized_end=6373 - _TOKENREFRESHRESPONSE._serialized_start=6375 - _TOKENREFRESHRESPONSE._serialized_end=6492 - _NODECATALOGREPORT._serialized_start=6494 - _NODECATALOGREPORT._serialized_end=6558 - _TOKENREVOKEREQUEST._serialized_start=6560 - _TOKENREVOKEREQUEST._serialized_end=6597 - _TOKENREVOKERESPONSE._serialized_start=6599 - _TOKENREVOKERESPONSE._serialized_end=6652 - _BOOTSTRAPTOKENCREATEREQUEST._serialized_start=6655 - _BOOTSTRAPTOKENCREATEREQUEST._serialized_end=6805 - _BOOTSTRAPTOKENCREATERESPONSE._serialized_start=6808 - _BOOTSTRAPTOKENCREATERESPONSE._serialized_end=6946 - _BOOTSTRAPTOKEN._serialized_start=6949 - _BOOTSTRAPTOKEN._serialized_end=7248 - _BOOTSTRAPTOKENLISTRESPONSE._serialized_start=7250 - _BOOTSTRAPTOKENLISTRESPONSE._serialized_end=7324 - _ENROLLMENTREQUEST._serialized_start=7327 - _ENROLLMENTREQUEST._serialized_end=7669 + _ROUTERLEASEREQUEST._serialized_start=3286 + _ROUTERLEASEREQUEST._serialized_end=3545 + _ROUTERLEASERESPONSE._serialized_start=3547 + _ROUTERLEASERESPONSE._serialized_end=3649 + _POLICYROLE._serialized_start=3652 + _POLICYROLE._serialized_end=3816 + _HTTPGRANT._serialized_start=3818 + _HTTPGRANT._serialized_end=3878 + _EGRESSDESTINATION._serialized_start=3881 + _EGRESSDESTINATION._serialized_end=4170 + _INSPECTION._serialized_start=4172 + _INSPECTION._serialized_end=4229 + _INSPECTOR._serialized_start=4231 + _INSPECTOR._serialized_end=4342 + _MODELARMOR._serialized_start=4345 + _MODELARMOR._serialized_end=4490 + _EXTPROC._serialized_start=4493 + _EXTPROC._serialized_end=4757 + _EXTPROCPROCESSINGMODE._serialized_start=4760 + _EXTPROCPROCESSINGMODE._serialized_end=5399 + _EXTPROCPROCESSINGMODE_HEADERMODE._serialized_start=5244 + _EXTPROCPROCESSINGMODE_HEADERMODE._serialized_end=5301 + _EXTPROCPROCESSINGMODE_BODYMODE._serialized_start=5303 + _EXTPROCPROCESSINGMODE_BODYMODE._serialized_end=5399 + _CREDENTIALBROKER._serialized_start=5402 + _CREDENTIALBROKER._serialized_end=5627 + _OIDCFEDERATION._serialized_start=5629 + _OIDCFEDERATION._serialized_end=5724 + _AWSASSUMEROLE._serialized_start=5726 + _AWSASSUMEROLE._serialized_end=5783 + _PLATFORMIDENTITY._serialized_start=5785 + _PLATFORMIDENTITY._serialized_end=5819 + _POLICYBINDING._serialized_start=5821 + _POLICYBINDING._serialized_end=5867 + _POLICYCONFIG._serialized_start=5870 + _POLICYCONFIG._serialized_end=6021 + _POLICYCONFIGGETREQUEST._serialized_start=6023 + _POLICYCONFIGGETREQUEST._serialized_end=6047 + _POLICYCONFIGGETRESPONSE._serialized_start=6049 + _POLICYCONFIGGETRESPONSE._serialized_end=6097 + _POLICYCONFIGUPDATERESPONSE._serialized_start=6099 + _POLICYCONFIGUPDATERESPONSE._serialized_end=6159 + _EGRESSASSIGNMENTSREQUEST._serialized_start=6161 + _EGRESSASSIGNMENTSREQUEST._serialized_end=6187 + _EGRESSASSIGNMENTSRESPONSE._serialized_start=6189 + _EGRESSASSIGNMENTSRESPONSE._serialized_end=6265 + _KEYSRESPONSE._serialized_start=6267 + _KEYSRESPONSE._serialized_end=6369 + _TOKENREFRESHREQUEST._serialized_start=6371 + _TOKENREFRESHREQUEST._serialized_end=6478 + _TOKENREFRESHRESPONSE._serialized_start=6480 + _TOKENREFRESHRESPONSE._serialized_end=6597 + _NODECATALOGREPORT._serialized_start=6599 + _NODECATALOGREPORT._serialized_end=6663 + _TOKENREVOKEREQUEST._serialized_start=6665 + _TOKENREVOKEREQUEST._serialized_end=6702 + _TOKENREVOKERESPONSE._serialized_start=6704 + _TOKENREVOKERESPONSE._serialized_end=6757 + _BOOTSTRAPTOKENCREATEREQUEST._serialized_start=6760 + _BOOTSTRAPTOKENCREATEREQUEST._serialized_end=6910 + _BOOTSTRAPTOKENCREATERESPONSE._serialized_start=6913 + _BOOTSTRAPTOKENCREATERESPONSE._serialized_end=7051 + _BOOTSTRAPTOKEN._serialized_start=7054 + _BOOTSTRAPTOKEN._serialized_end=7353 + _BOOTSTRAPTOKENLISTRESPONSE._serialized_start=7355 + _BOOTSTRAPTOKENLISTRESPONSE._serialized_end=7429 + _ENROLLMENTREQUEST._serialized_start=7432 + _ENROLLMENTREQUEST._serialized_end=7774 _ENROLLMENTREQUEST_LABELSENTRY._serialized_start=825 _ENROLLMENTREQUEST_LABELSENTRY._serialized_end=870 - _ENROLLMENTREQUESTLISTRESPONSE._serialized_start=7671 - _ENROLLMENTREQUESTLISTRESPONSE._serialized_end=7753 - _USER._serialized_start=7755 - _USER._serialized_end=7867 - _ENROLLEDNODE._serialized_start=7870 - _ENROLLEDNODE._serialized_end=8225 + _ENROLLMENTREQUESTLISTRESPONSE._serialized_start=7776 + _ENROLLMENTREQUESTLISTRESPONSE._serialized_end=7858 + _USER._serialized_start=7860 + _USER._serialized_end=7972 + _ENROLLEDNODE._serialized_start=7975 + _ENROLLEDNODE._serialized_end=8330 _ENROLLEDNODE_LABELSENTRY._serialized_start=825 _ENROLLEDNODE_LABELSENTRY._serialized_end=870 - _ROUTERLEASE._serialized_start=8228 - _ROUTERLEASE._serialized_end=8424 - _NODESERVICES._serialized_start=8426 - _NODESERVICES._serialized_end=8534 - _ADMINSTATUSRESPONSE._serialized_start=8537 - _ADMINSTATUSRESPONSE._serialized_end=9012 - _ADMINSTATUSRESPONSE_NODECATALOGENTRY._serialized_start=8934 - _ADMINSTATUSRESPONSE_NODECATALOGENTRY._serialized_end=9012 - _USERSTATUSRESPONSE._serialized_start=9015 - _USERSTATUSRESPONSE._serialized_end=9272 - _IDENTITYEVIDENCERESPONSE._serialized_start=9275 - _IDENTITYEVIDENCERESPONSE._serialized_end=9503 - _PEEREVIDENCERESPONSE._serialized_start=9506 - _PEEREVIDENCERESPONSE._serialized_end=9832 + _ROUTERLEASE._serialized_start=8333 + _ROUTERLEASE._serialized_end=8529 + _NODESERVICES._serialized_start=8531 + _NODESERVICES._serialized_end=8639 + _ADMINSTATUSRESPONSE._serialized_start=8642 + _ADMINSTATUSRESPONSE._serialized_end=9117 + _ADMINSTATUSRESPONSE_NODECATALOGENTRY._serialized_start=9039 + _ADMINSTATUSRESPONSE_NODECATALOGENTRY._serialized_end=9117 + _USERSTATUSRESPONSE._serialized_start=9120 + _USERSTATUSRESPONSE._serialized_end=9377 + _IDENTITYEVIDENCERESPONSE._serialized_start=9380 + _IDENTITYEVIDENCERESPONSE._serialized_end=9608 + _PEEREVIDENCERESPONSE._serialized_start=9611 + _PEEREVIDENCERESPONSE._serialized_end=9937 _PEEREVIDENCERESPONSE_LABELSENTRY._serialized_start=825 _PEEREVIDENCERESPONSE_LABELSENTRY._serialized_end=870 - _MEMBERCREDENTIAL._serialized_start=9835 - _MEMBERCREDENTIAL._serialized_end=10103 - _TRUSTEDSIGNINGKEY._serialized_start=10105 - _TRUSTEDSIGNINGKEY._serialized_end=10194 - _OIDCSESSION._serialized_start=10196 - _OIDCSESSION._serialized_end=10285 - _TASKAUTHORIZATIONRULE._serialized_start=10288 - _TASKAUTHORIZATIONRULE._serialized_end=10435 - _TASKRULE._serialized_start=10438 - _TASKRULE._serialized_end=10570 - _TASKOPERATION._serialized_start=10572 - _TASKOPERATION._serialized_end=10687 - _TOKENEXCHANGEREQUEST._serialized_start=10690 - _TOKENEXCHANGEREQUEST._serialized_end=10861 - _TOKENEXCHANGERESPONSE._serialized_start=10863 - _TOKENEXCHANGERESPONSE._serialized_end=10990 - _STSTOKENREQUEST._serialized_start=10993 - _STSTOKENREQUEST._serialized_end=11122 - _STSTOKENRESPONSE._serialized_start=11125 - _STSTOKENRESPONSE._serialized_end=11256 - _REVOCATIONSRESPONSE._serialized_start=11258 - _REVOCATIONSRESPONSE._serialized_end=11328 + _MEMBERCREDENTIAL._serialized_start=9940 + _MEMBERCREDENTIAL._serialized_end=10208 + _TRUSTEDSIGNINGKEY._serialized_start=10210 + _TRUSTEDSIGNINGKEY._serialized_end=10299 + _OIDCSESSION._serialized_start=10301 + _OIDCSESSION._serialized_end=10390 + _TASKAUTHORIZATIONRULE._serialized_start=10393 + _TASKAUTHORIZATIONRULE._serialized_end=10540 + _TASKRULE._serialized_start=10543 + _TASKRULE._serialized_end=10675 + _TASKOPERATION._serialized_start=10677 + _TASKOPERATION._serialized_end=10792 + _TOKENEXCHANGEREQUEST._serialized_start=10795 + _TOKENEXCHANGEREQUEST._serialized_end=10966 + _TOKENEXCHANGERESPONSE._serialized_start=10968 + _TOKENEXCHANGERESPONSE._serialized_end=11095 + _STSTOKENREQUEST._serialized_start=11098 + _STSTOKENREQUEST._serialized_end=11227 + _STSTOKENRESPONSE._serialized_start=11230 + _STSTOKENRESPONSE._serialized_end=11361 + _REVOCATIONSRESPONSE._serialized_start=11363 + _REVOCATIONSRESPONSE._serialized_end=11433 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.pyi b/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.pyi index ca9ef447..309f027f 100644 --- a/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.pyi +++ b/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.pyi @@ -255,14 +255,16 @@ class DirectoryProvide(_message.Message): def __init__(self, key: _Optional[bytes] = ..., addrs: _Optional[_Iterable[str]] = ..., provider: _Optional[str] = ...) -> None: ... class DirectoryRequest(_message.Message): - __slots__ = ["find_peer", "find_providers", "provide"] + __slots__ = ["find_peer", "find_providers", "provide", "withdraw"] FIND_PEER_FIELD_NUMBER: _ClassVar[int] FIND_PROVIDERS_FIELD_NUMBER: _ClassVar[int] PROVIDE_FIELD_NUMBER: _ClassVar[int] + WITHDRAW_FIELD_NUMBER: _ClassVar[int] find_peer: DirectoryFindPeer find_providers: DirectoryFindProviders provide: DirectoryProvide - def __init__(self, provide: _Optional[_Union[DirectoryProvide, _Mapping]] = ..., find_providers: _Optional[_Union[DirectoryFindProviders, _Mapping]] = ..., find_peer: _Optional[_Union[DirectoryFindPeer, _Mapping]] = ...) -> None: ... + withdraw: DirectoryWithdraw + def __init__(self, provide: _Optional[_Union[DirectoryProvide, _Mapping]] = ..., find_providers: _Optional[_Union[DirectoryFindProviders, _Mapping]] = ..., find_peer: _Optional[_Union[DirectoryFindPeer, _Mapping]] = ..., withdraw: _Optional[_Union[DirectoryWithdraw, _Mapping]] = ...) -> None: ... class DirectoryResponse(_message.Message): __slots__ = ["error", "peer", "providers"] @@ -274,6 +276,14 @@ class DirectoryResponse(_message.Message): providers: _containers.RepeatedCompositeFieldContainer[PeerRecord] def __init__(self, providers: _Optional[_Iterable[_Union[PeerRecord, _Mapping]]] = ..., peer: _Optional[_Union[PeerRecord, _Mapping]] = ..., error: _Optional[str] = ...) -> None: ... +class DirectoryWithdraw(_message.Message): + __slots__ = ["key", "provider"] + KEY_FIELD_NUMBER: _ClassVar[int] + PROVIDER_FIELD_NUMBER: _ClassVar[int] + key: bytes + provider: str + def __init__(self, key: _Optional[bytes] = ..., provider: _Optional[str] = ...) -> None: ... + class DiscoveredProvider(_message.Message): __slots__ = ["local_proxy_url", "peer_id", "srv_description", "srv_name"] LOCAL_PROXY_URL_FIELD_NUMBER: _ClassVar[int] diff --git a/site/content/docs/concepts/networking.md b/site/content/docs/concepts/networking.md index b204cb23..58b921b2 100644 --- a/site/content/docs/concepts/networking.md +++ b/site/content/docs/concepts/networking.md @@ -43,16 +43,18 @@ The [node API reference](../../reference/node-api/) lists every route. ## Discovery -The routers keep the mesh's directory: a Kademlia DHT among themselves, -served to members over `/mesh/directory/1.0.0`. A node announces where it -is reached, and a node that publishes a service announces itself as a -provider of that service name, to a router it holds; the router keeps the -records and places each on the routers closest to its key. A node that -looks for a service, or for a peer, asks a router it holds, and the router -runs the lookup across the routers' DHT. A node keeps no DHT of its own and -connects to no router it does not hold. It re-announces every reprovide -interval of the mesh (`/info`), and a record a provider stopped -announcing leaves the directory after three of them. +The routers keep the mesh's directory, served to members over +`/mesh/directory/1.0.0`. A node announces where it is reached, and a node +that publishes a service announces itself as a provider of that service +name, to a router it holds; the router keeps the records and places each +on the routers closest to its key, which it finds in the Kademlia table +the routers keep among themselves. A node that looks for a service, or +for a peer, asks a router it holds, and the router answers from its own +records and from those closest routers. A node keeps no DHT of its own +and connects to no router it does not hold. It re-announces every +reprovide interval of the mesh (`/info`), withdraws a record when it +stops the service or leaves, and a record a provider stopped announcing +without withdrawing it leaves the directory after three intervals. Before a node advertises a backend, it probes the backend (`--backend-probe-timeout`, 2 seconds by default). A backend that does not diff --git a/site/content/docs/guides/scalability.md b/site/content/docs/guides/scalability.md index c75fdc91..a7972e81 100644 --- a/site/content/docs/guides/scalability.md +++ b/site/content/docs/guides/scalability.md @@ -25,10 +25,11 @@ uses the directory: it announces each service it publishes, and where it is reached, to a router it holds, and asks a router it holds for providers and peers. The router keeps the member's records, places each on the routers closest to its key (three, the mesh's replication, -`agentmesh-control-plane --dht-replication`), and runs each lookup across -the routers' own DHT. The member keeps no DHT and connects to no router -it does not hold. It re-announces every 5 minutes (the mesh's -`--dht-reprovide-interval`). A member connects to another member only to +`agentmesh-control-plane --dht-replication`), and answers each lookup +from its own records and those routers', asked at once. The member keeps +no DHT and connects to no router it does not hold. It re-announces every +5 minutes (the mesh's `--dht-reprovide-interval`) and withdraws a record +when it stops the service or leaves. A member connects to another member only to call it, through that member's router, where it runs the credential handshake before asking for the circuit. At rest it uses about 55 MiB of memory, 300 goroutines and 20 file descriptors, and almost no CPU. @@ -213,13 +214,14 @@ N − 1 others, which is where the headroom in the next section goes. The directory costs a router no sessions. A member's announcements and lookups travel on the sessions it already holds, and a router answers a -lookup from the DHT the routers keep among themselves, over the -connections they already hold to each other. What the directory costs -is records and requests: a router keeps the records of its own members -and r copies of records whose keys land on it, and runs one lookup -across the routers per request. A key that every member publishes, such -as a service type, lands on the same r routers for all of them; those r -routers hold a record per publisher and answer every lookup of it. +lookup from its own records and the r routers closest to the key, over +the connections the routers already hold to each other. What the +directory costs is records and requests: a router keeps the records of +its own members and r copies of records whose keys land on it, and asks +r routers per lookup. A key that every member publishes, such as a +service type, lands on the same r routers for all of them; those r +routers hold a record per publisher and answer every lookup of it, and +a lookup of it returns at most 100 providers. What bounds the total is the router's libp2p connection cap, twice the high watermark: a router past it refuses the next connection, session or @@ -455,7 +457,7 @@ The router and the control plane serve Prometheus metrics on | `agentmesh_control_plane_http_request_duration_seconds` | The control plane's own latency; p99 above a second under a join burst means the database is the bound. | | `agentmesh_node_mesh_connected` | On every node: `0` for five minutes is a member off the mesh. | | `agentmesh_node_routers{state="attached"}` against `{state="wanted"}` | On every node: fewer attached than wanted for long is a member that cannot find a router with room, or one whose selector too few routers carry. `agentmesh_node_router_candidates{match="selector"}` is how many it could choose from. | -| `agentmesh_router_directory_requests_total{op,result}`, `agentmesh_router_directory_request_seconds{op}` | On every router: the directory requests its members made, by operation (`provide`, `find_providers`, `find_peer`; `place` is a record another router put here) and result. `error` rising is a lookup across the routers failing, a peer router down or slow; the histogram is what a member waits for a lookup. `agentmesh_router_directory_placements_total{result="error"}` rising is a router that does not take the records placed on it. | +| `agentmesh_router_directory_requests_total{op,result}`, `agentmesh_router_directory_request_seconds{op}` | On every router: the directory requests its members made, by operation (`provide`, `withdraw`, `find_providers`, `find_peer`; `place` is a record or withdrawal another router put here, `lookup` a find another router ran here) and result. `agentmesh_router_directory_lookups_total{result="error"}` rising is a peer router down or slow, whose records lookups miss; the histogram is what a member waits for a lookup. `agentmesh_router_directory_placements_total{result="error"}` rising is a router that does not take the records placed on it. `agentmesh_router_directory_records` is what each router holds. | | `agentmesh_node_directory_requests_total{op,result}`, `agentmesh_node_directory_request_seconds{op}` | On every node: the same from the member's side. `failed` is a request no held router answered; `empty` a lookup nobody had a record for, which on a fresh mesh is ordinary. | An autoscaler for the routers has its inputs in the first two rows: scale diff --git a/site/content/docs/reference/router.md b/site/content/docs/reference/router.md index 1a78eeab..f5f87834 100644 --- a/site/content/docs/reference/router.md +++ b/site/content/docs/reference/router.md @@ -75,9 +75,13 @@ replica count. holds. A member announces each service it publishes and where it is reached; the router keeps the record and places it on the routers closest to the key, as many as the mesh's replication (`/info`). A - member asks for providers of a key or for a peer; the router runs the - lookup across the routers' DHT and answers. A member keeps no DHT and - connects to no router it does not hold. + record lives for the mesh's record TTL after its last announcement, + and an announcement replaces the addresses of the one before; a member + that stops a service or leaves withdraws its records, and the router + withdraws them from the same routers. A member asks for providers of + a key or for a peer; the router answers from its own records and from + the routers closest to the key, asked at once. A member keeps no DHT + and connects to no router it does not hold. 6. Refreshes its own credential before it expires, like a node. 7. Every 5 seconds, compares the members it holds, the peers that passed the handshake, with `--high-watermark`. At the mark it picks members at @@ -130,8 +134,11 @@ The metrics address exposes, next to the Go runtime metrics: | `agentmesh_router_connected_peers`, `agentmesh_router_authenticated_peers`, `agentmesh_router_banned_peers` | Peers in each state. | | `agentmesh_router_peer_routers{state}` | Other routers this one knows, from the control plane's list or a handshake that proved the router role: `listed`, and `authenticated` for those it currently holds a mutual authentication with. The two differ while a federation pass is pending or a peer router is down. | | `agentmesh_router_dht_routing_table_size` | Peer routers in the DHT's routing table: the authenticated peer routers, since nothing else enters it. | -| `agentmesh_router_directory_requests_total{op,result}`, `agentmesh_router_directory_request_seconds{op}` | Directory requests served, by operation (`provide`, `find_providers`, `find_peer`, and `place` for a record another router put here) and result (`ok`, `empty`, `invalid`, `error`), with the time each took. | -| `agentmesh_router_directory_placements_total{result}` | Copies of a member's record this router placed on the routers closest to the key. | +| `agentmesh_router_directory_requests_total{op,result}`, `agentmesh_router_directory_request_seconds{op}` | Directory requests served, by operation (`provide`, `withdraw`, `find_providers`, `find_peer`; `place` is a record or withdrawal another router put here, `lookup` a find another router ran here) and result (`ok`, `empty`, `invalid`, `error`), with the time each took. | +| `agentmesh_router_directory_records` | Directory records this router holds: its own members' and the copies placed on it. | +| `agentmesh_router_directory_records_expired_total` | Records dropped because their provider stopped announcing them for the mesh's record TTL. | +| `agentmesh_router_directory_placements_total{result}` | Copies of a member's record, or of its withdrawal, this router placed on the routers closest to the key. | +| `agentmesh_router_directory_lookups_total{result}` | Peer routers asked during a lookup; `error` is one that did not answer in time and was left out of the answer. | | `agentmesh_router_auth_handshakes_total{result}`, `agentmesh_router_lease_renewals_total{result}` | Handshakes and lease renewals by outcome. | | `agentmesh_router_goaway_sent_total{reason,result}` | Members sent away, by reason (`DRAINING`, `OVERLOADED`) and outcome: `ok` when the member took the message, `unsupported` when it does not speak the protocol and was closed instead, `failed` when the stream broke. A rising `OVERLOADED` count on a router that is not draining is a fleet that has outgrown its routers. | | `agentmesh_router_unauthenticated_connections_closed_total` | Connections closed at `--auth-deadline` without a handshake. A member handshakes with a router the moment it connects, so a steady rate is members that cannot complete the handshake, and a burst is a scanner. | diff --git a/tests/integration/minimal_helpers_test.go b/tests/integration/minimal_helpers_test.go index 3a39cde3..5611d772 100644 --- a/tests/integration/minimal_helpers_test.go +++ b/tests/integration/minimal_helpers_test.go @@ -877,6 +877,8 @@ func (d *mockDirectory) handle(s network.Stream) { d.records[string(q.GetKey())] = map[peer.ID][]string{} } d.records[string(q.GetKey())][s.Conn().RemotePeer()] = q.GetAddrs() + case req.GetWithdraw() != nil: + delete(d.records[string(req.GetWithdraw().GetKey())], s.Conn().RemotePeer()) case req.GetFindProviders() != nil: for p, addrs := range d.records[string(req.GetFindProviders().GetKey())] { resp.Providers = append(resp.Providers, &api.PeerRecord{PeerId: p.String(), Addrs: addrs}) diff --git a/tests/integration/router_attachment_test.go b/tests/integration/router_attachment_test.go index 0268c3d9..4422e7c9 100644 --- a/tests/integration/router_attachment_test.go +++ b/tests/integration/router_attachment_test.go @@ -830,11 +830,21 @@ func TestCallerTransitsARouterItsCatalogMissed(t *testing.T) { peerIDStrings(routerB.peerID), 15*time.Second) callAcrossMesh(t, caller, "token-c", provider, "late-tool") - // Router b admitted the provider, router a, the control plane's publisher - // and, for the call, the caller. - if got := routerB.gauge(t, "agentmesh_router_authenticated_peers"); got != 4 { - t.Errorf("router b authenticated %v peers, want 4: the caller must have handshaken there before asking for the circuit", got) + // The caller handshook with router b before asking it for the circuit: + // one transit handshake, and router b counts the caller among the peers + // it admitted, with the provider, router a and the control plane's + // publisher. The publisher reaches a new router on its own clock, so + // its admission is waited for rather than read off the call's finish. + var transits float64 + for _, kind := range []string{"listed", "unlisted"} { + transits += nodeCounter(t, caller, fmt.Sprintf(`agentmesh_node_transit_handshakes_total{result="ok",router="%s"}`, kind)) + } + if transits != 1 { + t.Errorf("caller ran %v transit handshakes, want 1: the one with router b before the circuit", transits) } + waitForEqual(t, "peers router b authenticated", func() []string { + return []string{fmt.Sprint(routerB.gauge(t, "agentmesh_router_authenticated_peers"))} + }, []string{"4"}, 10*time.Second) t.Logf("--- caller ---\n%s", caller.log()) } From 34fbd0b2a08043106f82d8733c7cd91555ad4c3a Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Wed, 14 Oct 2026 08:37:47 +0000 Subject: [PATCH 2/7] directory: a lookup one router answers with nothing is put to the next A member took the first router's answer to a lookup, empty or not. The router answers from its own records and the routers closest to the key; one cut off from those answers from what it can reach, and the member holds another router that may not be. A lookup that comes back empty is now put to the next held router before it is empty; an announcement or a withdrawal one router took is still not repeated. The same in the Go node and both SDKs. --- internal/node/directory_client.go | 19 +++-- internal/node/directory_client_test.go | 12 +++ sdk/js/src/discovery.test.ts | 101 +++++++++++++++++++++++++ sdk/js/src/discovery.ts | 16 +++- sdk/python/src/agent_mesh/discovery.py | 15 +++- sdk/python/tests/test_discovery.py | 28 +++++++ 6 files changed, 182 insertions(+), 9 deletions(-) create mode 100644 sdk/js/src/discovery.test.ts diff --git a/internal/node/directory_client.go b/internal/node/directory_client.go index 5d0e44b8..b4899a0e 100644 --- a/internal/node/directory_client.go +++ b/internal/node/directory_client.go @@ -185,7 +185,10 @@ func (d *routerDirectory) FindPeer(ctx context.Context, p peer.ID) (peer.AddrInf return info, nil } -// request sends req to the attached routers in turn until one answers. +// request sends req to the attached routers in turn until one answers. A +// lookup one router answers with nothing is put to the next as well: a +// router cut off from the routers closest to the key answers from what it +// can reach, and the member holds another that may not be. func (d *routerDirectory) request(ctx context.Context, op string, req *api.DirectoryRequest) (*api.DirectoryResponse, error) { start := time.Now() defer func() { directoryRequestSeconds.WithLabelValues(op).Observe(time.Since(start).Seconds()) }() @@ -199,15 +202,17 @@ func (d *routerDirectory) request(ctx context.Context, op string, req *api.Direc logger.Warnf("[Directory] %s: %v", op, errNoDirectoryRouter) return nil, errNoDirectoryRouter } + lookup := op == directoryOpFindProviders || op == directoryOpFindPeer var lastErr error + var empty *api.DirectoryResponse for _, router := range routers { resp, err := d.ask(ctx, router, data) if err == nil { - result := directoryResultOK - if op != directoryOpProvide && op != directoryOpWithdraw && len(resp.GetProviders()) == 0 && resp.GetPeer() == nil { - result = directoryResultEmpty + if lookup && len(resp.GetProviders()) == 0 && resp.GetPeer() == nil { + empty = resp + continue } - directoryRequestsTotal.WithLabelValues(op, result).Inc() + directoryRequestsTotal.WithLabelValues(op, directoryResultOK).Inc() return resp, nil } lastErr = err @@ -216,6 +221,10 @@ func (d *routerDirectory) request(ctx context.Context, op string, req *api.Direc break } } + if empty != nil { + directoryRequestsTotal.WithLabelValues(op, directoryResultEmpty).Inc() + return empty, nil + } directoryRequestsTotal.WithLabelValues(op, directoryResultFailed).Inc() return nil, fmt.Errorf("%s: no attached router answered: %w", op, lastErr) } diff --git a/internal/node/directory_client_test.go b/internal/node/directory_client_test.go index 52e08f47..e175256d 100644 --- a/internal/node/directory_client_test.go +++ b/internal/node/directory_client_test.go @@ -227,6 +227,18 @@ func TestRouterDirectoryAsksTheAttachedRouters(t *testing.T) { t.Fatalf("FindPeer of the member = %v, %v; want it from the router's own connection", info, err) } + // The first router knows nothing of a key the second holds: a lookup + // one router answers with nothing is put to the next. + elsewhere, _ := api.ServiceKey(api.ServiceType_SERVICE_TYPE_MCP, "elsewhere") + dir2.add(elsewhere, member.ID(), member.Addrs()...) + before1, before2 := dir1.servedCount(), dir2.servedCount() + if found, err := directory.FindProviders(ctx, elsewhere, 0); err != nil || len(found) != 1 { + t.Fatalf("FindProviders of a key only the second router holds = %v, %v; want the second router's answer", found, err) + } + if dir1.servedCount() != before1+1 || dir2.servedCount() != before2+1 { + t.Fatalf("routers served %d and %d requests, want %d and %d: the second is asked when the first has nothing", dir1.servedCount(), dir2.servedCount(), before1+1, before2+1) + } + // The first router fails: the second answers, and the member is none the wiser. dir1.fail(true) dir2.add(key, member.ID(), member.Addrs()...) diff --git a/sdk/js/src/discovery.test.ts b/sdk/js/src/discovery.test.ts new file mode 100644 index 00000000..1f2d4826 --- /dev/null +++ b/sdk/js/src/discovery.test.ts @@ -0,0 +1,101 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { create, fromBinary, toBinary } from "@bufbuild/protobuf"; +import { yamux } from "@chainsafe/libp2p-yamux"; +import type { Connection, Libp2p } from "@libp2p/interface"; +import { tcp } from "@libp2p/tcp"; +import { tls } from "@libp2p/tls"; +import { lpStream } from "@libp2p/utils"; +import { createLibp2p } from "libp2p"; +import assert from "node:assert/strict"; +import { after, before, test } from "node:test"; +import { DIRECTORY_PROTOCOL, DirectoryError, request, serviceKey } from "./discovery.ts"; +import { DirectoryRequestSchema, DirectoryResponseSchema, PeerRecordSchema, type DirectoryResponse } from "./gen/agentmesh_pb.ts"; + +const PROVIDER = "12D3KooWA4Xop1JaT3MHxwYMkCepYsv4iPVopMXwCz5iHYdBfeSB"; + +let member: Libp2p; +let routerA: Libp2p; +let routerB: Libp2p; +const asked: string[] = []; +// What each router answers a find with; an announcement is always taken. +const answers = new Map DirectoryResponse>(); + +async function node(): Promise { + return createLibp2p({ addresses: { listen: ["/ip4/127.0.0.1/tcp/0"] }, transports: [tcp()], connectionEncrypters: [tls()], streamMuxers: [yamux()] }); +} + +function serve(router: Libp2p, name: string): Promise { + return router.handle(DIRECTORY_PROTOCOL, (stream) => { + void (async () => { + const lp = lpStream(stream, { maxDataLength: 64 * 1024 }); + const req = fromBinary(DirectoryRequestSchema, (await lp.read()).subarray()); + asked.push(name); + const resp = req.request.case === "provide" ? create(DirectoryResponseSchema, {}) : (answers.get(name) ?? (() => create(DirectoryResponseSchema, {})))(); + await lp.write(toBinary(DirectoryResponseSchema, resp)); + await stream.close(); + })(); + }); +} + +before(async () => { + [member, routerA, routerB] = await Promise.all([node(), node(), node()]); + await Promise.all([serve(routerA, "a"), serve(routerB, "b")]); + await member.dial(routerA.getMultiaddrs()); + await member.dial(routerB.getMultiaddrs()); +}); + +after(async () => { + await Promise.all([member.stop(), routerA.stop(), routerB.stop()]); +}); + +function held(router: Libp2p) { + return { peerId: router.peerId.toString(), connection: (): Connection | undefined => member.getConnections(router.peerId)[0] }; +} + +const found = () => create(DirectoryResponseSchema, { providers: [create(PeerRecordSchema, { peerId: PROVIDER, addrs: [`/ip4/10.0.0.9/tcp/4001/p2p/${PROVIDER}`] })] }); +const nothing = () => create(DirectoryResponseSchema, {}); + +test("a lookup one router answers with nothing is put to the next; an announcement is not", async () => { + // A router cut off from the routers closest to the key answers from what + // it can reach; the member holds another that may not be. + const key = await serviceKey("mcp", "calculator"); + const find = create(DirectoryRequestSchema, { request: { case: "findProviders", value: { key, limit: 20 } } }); + + answers.set("a", nothing); + answers.set("b", found); + asked.length = 0; + let resp = await request([held(routerA), held(routerB)], find); + assert.equal(resp.providers[0]?.peerId, PROVIDER); + assert.deepEqual(asked, ["a", "b"]); + + answers.set("b", nothing); + asked.length = 0; + resp = await request([held(routerA), held(routerB)], find); + assert.equal(resp.providers.length, 0, "nobody has a record: the answer is empty, not an error"); + assert.deepEqual(asked, ["a", "b"]); + + answers.set("a", found); + asked.length = 0; + resp = await request([held(routerA), held(routerB)], find); + assert.equal(resp.providers[0]?.peerId, PROVIDER); + assert.deepEqual(asked, ["a"], "the first router answered"); + + asked.length = 0; + await request([held(routerA), held(routerB)], create(DirectoryRequestSchema, { request: { case: "provide", value: { key, addrs: [] } } })); + assert.deepEqual(asked, ["a"], "an announcement one router took is not repeated"); + + await assert.rejects(request([{ peerId: "nobody", connection: () => undefined }], find), DirectoryError); +}); diff --git a/sdk/js/src/discovery.ts b/sdk/js/src/discovery.ts index 466afec3..909db12b 100644 --- a/sdk/js/src/discovery.ts +++ b/sdk/js/src/discovery.ts @@ -104,11 +104,15 @@ async function ask(conn: Connection, req: DirectoryRequest, signal: AbortSignal) /** * Sends req to the held routers in turn until one answers. A router that - * is restarting costs the request a timeout, not an answer. + * is restarting costs the request a timeout, not an answer. A lookup one + * router answers with nothing is put to the next as well: a router cut + * off from the routers closest to the key answers from what it can reach. */ export async function request(routers: DirectoryRouter[], req: DirectoryRequest, signal?: AbortSignal): Promise { + const lookup = req.request.case === "findProviders" || req.request.case === "findPeer"; let last: unknown; let asked = 0; + let empty: DirectoryResponse | undefined; for (const router of routers) { const conn = router.connection(); if (conn === undefined) { @@ -117,7 +121,12 @@ export async function request(routers: DirectoryRouter[], req: DirectoryRequest, asked++; const bounded = signal === undefined ? AbortSignal.timeout(REQUEST_TIMEOUT_MS) : AbortSignal.any([signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]); try { - return await ask(conn, req, bounded); + const resp = await ask(conn, req, bounded); + if (lookup && resp.providers.length === 0 && resp.peer === undefined) { + empty = resp; + continue; + } + return resp; } catch (err) { last = err; if (signal?.aborted === true) { @@ -125,6 +134,9 @@ export async function request(routers: DirectoryRouter[], req: DirectoryRequest, } } } + if (empty !== undefined) { + return empty; + } if (asked === 0) { throw new DirectoryError("no held router to ask"); } diff --git a/sdk/python/src/agent_mesh/discovery.py b/sdk/python/src/agent_mesh/discovery.py index 06e9bb56..ce901bf2 100644 --- a/sdk/python/src/agent_mesh/discovery.py +++ b/sdk/python/src/agent_mesh/discovery.py @@ -107,16 +107,27 @@ async def _ask(host: IHost, router: ID, req: pb.DirectoryRequest) -> pb.Director async def request(host: IHost, routers: Iterable[ID], req: pb.DirectoryRequest) -> pb.DirectoryResponse: """Sends req to the held routers in turn until one answers. A router that - is restarting costs the request a timeout, not an answer.""" + is restarting costs the request a timeout, not an answer. A lookup one + router answers with nothing is put to the next as well: a router cut off + from the routers closest to the key answers from what it can reach.""" + lookup = req.WhichOneof("request") in ("find_providers", "find_peer") last: Exception | None = None asked = 0 + empty: pb.DirectoryResponse | None = None for router in routers: asked += 1 try: - return await _ask(host, router, req) + resp = await _ask(host, router, req) except Exception as err: # noqa: BLE001 - the next router is asked last = err logger.warning("directory: request through router %s failed: %s", router, err) + continue + if lookup and not resp.providers and not resp.HasField("peer"): + empty = resp + continue + return resp + if empty is not None: + return empty if asked == 0: raise DirectoryError("no held router to ask") raise DirectoryError(f"no held router answered: {last}") diff --git a/sdk/python/tests/test_discovery.py b/sdk/python/tests/test_discovery.py index 9e9d5b71..65785203 100644 --- a/sdk/python/tests/test_discovery.py +++ b/sdk/python/tests/test_discovery.py @@ -216,3 +216,31 @@ async def main(): assert bomb.payload_reads == 0 trio.run(main) + + +def test_an_empty_answer_is_put_to_the_next_router(): + """A router cut off from the routers closest to the key answers with + nothing; the next held router is asked before the lookup is empty. An + announcement is not repeated: one router taking it is enough.""" + + def knows_nothing(req): + return pb.DirectoryResponse() + + def answers(req): + return pb.DirectoryResponse(providers=[_record(PROVIDER, "/ip4/10.0.0.9/tcp/4001")]) + + async def main(): + host = _Host({ROUTER: knows_nothing, ROUTER_B: answers}) + found = await find_providers(host, [ROUTER, ROUTER_B], service_key("mcp", "calculator")) + assert [p.peer_id for p in found] == [str(PROVIDER)] + assert host.asked == [ROUTER, ROUTER_B] + + host = _Host({ROUTER: knows_nothing, ROUTER_B: knows_nothing}) + assert await find_providers(host, [ROUTER, ROUTER_B], service_key("mcp", "calculator")) == [] + assert host.asked == [ROUTER, ROUTER_B] + + host = _Host({ROUTER: knows_nothing, ROUTER_B: answers}) + await provide(host, [ROUTER, ROUTER_B], service_key("mcp", "calculator"), []) + assert host.asked == [ROUTER], "an announcement one router took is not repeated" + + trio.run(main) From 6c8910873935c3644e4341f04dba4d4e0feaad4d Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Wed, 14 Oct 2026 09:09:10 +0000 Subject: [PATCH 3/7] sdk: a member announces where it is reached under its peer key An SDK member published no address, so the directory knew it only through the router that held it: a find_peer from any other router found nothing, and a caller on other routers fell back to circuits through its own routers, which refuse it. The rollout probe on the testnet hit this: a node reaching the JS agent by peer ID got PERMISSION_DENIED from every circuit it tried. Both SDKs now announce their relayed addresses under their peer key to a router they hold: after join, when the addresses change (a router replaced) and every five minutes, agentmesh-node's reprovide interval; a member that reserves no relay announces nothing, there is nothing to reach it at. A member leaving withdraws the record. The announcement is best effort: a router that does not take it is a warning the next one retries. The Python directory client asks only routers it is connected to, as the JS one does: a request goes over the session the handshake opened, never over a dial of its own, which left the reservation loop facing a connection it had not authenticated. top_up_routers runs one at a time: the reservation loop and a go-away both call it, and two picking the same router at once held it twice, or shunned it while holding it. --- sdk/js/src/discovery.ts | 8 +++ sdk/js/src/session.test.ts | 45 ++++++++++++++- sdk/js/src/session.ts | 60 ++++++++++++++++++- sdk/python/src/agent_mesh/discovery.py | 6 ++ sdk/python/src/agent_mesh/session.py | 80 ++++++++++++++++++++++++-- sdk/python/tests/test_session.py | 41 ++++++++++++- 6 files changed, 233 insertions(+), 7 deletions(-) diff --git a/sdk/js/src/discovery.ts b/sdk/js/src/discovery.ts index 909db12b..711a7784 100644 --- a/sdk/js/src/discovery.ts +++ b/sdk/js/src/discovery.ts @@ -152,6 +152,14 @@ export async function provide(routers: DirectoryRouter[], key: Uint8Array, addrs await request(routers, create(DirectoryRequestSchema, { request: { case: "provide", value: { key, addrs } } }), signal); } +/** + * Removes this member's record under key from the held router and from the + * routers it was placed on. + */ +export async function withdraw(routers: DirectoryRouter[], key: Uint8Array, signal?: AbortSignal): Promise { + await request(routers, create(DirectoryRequestSchema, { request: { case: "withdraw", value: { key } } }), signal); +} + /** Who provides key, up to limit, with their addresses; empty when nobody does. */ export async function findProviders(routers: DirectoryRouter[], key: Uint8Array, limit = DEFAULT_LIMIT, signal?: AbortSignal): Promise { const resp = await request(routers, create(DirectoryRequestSchema, { request: { case: "findProviders", value: { key, limit: Math.max(limit, 1) } } }), signal); diff --git a/sdk/js/src/session.test.ts b/sdk/js/src/session.test.ts index 54344127..9bea398d 100644 --- a/sdk/js/src/session.test.ts +++ b/sdk/js/src/session.test.ts @@ -28,7 +28,7 @@ import type { Libp2p } from "@libp2p/interface"; import { tcp } from "@libp2p/tcp"; import { tls } from "@libp2p/tls"; import { lpStream } from "@libp2p/utils"; -import { DIRECTORY_PROTOCOL } from "./discovery.ts"; +import { DIRECTORY_PROTOCOL, peerKey } from "./discovery.ts"; import { multiaddr } from "@multiformats/multiaddr"; import { createLibp2p } from "libp2p"; import assert from "node:assert/strict"; @@ -454,6 +454,49 @@ test("discovery asks a held router over the directory protocol and connects to n } }); +test("a member announces where it is reached under its peer key and withdraws it on close", async () => { + // A member with a relay reservation announces its relayed addresses so + // that a peer on other routers finds it by ID; a member that reserves + // nothing has nothing to announce. + const requests: Array<{ op: string; key: Uint8Array; addrs: string[] }> = []; + await router.handle(DIRECTORY_PROTOCOL, (stream) => { + void (async () => { + const lp = lpStream(stream, { maxDataLength: 64 * 1024 }); + const req = fromBinary(DirectoryRequestSchema, (await lp.read()).subarray()); + const value = req.request.value as { key?: Uint8Array; addrs?: string[] } | undefined; + requests.push({ op: req.request.case ?? "", key: value?.key ?? new Uint8Array(), addrs: value?.addrs ?? [] }); + await lp.write(toBinary(DirectoryResponseSchema, create(DirectoryResponseSchema, {}))); + await stream.close(); + })(); + }); + try { + const mesh = await AgentMesh.enroll({ controlPlaneUrl: "http://127.0.0.1:1", bootstrapToken: "sbt", fetch: fakeControlPlane([routerAddr]) }); + const session = await mesh.join({ refreshLeadMs: 0 }); + const relayed = session.relayAddresses.map(String).sort(); + assert.ok(relayed.length > 0, "no relayed address after the reservation"); + const key = await peerKey(session.peerId); + assert.deepEqual( + requests.map((r) => r.op), + ["provide"], + ); + assert.equal(Buffer.from(requests[0]?.key ?? []).toString("hex"), Buffer.from(key).toString("hex")); + assert.deepEqual(requests[0]?.addrs, relayed); + await session.close(); + assert.deepEqual( + requests.map((r) => r.op), + ["provide", "withdraw"], + ); + assert.equal(Buffer.from(requests[1]?.key ?? []).toString("hex"), Buffer.from(key).toString("hex")); + + requests.length = 0; + const silent = await mesh.join({ refreshLeadMs: 0, reserveRelay: false }); + await silent.close(); + assert.deepEqual(requests, [], "a member with no relayed address announces nothing"); + } finally { + await router.unhandle(DIRECTORY_PROTOCOL); + } +}); + test("a resumed member adopts a rotated key before admitting routers", async () => { // The control plane rotated its signing key while this member was not // running: the routers already hold credentials under the new key, and diff --git a/sdk/js/src/session.ts b/sdk/js/src/session.ts index 3d426f63..598efbef 100644 --- a/sdk/js/src/session.ts +++ b/sdk/js/src/session.ts @@ -32,7 +32,7 @@ import { ROLE_ROUTER, attenuateBiscuit, requireRole, sealBiscuit, type VerifiedB import { ROLE_NODE } from "./controlplane.ts"; import { encodeAuthFrame } from "./credential.ts"; import { canonicalPeerId } from "./identity.ts"; -import { findPeer, findProviders, isServiceType, parseServiceTarget, serviceKey, type DirectoryRouter } from "./discovery.ts"; +import { findPeer, findProviders, isServiceType, parseServiceTarget, peerKey, provide, serviceKey, withdraw, type DirectoryRouter } from "./discovery.ts"; import type { PeerRecord, RouterGoAway, RouterInfo, TaskAuthorizationRuleSchema } from "./gen/agentmesh_pb.ts"; import { createMeshHost, listenThroughRelay, type MeshHost, type MeshHostOptions, type RelayListener } from "./host.ts"; import { openMCPSession, requireEgressLabels, type MCPSession, type MCPSessionOptions } from "./mcp.ts"; @@ -114,6 +114,11 @@ export interface JoinOptions extends MeshHostOptions { * how long the member is unreachable at most before it reserves again. */ relayCheckIntervalMs?: number; + /** + * How often this member announces where it is reached under its peer + * key (default 5 minutes, agentmesh-node's reprovide interval). + */ + announceIntervalMs?: number; /** * agentmesh-node's egress.require_labels for an SDK member: every provider this * session calls must attest all of these pairs, on top of a call's @@ -186,6 +191,13 @@ const DEFAULT_CONTROL_PLANE_SYNC_MS = 15 * 60 * 1000; const FIRST_CONTROL_PLANE_SYNC_MS = 2_000; const DEFAULT_CONTROL_PLANE_SYNC_JITTER_MS = 2_000; const DEFAULT_RELAY_CHECK_MS = 30 * 1000; +/** + * How often a member announces where it is reached: agentmesh-node's default + * reprovide interval, a third of the mesh's default record TTL. + */ +const DEFAULT_ANNOUNCE_MS = 5 * 60 * 1000; +/** What close() waits for the member's record to be withdrawn. */ +const WITHDRAW_TIMEOUT_MS = 2 * 1000; /** How long a provider's positive egress verdict is kept; agentmesh-node's labelGateTTL. */ const EGRESS_VERDICT_TTL_MS = 5 * 60 * 1000; @@ -212,6 +224,9 @@ export class MeshSession { #policyTimer: ReturnType | undefined; #syncTimer: ReturnType | undefined; #relayTimer: ReturnType | undefined; + #announceTimer: ReturnType | undefined; + /** The addresses of the record last announced under this member's peer key. */ + #announced: string[] = []; #syncing: Promise | undefined; #keepingRelay: Promise | undefined; #keepRelayPending = false; @@ -286,6 +301,8 @@ export class MeshSession { } this.#relayTimer = setInterval(() => void this.keepRelay().catch(() => {}), options.relayCheckIntervalMs ?? DEFAULT_RELAY_CHECK_MS); this.#relayTimer.unref?.(); + this.#announceTimer = setInterval(() => void this.announce().catch(() => {}), options.announceIntervalMs ?? DEFAULT_ANNOUNCE_MS); + this.#announceTimer.unref?.(); } } @@ -369,6 +386,26 @@ export class MeshSession { }); } + /** + * Announces where this member is reached, its relayed addresses on the + * routers it holds, under its peer key, so that a peer attached to other + * routers finds it by ID. Runs after join, when the addresses change and + * every announceIntervalMs; the record outlives its last announcement by + * the mesh's record TTL. A member with no relayed address announces + * nothing: there is nothing to reach it at. + */ + async announce(): Promise { + if (this.#closed || this.#isTaskView) { + return; + } + const addrs = this.relayAddresses.map(String).sort(); + if (addrs.length === 0) { + return; + } + await provide(this.#directoryRouters(), await peerKey(this.peerId), addrs); + this.#announced = addrs; + } + /** Whether peerId is currently one of the routers this member holds. */ isHeldRouter(peerId: string): boolean { try { @@ -567,6 +604,17 @@ export class MeshSession { if (this.#closed) { return; } + try { + await this.#maintainRouters(); + } finally { + const addrs = this.relayAddresses.map(String).sort(); + if (!this.#closed && addrs.length > 0 && addrs.join(" ") !== this.#announced.join(" ")) { + await this.announce().catch(() => {}); + } + } + } + + async #maintainRouters(): Promise { const held = this.routers.filter((r) => this.#heldRouterIds.has(r.peerId)); for (const r of held) { if (this.#closed) { @@ -1160,11 +1208,18 @@ export class MeshSession { } async close(): Promise { + const announced = this.#announced.length > 0 && !this.#closed && !this.#isTaskView; this.#closed = true; clearTimeout(this.#refreshTimer); clearTimeout(this.#syncTimer); clearInterval(this.#policyTimer); clearInterval(this.#relayTimer); + clearInterval(this.#announceTimer); + if (announced) { + // Best effort: the record expires on its own within the record TTL. + await withdraw(this.#directoryRouters(), await peerKey(this.peerId), AbortSignal.timeout(WITHDRAW_TIMEOUT_MS)).catch(() => {}); + this.#announced = []; + } if (!this.#isTaskView) { await this.node.stop(); } @@ -1401,6 +1456,9 @@ export async function joinMesh(mesh: AgentMesh, options: JoinOptions = {}): Prom const detail = failures.length > 0 ? `:\n ${failures.join("\n ")}` : ""; throw new Error(`no router admitted this member${detail}`); } + // Best effort: a router that does not take the record is a warning the + // next announcement retries; the member is on the mesh regardless. + await session.announce().catch(() => {}); return session; } catch (err) { if (session !== undefined) { diff --git a/sdk/python/src/agent_mesh/discovery.py b/sdk/python/src/agent_mesh/discovery.py index ce901bf2..bad98c31 100644 --- a/sdk/python/src/agent_mesh/discovery.py +++ b/sdk/python/src/agent_mesh/discovery.py @@ -141,6 +141,12 @@ async def provide(host: IHost, routers: Iterable[ID], key: bytes, addrs: Iterabl await request(host, routers, req) +async def withdraw(host: IHost, routers: Iterable[ID], key: bytes) -> None: + """Removes this member's record under key from the held router and from + the routers it was placed on.""" + await request(host, routers, pb.DirectoryRequest(withdraw=pb.DirectoryWithdraw(key=key))) + + async def find_providers(host: IHost, routers: Iterable[ID], key: bytes, limit: int = DEFAULT_LIMIT) -> list[DiscoveredProvider]: """Who provides key, up to limit, with their addresses; empty when nobody does.""" req = pb.DirectoryRequest(find_providers=pb.DirectoryFindProviders(key=key, limit=max(limit, 1))) diff --git a/sdk/python/src/agent_mesh/session.py b/sdk/python/src/agent_mesh/session.py index 73fda5a6..2959d0aa 100644 --- a/sdk/python/src/agent_mesh/session.py +++ b/sdk/python/src/agent_mesh/session.py @@ -48,7 +48,7 @@ from .biscuit import ROLE_ROUTER, VerifiedBiscuit, attenuate_biscuit, require_role, seal_biscuit from .controlplane import ROLE_NODE from .credential import encode_auth_frame -from .discovery import DiscoveredProvider, find_peer, find_providers, parse_service_target, service_key +from .discovery import DiscoveredProvider, find_peer, find_providers, parse_service_target, peer_key, provide, service_key, withdraw from .host import create_mesh_host, dial, dial_addrs, peer_info from .identity import canonical_peer_id from .libp2p_http import ( @@ -94,6 +94,11 @@ DEFAULT_CONTROL_PLANE_SYNC = 15 * 60.0 FIRST_CONTROL_PLANE_SYNC = 2.0 DEFAULT_CONTROL_PLANE_SYNC_JITTER = 2.0 +# How often a member announces where it is reached: agentmesh-node's default +# reprovide interval, a third of the mesh's default record TTL. +DEFAULT_ANNOUNCE_INTERVAL = 5 * 60.0 +# What leaving the mesh waits for the member's record to be withdrawn. +WITHDRAW_TIMEOUT = 2.0 # How long a provider's positive egress verdict is kept; agentmesh-node's labelGateTTL. EGRESS_VERDICT_TTL = 5 * 60.0 @@ -318,7 +323,10 @@ class MeshSession: reservation_lead: float = RESERVATION_RENEW_LEAD reservation_retry: float = DEFAULT_REFRESH_RETRY reservation_check_interval: float = RESERVATION_CHECK_INTERVAL + announce_interval: float = DEFAULT_ANNOUNCE_INTERVAL _held_router_ids: set[str] = field(default_factory=set, repr=False) + # The addresses of the record last announced under this member's peer key. + _announced: list[str] = field(default_factory=list, repr=False) _relay_lock: trio.Lock = field(default_factory=trio.Lock, repr=False) _relay_trigger: trio.Event = field(default_factory=trio.Event, repr=False) _nursery: Optional[trio.Nursery] = field(default=None, repr=False) @@ -401,7 +409,14 @@ async def handle_go_away(self, peer_id: str, msg: pb.RouterGoAway, retry_after: async def top_up_routers(self) -> list[str]: """Attaches and reserves on additional routers from the catalog until this - session holds _want_routers() routers or no more eligible candidates remain.""" + session holds _want_routers() routers or no more eligible candidates remain. + One top-up at a time: the reservation loop and a go-away both call it, + and two picking the same router at once would hold it twice, or shun + it while holding it.""" + async with self._relay_lock: + return await self._top_up_routers() + + async def _top_up_routers(self) -> list[str]: failures: list[str] = [] want = self._want_routers() if len(self._held_router_ids) >= want: @@ -462,6 +477,50 @@ def agent_url(self) -> Optional[str]: """The mesh URL of this member's own agent, once accept_a2a was called.""" return self.mesh_url(self.peer_id, self.endpoint.service) if self.endpoint is not None else None + async def announce(self) -> None: + """Announces where this member is reached, its relayed addresses on the + routers it holds, under its peer key, so that a peer attached to other + routers finds it by ID. Runs after join, when the addresses change and + every announce_interval; the record outlives its last announcement by + the mesh's record TTL. A member with no relayed address announces + nothing: there is nothing to reach it at.""" + if self._task_biscuit is not None: + return + addrs = sorted(self.relay_addresses) + if not addrs: + return + await provide(self.host, self._directory_routers(), peer_key(self.host.get_id()), [multiaddr.Multiaddr(a) for a in addrs]) + self._announced = addrs + + async def _announce_loop(self) -> None: + while True: + await trio.sleep(self.announce_interval) + try: + await self.announce() + except Exception as err: # noqa: BLE001 - the next announcement retries + logger.warning("announcing where this member is reached: %s", err) + + async def _announce_if_moved(self) -> None: + """Announces again when the relayed addresses changed since the last + announcement, as after a router was replaced.""" + if self.relay_addresses and sorted(self.relay_addresses) != self._announced: + try: + await self.announce() + except Exception as err: # noqa: BLE001 + logger.warning("announcing where this member is reached: %s", err) + + async def _withdraw_self(self) -> None: + """Withdraws this member's record as it leaves; best effort, the record + expires on its own within the record TTL.""" + if not self._announced or self._task_biscuit is not None: + return + with trio.move_on_after(WITHDRAW_TIMEOUT): + try: + await withdraw(self.host, self._directory_routers(), peer_key(self.host.get_id())) + except Exception as err: # noqa: BLE001 + logger.debug("withdrawing this member's record: %s", err) + self._announced = [] + @property def relay_addresses(self) -> list[str]: """The `.../p2p-circuit/p2p/` addresses reserved on routers.""" @@ -601,8 +660,12 @@ async def _routed_addresses(self, target: ID) -> tuple[list[multiaddr.Multiaddr] return direct, relays def _directory_routers(self) -> list[ID]: - """The routers this member holds, which answer its directory requests.""" - return [ID.from_base58(r.peer_id) for r in self.routers] + """The routers this member holds and is connected to, which answer its + directory requests. A request goes over the session the handshake + opened, never over a dial of its own: a router admits a connection it + has verified, and the reservation loop is what reconnects.""" + connected = set(self.host.get_connected_peers()) + return [ID.from_base58(r.peer_id) for r in self.routers if ID.from_base58(r.peer_id) in connected] async def _admit_router(self, addr: multiaddr.Multiaddr) -> AdmittedRouter: """Dials a router, runs the handshake and, its role verified, adds it @@ -891,6 +954,7 @@ async def _reservation_loop(self) -> None: if len(self._held_router_ids) < self._want_routers(): await self.top_up_routers() + await self._announce_if_moved() if failed: await trio.sleep(self.reservation_retry) @@ -1014,6 +1078,7 @@ async def join_mesh( refresh_retry: float = DEFAULT_REFRESH_RETRY, reservation_lead: float = RESERVATION_RENEW_LEAD, reservation_check_interval: float = RESERVATION_CHECK_INTERVAL, + announce_interval: float = DEFAULT_ANNOUNCE_INTERVAL, policy_sync_interval: float = DEFAULT_POLICY_SYNC, control_plane_sync_interval: float = DEFAULT_CONTROL_PLANE_SYNC, control_plane_sync_jitter: float = DEFAULT_CONTROL_PLANE_SYNC_JITTER, @@ -1111,6 +1176,7 @@ async def join_mesh( reservation_lead=reservation_lead, reservation_retry=refresh_retry, reservation_check_interval=reservation_check_interval, + announce_interval=announce_interval, _nursery=nursery, ) session_ref.append(session) @@ -1118,9 +1184,15 @@ async def join_mesh( nursery.start_soon(session._events_loop, pubsub) # noqa: SLF001 nursery.start_soon(session._sync_loop) # noqa: SLF001 nursery.start_soon(session._reservation_loop) # noqa: SLF001 + nursery.start_soon(session._announce_loop) # noqa: SLF001 + # Best effort: a router that does not take the record is a + # warning the next announcement retries. + await session._announce_if_moved() # noqa: SLF001 try: yield session finally: + with trio.CancelScope(shield=True): + await session._withdraw_self() # noqa: SLF001 nursery.cancel_scope.cancel() except BaseExceptionGroup as group: cause = _single_cause(group) diff --git a/sdk/python/tests/test_session.py b/sdk/python/tests/test_session.py index c762e642..e767d5ca 100644 --- a/sdk/python/tests/test_session.py +++ b/sdk/python/tests/test_session.py @@ -52,7 +52,7 @@ from agent_mesh.authorizer import ProviderAuthorizerOptions from agent_mesh.biscuit import ROLE_ROUTER, BiscuitVerificationError from agent_mesh.controlplane import ROLE_NODE -from agent_mesh.discovery import DiscoveredProvider +from agent_mesh.discovery import DIRECTORY_PROTOCOL, DiscoveredProvider, peer_key from agent_mesh.httpx_transport import MeshTransport from agent_mesh.identity import Identity from agent_mesh.libp2p_http import HTTP_PROTOCOL, A2AEndpoint, HTTPResponse, ProviderOptions, http_ingress_handler @@ -1037,3 +1037,42 @@ async def main(): trio.run(with_timeout, 60, main) + + +def test_a_member_announces_where_it_is_reached_and_withdraws_it_on_leaving(): + """A member with a relay reservation announces its relayed addresses under + its peer key to a router it holds, so that a peer on other routers finds + it by ID; it withdraws the record as it leaves.""" + requests = [] + + def directory_handler(stream): + async def run(): + req = pb.DirectoryRequest.FromString(await read_varint_prefixed_bytes(stream)) + requests.append((req.WhichOneof("request"), req)) + await stream.write(encode_varint_prefixed(pb.DirectoryResponse().SerializeToString())) + await stream.close() + + return run() + + async def main(): + async with trio.open_nursery() as nursery: + router, router_addr = await start_router(nursery) + router.set_stream_handler(DIRECTORY_PROTOCOL, directory_handler) + mesh = AgentMesh.enroll("http://127.0.0.1:1", bootstrap_token="sbt", transport=fake_control_plane([router_addr])) + async with mesh.join(listen_addrs=["/ip4/127.0.0.1/tcp/0"], refresh_lead=0) as session: + relayed = f"{router_addr}/p2p-circuit/p2p/{mesh.peer_id}" + assert session.relay_addresses == [relayed] + assert [op for op, _ in requests] == ["provide"] + assert requests[0][1].provide.key == peer_key(session.host.get_id()) + assert list(requests[0][1].provide.addrs) == [relayed] + assert [op for op, _ in requests] == ["provide", "withdraw"] + assert requests[1][1].withdraw.key == peer_key(session.host.get_id()) + + # A member that reserves no relay has no address to announce. + del requests[:] + async with mesh.join(reserve=False) as session: + assert session.relay_addresses == [] + assert requests == [] + nursery.cancel_scope.cancel() + + trio.run(with_timeout, 30, main) From c0bb3efa2451863750acbd423136a03e864d432e Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Wed, 14 Oct 2026 09:24:33 +0000 Subject: [PATCH 4/7] directory: a record names its service, and the router checks the key is its A member could announce any key: a record under a key it had no business with, a service name it does not serve, another member's peer key. The router stored whatever multihash came with addresses of the sender. DirectoryProvide now names the service a record is about, "://", and the member's router refuses a key that is not that service's by name or by type (api.ServiceOfRecord); a record naming no service must be the sender's own peer key. A placed copy is not checked again: the router that holds the member did. This is also what a publish grant will be checked against: the service by name, not a hash. The node's Directory.Provide takes the service; the SDKs announce only their peer key, which names none. --- api/agentmesh.pb.go | 22 +- api/agentmesh.proto | 10 +- api/directory_keys.go | 30 ++ api/directory_keys_test.go | 38 +++ internal/node/directory_client.go | 8 +- internal/node/directory_client_test.go | 2 +- internal/node/node.go | 2 +- internal/node/service_registry.go | 17 +- internal/node/service_registry_test.go | 2 +- internal/router/directory.go | 9 +- internal/router/directory_test.go | 38 ++- sdk/js/src/gen/agentmesh_pb.ts | 16 +- .../src/agent_mesh/_proto/agentmesh_pb2.py | 264 +++++++++--------- .../src/agent_mesh/_proto/agentmesh_pb2.pyi | 6 +- 14 files changed, 292 insertions(+), 172 deletions(-) diff --git a/api/agentmesh.pb.go b/api/agentmesh.pb.go index 91ddad18..04c52851 100644 --- a/api/agentmesh.pb.go +++ b/api/agentmesh.pb.go @@ -1643,14 +1643,18 @@ func (*DirectoryRequest_Withdraw) isDirectoryRequest_Request() {} // addrs. A key is a sha256 multihash: of "mesh:service:[:]" for // a service, of "mesh:peer:" for the member itself. A record lives // for the mesh's record TTL after its last announcement; announcing it -// again replaces its addresses. provider is set only between routers, by -// the router that holds the member, and refused from anyone else: a member -// can only announce itself. +// again replaces its addresses. service names the service a record is +// about, "://", and the router refuses a key that is not that +// service's by name or by type; a record naming no service is the +// member's own, under its peer key. provider is set only between routers, +// by the router that holds the member, and refused from anyone else: a +// member can only announce itself. type DirectoryProvide struct { state protoimpl.MessageState `protogen:"open.v1"` Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` Addrs []string `protobuf:"bytes,2,rep,name=addrs,proto3" json:"addrs,omitempty"` Provider string `protobuf:"bytes,3,opt,name=provider,proto3" json:"provider,omitempty"` + Service string `protobuf:"bytes,4,opt,name=service,proto3" json:"service,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1706,6 +1710,13 @@ func (x *DirectoryProvide) GetProvider() string { return "" } +func (x *DirectoryProvide) GetService() string { + if x != nil { + return x.Service + } + return "" +} + // DirectoryWithdraw removes the sender's record under key, from the router // asked and from the routers closest to the key: what a member sends when // it stops a service or leaves the mesh, so that callers do not find it @@ -6068,11 +6079,12 @@ const file_api_agentmesh_proto_rawDesc = "" + "\x0efind_providers\x18\x02 \x01(\v2$.agentmesh.v1.DirectoryFindProvidersH\x00R\rfindProviders\x12>\n" + "\tfind_peer\x18\x03 \x01(\v2\x1f.agentmesh.v1.DirectoryFindPeerH\x00R\bfindPeer\x12=\n" + "\bwithdraw\x18\x04 \x01(\v2\x1f.agentmesh.v1.DirectoryWithdrawH\x00R\bwithdrawB\t\n" + - "\arequest\"V\n" + + "\arequest\"p\n" + "\x10DirectoryProvide\x12\x10\n" + "\x03key\x18\x01 \x01(\fR\x03key\x12\x14\n" + "\x05addrs\x18\x02 \x03(\tR\x05addrs\x12\x1a\n" + - "\bprovider\x18\x03 \x01(\tR\bprovider\"A\n" + + "\bprovider\x18\x03 \x01(\tR\bprovider\x12\x18\n" + + "\aservice\x18\x04 \x01(\tR\aservice\"A\n" + "\x11DirectoryWithdraw\x12\x10\n" + "\x03key\x18\x01 \x01(\fR\x03key\x12\x1a\n" + "\bprovider\x18\x02 \x01(\tR\bprovider\"@\n" + diff --git a/api/agentmesh.proto b/api/agentmesh.proto index c855a0b0..192a1fbb 100644 --- a/api/agentmesh.proto +++ b/api/agentmesh.proto @@ -240,13 +240,17 @@ message DirectoryRequest { // addrs. A key is a sha256 multihash: of "mesh:service:[:]" for // a service, of "mesh:peer:" for the member itself. A record lives // for the mesh's record TTL after its last announcement; announcing it -// again replaces its addresses. provider is set only between routers, by -// the router that holds the member, and refused from anyone else: a member -// can only announce itself. +// again replaces its addresses. service names the service a record is +// about, "://", and the router refuses a key that is not that +// service's by name or by type; a record naming no service is the +// member's own, under its peer key. provider is set only between routers, +// by the router that holds the member, and refused from anyone else: a +// member can only announce itself. message DirectoryProvide { bytes key = 1; repeated string addrs = 2; string provider = 3; + string service = 4; } // DirectoryWithdraw removes the sender's record under key, from the router diff --git a/api/directory_keys.go b/api/directory_keys.go index ac923c53..9a97b40c 100644 --- a/api/directory_keys.go +++ b/api/directory_keys.go @@ -15,6 +15,7 @@ package api import ( + "bytes" "crypto/sha256" "fmt" "strings" @@ -71,3 +72,32 @@ func ValidateDirectoryKey(key []byte) error { } return nil } + +// ServiceOfRecord parses the service a record names, "://", +// and reports whether key is one of that service's: by name or by type. +// A record that names no service is the provider's own, under PeerKey. +// This is what a router checks before it keeps a member's record, so that +// a member announces itself, and the services it names, and nothing else. +func ServiceOfRecord(provider peer.ID, key []byte, service string) (ServiceType, string, error) { + if service == "" { + if !bytes.Equal(key, PeerKey(provider)) { + return ServiceType_SERVICE_TYPE_UNSPECIFIED, "", fmt.Errorf("a record naming no service is the provider's own, under its peer key") + } + return ServiceType_SERVICE_TYPE_UNSPECIFIED, "", nil + } + typeStr, name := ParseServiceTarget(service) + t, err := ParseServiceType(typeStr) + if err != nil || t == ServiceType_SERVICE_TYPE_UNSPECIFIED || name == "" || name == "*" { + return ServiceType_SERVICE_TYPE_UNSPECIFIED, "", fmt.Errorf("service %q must be ://", service) + } + for _, n := range []string{name, ""} { + k, err := ServiceKey(t, n) + if err != nil { + return ServiceType_SERVICE_TYPE_UNSPECIFIED, "", err + } + if bytes.Equal(key, k) { + return t, name, nil + } + } + return ServiceType_SERVICE_TYPE_UNSPECIFIED, "", fmt.Errorf("key is not %s's, by name or by type", service) +} diff --git a/api/directory_keys_test.go b/api/directory_keys_test.go index 4dd78499..6c6e3e26 100644 --- a/api/directory_keys_test.go +++ b/api/directory_keys_test.go @@ -68,3 +68,41 @@ func TestDirectoryKeys(t *testing.T) { } } } + +// A record is the provider's own, under its peer key, or a service's, by +// name or by type, and the key must be that of the service it names. +func TestServiceOfRecord(t *testing.T) { + provider := peer.ID("12D3KooWprovider") + stranger := peer.ID("12D3KooWstranger") + calc, _ := ServiceKey(ServiceType_SERVICE_TYPE_MCP, "calc") + mcp, _ := ServiceKey(ServiceType_SERVICE_TYPE_MCP, "") + for name, tc := range map[string]struct { + key []byte + service string + wantErr bool + wantTyp ServiceType + }{ + "own peer key": {key: PeerKey(provider), service: ""}, + "service by name": {key: calc, service: "mcp://calc", wantTyp: ServiceType_SERVICE_TYPE_MCP}, + "service by type": {key: mcp, service: "mcp://calc", wantTyp: ServiceType_SERVICE_TYPE_MCP}, + "another peer's key": {key: PeerKey(stranger), service: "", wantErr: true}, + "service key naming no service": {key: calc, service: "", wantErr: true}, + "peer key naming a service": {key: PeerKey(provider), service: "mcp://calc", wantErr: true}, + "key of another service": {key: calc, service: "mcp://weather", wantErr: true}, + "key of another type": {key: calc, service: "inference://calc", wantErr: true}, + "service without a name": {key: mcp, service: "mcp://", wantErr: true}, + "service with a wildcard name": {key: mcp, service: "mcp://*", wantErr: true}, + "service of an unknown type": {key: calc, service: "ftp://calc", wantErr: true}, + "service that is not a target": {key: calc, service: "calc", wantErr: true}, + } { + t.Run(name, func(t *testing.T) { + typ, svcName, err := ServiceOfRecord(provider, tc.key, tc.service) + if (err != nil) != tc.wantErr { + t.Fatalf("ServiceOfRecord(%q) err = %v, wantErr %v", tc.service, err, tc.wantErr) + } + if err == nil && tc.service != "" && (typ != tc.wantTyp || svcName != "calc") { + t.Fatalf("ServiceOfRecord(%q) = %v, %q; want %v, calc", tc.service, typ, svcName, tc.wantTyp) + } + }) + } +} diff --git a/internal/node/directory_client.go b/internal/node/directory_client.go index b4899a0e..b2358726 100644 --- a/internal/node/directory_client.go +++ b/internal/node/directory_client.go @@ -43,7 +43,9 @@ type Directory interface { // Provide announces this member as a provider of key, at the // addresses it announces, to a router it is attached to; the router // keeps the record and places it on the routers closest to the key. - Provide(ctx context.Context, key []byte) error + // service is what the record is about, "://", and "" for + // the member's own record under its peer key. + Provide(ctx context.Context, key []byte, service string) error // Withdraw removes this member's record under key from the router it is // attached to and from the routers the record was placed on. Withdraw(ctx context.Context, key []byte) error @@ -112,12 +114,12 @@ type routerDirectory struct { announced []multiaddr.Multiaddr // the addresses of the record last announced } -func (d *routerDirectory) Provide(ctx context.Context, key []byte) error { +func (d *routerDirectory) Provide(ctx context.Context, key []byte, service string) error { addrs := d.addrs() if len(addrs) > api.DirectoryMaxAddrs { addrs = addrs[:api.DirectoryMaxAddrs] } - rec := &api.DirectoryProvide{Key: key, Addrs: make([]string, 0, len(addrs))} + rec := &api.DirectoryProvide{Key: key, Service: service, Addrs: make([]string, 0, len(addrs))} for _, a := range addrs { rec.Addrs = append(rec.Addrs, a.String()) } diff --git a/internal/node/directory_client_test.go b/internal/node/directory_client_test.go index e175256d..e73b23a6 100644 --- a/internal/node/directory_client_test.go +++ b/internal/node/directory_client_test.go @@ -192,7 +192,7 @@ func TestRouterDirectoryAsksTheAttachedRouters(t *testing.T) { t.Fatal(err) } - if err := directory.Provide(ctx, key); err != nil { + if err := directory.Provide(ctx, key, "mcp://calculator"); err != nil { t.Fatalf("Provide: %v", err) } if got := dir1.providers(key); len(got) != 1 || got[0] != member.ID() { diff --git a/internal/node/node.go b/internal/node/node.go index 9e89ed6a..6a1210a5 100644 --- a/internal/node/node.go +++ b/internal/node/node.go @@ -808,7 +808,7 @@ func (n *AgentMeshNode) announceAll(ctx context.Context) int { withheld := n.services.ReprovideAll(ctx) provideCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() - if err := n.Directory.Provide(provideCtx, api.PeerKey(n.Host.ID())); err != nil && ctx.Err() == nil && !errors.Is(err, errNoDirectoryRouter) { + if err := n.Directory.Provide(provideCtx, api.PeerKey(n.Host.ID()), ""); err != nil && ctx.Err() == nil && !errors.Is(err, errNoDirectoryRouter) { // With no router attached the directory client has said so already. logger.Warnf("[Directory] Announcing where this node is reached: %v", err) } diff --git a/internal/node/service_registry.go b/internal/node/service_registry.go index 07e2d90a..78d74097 100644 --- a/internal/node/service_registry.go +++ b/internal/node/service_registry.go @@ -29,7 +29,7 @@ import ( // announce a key and to withdraw it. Production wires this to the node's // Directory; tests use a fake. type directoryProvider interface { - Provide(ctx context.Context, key []byte) error + Provide(ctx context.Context, key []byte, service string) error Withdraw(ctx context.Context, key []byte) error } @@ -156,13 +156,13 @@ func (r *ServiceRegistry) Register(ctx context.Context, svc Service) error { } else { nameCtx, nameCancel := context.WithTimeout(ctx, 5*time.Second) defer nameCancel() - if err := r.dht.Provide(nameCtx, nameKey); err != nil { + if err := r.dht.Provide(nameCtx, nameKey, serviceTarget(info)); err != nil { logger.Warnf("[ServiceRegistry] Announcing %s by name: %v", info.Name, err) } typeCtx, typeCancel := context.WithTimeout(ctx, 5*time.Second) defer typeCancel() - if err := r.dht.Provide(typeCtx, typeKey); err != nil { + if err := r.dht.Provide(typeCtx, typeKey, serviceTarget(info)); err != nil { logger.Warnf("[ServiceRegistry] Announcing %s by type: %v", info.Name, err) } } @@ -218,6 +218,15 @@ func (r *ServiceRegistry) Unregister(ctx context.Context, name string) error { return nil } +// serviceTarget is a service as a record names it, "://". +func serviceTarget(info *api.ServiceInfo) string { + typeStr, err := api.ServiceTypeToString(info.GetType()) + if err != nil { + return "" + } + return typeStr + "://" + info.GetName() +} + // withdraw removes one of the registry's records from the directory. A // failure is logged: the record expires on its own within the mesh's // record TTL. @@ -359,7 +368,7 @@ Loop: continue } provideCtx, cancel := context.WithTimeout(ctx, 5*time.Second) - if err := r.dht.Provide(provideCtx, key); err != nil && ctx.Err() == nil { + if err := r.dht.Provide(provideCtx, key, serviceTarget(info)); err != nil && ctx.Err() == nil { logger.Warnf("[ServiceRegistry] Announcing %s/%s again: %v", info.Type, info.Name, err) } cancel() diff --git a/internal/node/service_registry_test.go b/internal/node/service_registry_test.go index 01239131..c07bb46a 100644 --- a/internal/node/service_registry_test.go +++ b/internal/node/service_registry_test.go @@ -34,7 +34,7 @@ type fakeDHT struct { count int32 } -func (f *fakeDHT) Provide(ctx context.Context, key []byte) error { +func (f *fakeDHT) Provide(ctx context.Context, key []byte, service string) error { n := atomic.AddInt32(&f.count, 1) f.calls = append(f.calls, key) if f.failNth > 0 && int(n) == f.failNth { diff --git a/internal/router/directory.go b/internal/router/directory.go index 10d14bd0..0417655e 100644 --- a/internal/router/directory.go +++ b/internal/router/directory.go @@ -258,13 +258,20 @@ func (r *Router) directoryProvide(ctx context.Context, remote peer.ID, q *api.Di if err != nil { return nil, invalidDirectoryRequest("%w", err) } + if !placed { + // The member's own router checks what the record is about; a placed + // copy was checked by the router that holds the member. + if _, _, err := api.ServiceOfRecord(provider, q.GetKey(), q.GetService()); err != nil { + return nil, invalidDirectoryRequest("%w", err) + } + } if r.records == nil { return nil, errDirectoryNotUp } r.records.put(q.GetKey(), provider, addrs, time.Now()) if !placed { r.placeRecord(ctx, provider, &api.DirectoryRequest{Request: &api.DirectoryRequest_Provide{Provide: &api.DirectoryProvide{ - Key: q.GetKey(), Addrs: q.GetAddrs(), Provider: provider.String(), + Key: q.GetKey(), Addrs: q.GetAddrs(), Provider: provider.String(), Service: q.GetService(), }}}) } return &api.DirectoryResponse{}, nil diff --git a/internal/router/directory_test.go b/internal/router/directory_test.go index 530a1258..7c32c0fc 100644 --- a/internal/router/directory_test.go +++ b/internal/router/directory_test.go @@ -104,8 +104,10 @@ func ask(t *testing.T, from host.Host, router peer.ID, req *api.DirectoryRequest return &resp } -func provide(key []byte, addrs ...string) *api.DirectoryRequest { - return &api.DirectoryRequest{Request: &api.DirectoryRequest_Provide{Provide: &api.DirectoryProvide{Key: key, Addrs: addrs}}} +// provide is a member's record for service under key; a service of "" +// is the member's own record under its peer key. +func provide(key []byte, service string, addrs ...string) *api.DirectoryRequest { + return &api.DirectoryRequest{Request: &api.DirectoryRequest_Provide{Provide: &api.DirectoryProvide{Key: key, Service: service, Addrs: addrs}}} } func findProviders(key []byte) *api.DirectoryRequest { @@ -145,10 +147,10 @@ func TestDirectoryRecordsAreFoundFromAnyRouter(t *testing.T) { t.Fatal(err) } memberAddr := member.Addrs()[0].String() + "/p2p/" + member.ID().String() - if resp := ask(t, member, a.Host.ID(), provide(key, memberAddr)); resp.GetError() != "" { + if resp := ask(t, member, a.Host.ID(), provide(key, "mcp://calculator", memberAddr)); resp.GetError() != "" { t.Fatalf("provide refused: %s", resp.GetError()) } - if resp := ask(t, member, a.Host.ID(), provide(api.PeerKey(member.ID()), memberAddr)); resp.GetError() != "" { + if resp := ask(t, member, a.Host.ID(), provide(api.PeerKey(member.ID()), "", memberAddr)); resp.GetError() != "" { t.Fatalf("provide of the peer key refused: %s", resp.GetError()) } @@ -219,9 +221,11 @@ func TestDirectoryRecordsAreFoundFromAnyRouter(t *testing.T) { }) } -// A member can only announce itself: a record naming another provider, an -// address of another peer, or a key that is not a sha256 multihash is -// refused and counted as invalid, and nothing is stored. +// A member can only announce itself and the services it names: a record +// naming another provider, an address of another peer, a key that is not +// a sha256 multihash, or a key that is not the named service's (nor the +// member's own peer key when no service is named) is refused and counted +// as invalid, and nothing is stored. func TestDirectoryRefusesWhatAMemberMayNotAnnounce(t *testing.T) { a := newDirectoryRouter(t) member := attach(t, a) @@ -229,11 +233,15 @@ func TestDirectoryRefusesWhatAMemberMayNotAnnounce(t *testing.T) { key, _ := api.ServiceKey(api.ServiceType_SERVICE_TYPE_MCP, "calculator") for name, req := range map[string]*api.DirectoryRequest{ - "another provider": {Request: &api.DirectoryRequest_Provide{Provide: &api.DirectoryProvide{Key: key, Provider: stranger.ID().String()}}}, - "another peer's address": provide(key, stranger.Addrs()[0].String()+"/p2p/"+stranger.ID().String()), - "a key that is not a multihash": provide([]byte("calculator")), - "a lookup of such a key": findProviders([]byte("calculator")), - "a peer id that is not one": findPeer(peer.ID("nobody")), + "another provider": {Request: &api.DirectoryRequest_Provide{Provide: &api.DirectoryProvide{Key: key, Provider: stranger.ID().String()}}}, + "another peer's address": provide(key, "mcp://calculator", stranger.Addrs()[0].String()+"/p2p/"+stranger.ID().String()), + "a key that is not a multihash": provide([]byte("calculator"), "mcp://calculator"), + "a key that is not the named service's": provide(key, "mcp://weather"), + "a service key naming no service": provide(key, ""), + "a peer key naming a service": provide(api.PeerKey(member.ID()), "mcp://calculator"), + "another peer's key": provide(api.PeerKey(stranger.ID()), ""), + "a lookup of such a key": findProviders([]byte("calculator")), + "a peer id that is not one": findPeer(peer.ID("nobody")), } { t.Run(name, func(t *testing.T) { op := directoryOpProvide @@ -270,7 +278,7 @@ func TestDirectoryPlacementNamesTheProvider(t *testing.T) { member := attach(t, a) key, _ := api.ServiceKey(api.ServiceType_SERVICE_TYPE_INFERENCE, "") memberAddr := member.Addrs()[0].String() + "/p2p/" + member.ID().String() - if resp := ask(t, member, a.Host.ID(), provide(key, memberAddr)); resp.GetError() != "" { + if resp := ask(t, member, a.Host.ID(), provide(key, "inference://llm", memberAddr)); resp.GetError() != "" { t.Fatalf("provide refused: %s", resp.GetError()) } provs := held(b, key) @@ -299,10 +307,10 @@ func TestDirectoryWithdrawAndReplace(t *testing.T) { oldAddr := "/ip4/10.0.0.1/tcp/4001/p2p/" + member.ID().String() newAddr := "/ip4/10.0.0.2/tcp/4001/p2p/" + member.ID().String() - if resp := ask(t, member, a.Host.ID(), provide(key, oldAddr)); resp.GetError() != "" { + if resp := ask(t, member, a.Host.ID(), provide(key, "mcp://calculator", oldAddr)); resp.GetError() != "" { t.Fatalf("provide refused: %s", resp.GetError()) } - if resp := ask(t, member, a.Host.ID(), provide(key, newAddr)); resp.GetError() != "" { + if resp := ask(t, member, a.Host.ID(), provide(key, "mcp://calculator", newAddr)); resp.GetError() != "" { t.Fatalf("second provide refused: %s", resp.GetError()) } for name, router := range map[string]*Router{"own router": a, "placed on": b} { diff --git a/sdk/js/src/gen/agentmesh_pb.ts b/sdk/js/src/gen/agentmesh_pb.ts index c2f5a251..f5624249 100644 --- a/sdk/js/src/gen/agentmesh_pb.ts +++ b/sdk/js/src/gen/agentmesh_pb.ts @@ -26,7 +26,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file agentmesh.proto. */ export const file_agentmesh: GenFile = /*@__PURE__*/ - fileDesc("Cg9hZ2VudG1lc2gucHJvdG8SDGFnZW50bWVzaC52MSI0CglBdXRoRnJhbWUSDwoHYmlzY3VpdBgBIAEoDBIWCg50YXJnZXRfc2VydmljZRgCIAEoCSI/CgxBdXRoUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCBINCgVlcnJvchgCIAEoCRIPCgdiaXNjdWl0GAMgASgMIrEBCgxSb3V0ZXJHb0F3YXkSMQoGcmVhc29uGAEgASgOMiEuYWdlbnRtZXNoLnYxLlJvdXRlckdvQXdheS5SZWFzb24SLgoLcmV0cnlfYWZ0ZXIYAiABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24iPgoGUmVhc29uEhYKElJFQVNPTl9VTlNQRUNJRklFRBAAEgwKCERSQUlOSU5HEAESDgoKT1ZFUkxPQURFRBACItwBCglNZXNoRXZlbnQSKgoEdHlwZRgBIAEoDjIcLmFnZW50bWVzaC52MS5NZXNoRXZlbnQuVHlwZRIPCgdwZWVyX2lkGAIgASgJEi4KCmV2ZW50X3RpbWUYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhYKDm5ld19wdWJsaWNfa2V5GAQgASgMEhEKCXNpZ25hdHVyZRgFIAEoDCI3CgRUeXBlEgoKBkJBTk5FRBAAEhAKDEtFWV9ST1RBVElPThABEhEKDVBPTElDWV9VUERBVEUQAiL5AQoNRW5yb2xsUmVxdWVzdBILCgNqd3QYASABKAkSDwoHcGVlcl9pZBgCIAEoCRISCgpwdWJsaWNfa2V5GAMgASgMEhYKDnJlcXVlc3RlZF9yb2xlGAQgASgJEjcKBmxhYmVscxgFIAMoCzInLmFnZW50bWVzaC52MS5FbnJvbGxSZXF1ZXN0LkxhYmVsc0VudHJ5EhkKEWNoYWxsZW5nZV91bml4X21zGAYgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYByABKAwaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKrAQoORW5yb2xsUmVzcG9uc2USFQoNYmlzY3VpdF90b2tlbhgBIAEoDBIVCg1lcnJvcl9tZXNzYWdlGAIgASgJEiAKGGNvbnRyb2xfcGxhbmVfcHVibGljX2tleRgDIAEoDBIYChByb3V0ZXJfYWRkcmVzc2VzGAQgAygJEi8KC2V4cGlyZV90aW1lGAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCKXAgoWQm9vdHN0cmFwRW5yb2xsUmVxdWVzdBIXCg9ib290c3RyYXBfdG9rZW4YASABKAkSDwoHcGVlcl9pZBgCIAEoCRISCgpwdWJsaWNfa2V5GAMgASgMEhYKDnJlcXVlc3RlZF9yb2xlGAQgASgJEkAKBmxhYmVscxgFIAMoCzIwLmFnZW50bWVzaC52MS5Cb290c3RyYXBFbnJvbGxSZXF1ZXN0LkxhYmVsc0VudHJ5EhkKEWNoYWxsZW5nZV91bml4X21zGAYgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYByABKAwaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKDAgoXQm9vdHN0cmFwRW5yb2xsUmVzcG9uc2USLgoGc3RhdHVzGAEgASgOMh4uYWdlbnRtZXNoLnYxLkVucm9sbG1lbnRTdGF0dXMSFQoNYmlzY3VpdF90b2tlbhgCIAEoDBIdChVwb2xsX2ludGVydmFsX3NlY29uZHMYAyABKAUSFQoNZXJyb3JfbWVzc2FnZRgEIAEoCRIgChhjb250cm9sX3BsYW5lX3B1YmxpY19rZXkYBSABKAwSGAoQcm91dGVyX2FkZHJlc3NlcxgGIAMoCRIvCgtleHBpcmVfdGltZRgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiWQoLU2VydmljZUluZm8SJwoEdHlwZRgBIAEoDjIZLmFnZW50bWVzaC52MS5TZXJ2aWNlVHlwZRIMCgRuYW1lGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJIoEBCg5Db21tYW5kQmFja2VuZBIPCgdjb21tYW5kGAEgAygJEjIKA2VudhgCIAMoCzIlLmFnZW50bWVzaC52MS5Db21tYW5kQmFja2VuZC5FbnZFbnRyeRoqCghFbnZFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIpYBChZSZWdpc3RlclNlcnZpY2VSZXF1ZXN0EioKB3NlcnZpY2UYASABKAsyGS5hZ2VudG1lc2gudjEuU2VydmljZUluZm8SFAoKdGFyZ2V0X3VybBgCIAEoCUgAEi8KB2NvbW1hbmQYAyABKAsyHC5hZ2VudG1lc2gudjEuQ29tbWFuZEJhY2tlbmRIAEIJCgdiYWNrZW5kImkKEkRpc2NvdmVyZWRQcm92aWRlchIPCgdwZWVyX2lkGAEgASgJEhcKD2xvY2FsX3Byb3h5X3VybBgCIAEoCRIQCghzcnZfbmFtZRgDIAEoCRIXCg9zcnZfZGVzY3JpcHRpb24YBCABKAki6AEKGENvbnRyb2xQbGFuZUluZm9SZXNwb25zZRITCgtvaWRjX2lzc3VlchgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSEAoIYXVkaWVuY2UYAyABKAkSGAoQcm91dGVyX2FkZHJlc3NlcxgEIAMoCRIXCg9iYW5uZWRfcGVlcl9pZHMYBSADKAkSKQoHcm91dGVycxgGIAMoCzIYLmFnZW50bWVzaC52MS5Sb3V0ZXJJbmZvEjQKCWRpcmVjdG9yeRgHIAEoCzIhLmFnZW50bWVzaC52MS5EaXJlY3RvcnlQYXJhbWV0ZXJzIpABChNEaXJlY3RvcnlQYXJhbWV0ZXJzEhMKC3JlcGxpY2F0aW9uGAEgASgFEjUKEnJlcHJvdmlkZV9pbnRlcnZhbBgCIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbhItCgpyZWNvcmRfdHRsGAMgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uIvsBChBEaXJlY3RvcnlSZXF1ZXN0EjEKB3Byb3ZpZGUYASABKAsyHi5hZ2VudG1lc2gudjEuRGlyZWN0b3J5UHJvdmlkZUgAEj4KDmZpbmRfcHJvdmlkZXJzGAIgASgLMiQuYWdlbnRtZXNoLnYxLkRpcmVjdG9yeUZpbmRQcm92aWRlcnNIABI0CglmaW5kX3BlZXIYAyABKAsyHy5hZ2VudG1lc2gudjEuRGlyZWN0b3J5RmluZFBlZXJIABIzCgh3aXRoZHJhdxgEIAEoCzIfLmFnZW50bWVzaC52MS5EaXJlY3RvcnlXaXRoZHJhd0gAQgkKB3JlcXVlc3QiQAoQRGlyZWN0b3J5UHJvdmlkZRILCgNrZXkYASABKAwSDQoFYWRkcnMYAiADKAkSEAoIcHJvdmlkZXIYAyABKAkiMgoRRGlyZWN0b3J5V2l0aGRyYXcSCwoDa2V5GAEgASgMEhAKCHByb3ZpZGVyGAIgASgJIjQKFkRpcmVjdG9yeUZpbmRQcm92aWRlcnMSCwoDa2V5GAEgASgMEg0KBWxpbWl0GAIgASgNIiQKEURpcmVjdG9yeUZpbmRQZWVyEg8KB3BlZXJfaWQYASABKAkidwoRRGlyZWN0b3J5UmVzcG9uc2USKwoJcHJvdmlkZXJzGAEgAygLMhguYWdlbnRtZXNoLnYxLlBlZXJSZWNvcmQSJgoEcGVlchgCIAEoCzIYLmFnZW50bWVzaC52MS5QZWVyUmVjb3JkEg0KBWVycm9yGAMgASgJIiwKClBlZXJSZWNvcmQSDwoHcGVlcl9pZBgBIAEoCRINCgVhZGRycxgCIAMoCSLEAQoKUm91dGVySW5mbxIPCgdwZWVyX2lkGAEgASgJEhEKCWFkZHJlc3NlcxgCIAMoCRI0CgZsYWJlbHMYAyADKAsyJC5hZ2VudG1lc2gudjEuUm91dGVySW5mby5MYWJlbHNFbnRyeRITCgtjb25uZWN0aW9ucxgEIAEoBRIYChBjb25uZWN0aW9uX2xpbWl0GAUgASgFGi0KC0xhYmVsc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEigwIKElJvdXRlckxlYXNlUmVxdWVzdBIPCgdwZWVyX2lkGAEgASgJEhEKCWFkZHJlc3NlcxgCIAMoCRIPCgdiaXNjdWl0GAMgASgMEhcKD2Nvbm5lY3RlZF9wZWVycxgEIAMoCRIQCghkaHRfc2l6ZRgFIAEoBRIZChFjaGFsbGVuZ2VfdW5peF9tcxgGIAEoAxIbChNjaGFsbGVuZ2Vfc2lnbmF0dXJlGAcgASgMEiYKA3R0bBgIIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbhITCgtjb25uZWN0aW9ucxgJIAEoBRIYChBjb25uZWN0aW9uX2xpbWl0GAogASgFImYKE1JvdXRlckxlYXNlUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCBINCgVlcnJvchgCIAEoCRIvCgtleHBpcmVfdGltZRgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAipAEKClBvbGljeVJvbGUSDAoEbmFtZRgBIAEoCRIXCg9hbGxvd2VkX3RhcmdldHMYAiADKAkSGAoQYWxsb3dlZF9zZXJ2aWNlcxgDIAMoCRIWCg5jdXN0b21fZGF0YWxvZxgEIAMoCRIWCg5hbGxvd2VkX2xhYmVscxgFIAMoCRIlCgRodHRwGAYgAygLMhcuYWdlbnRtZXNoLnYxLkhUVFBHcmFudCI8CglIVFRQR3JhbnQSDwoHc2VydmljZRgBIAEoCRIPCgdtZXRob2RzGAIgAygJEg0KBXBhdGhzGAMgAygJIqECChFFZ3Jlc3NEZXN0aW5hdGlvbhIMCgRuYW1lGAEgASgJEhIKCnRhcmdldF91cmwYAiABKAkSEgoKY3JlZGVudGlhbBgDIAEoCRIRCglzZXJ2ZWRfYnkYBCADKAkSLgoGYnJva2VyGAUgASgLMh4uYWdlbnRtZXNoLnYxLkNyZWRlbnRpYWxCcm9rZXISLAoKaW5zcGVjdGlvbhgGIAEoCzIYLmFnZW50bWVzaC52MS5JbnNwZWN0aW9uEiYKBG1vZGUYByABKA4yGC5hZ2VudG1lc2gudjEuRWdyZXNzTW9kZRINCgVwb3J0cxgIIAMoDRIVCg1wcmVzZXJ2ZV9ob3N0GAkgASgIEhcKD2ZvcndhcmRfY29udGV4dBgKIAEoCCI5CgpJbnNwZWN0aW9uEisKCmluc3BlY3RvcnMYASADKAsyFy5hZ2VudG1lc2gudjEuSW5zcGVjdG9yIm8KCUluc3BlY3RvchIvCgttb2RlbF9hcm1vchgBIAEoCzIYLmFnZW50bWVzaC52MS5Nb2RlbEFybW9ySAASKQoIZXh0X3Byb2MYAiABKAsyFS5hZ2VudG1lc2gudjEuRXh0UHJvY0gAQgYKBGtpbmQikQEKCk1vZGVsQXJtb3ISEAoIdGVtcGxhdGUYASABKAkSMgoIcmVzcG9uc2UYAiABKA4yIC5hZ2VudG1lc2gudjEuUmVzcG9uc2VJbnNwZWN0aW9uEhEKCWZhaWxfb3BlbhgDIAEoCBIqCgd0aW1lb3V0GAQgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uIogCCgdFeHRQcm9jEg4KBnRhcmdldBgBIAEoCRIKCgJjYRgCIAEoCRIaChJjbGllbnRfY2VydGlmaWNhdGUYAyABKAkSPAoPcHJvY2Vzc2luZ19tb2RlGAQgASgLMiMuYWdlbnRtZXNoLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZRIbChNhbGxvd19tb2RlX292ZXJyaWRlGAUgASgIEjIKD21lc3NhZ2VfdGltZW91dBgGIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbhIaChJmYWlsdXJlX21vZGVfYWxsb3cYByABKAgSGgoSbWF4X2J1ZmZlcmVkX2J5dGVzGAggASgNIv8EChVFeHRQcm9jUHJvY2Vzc2luZ01vZGUSSwoTcmVxdWVzdF9oZWFkZXJfbW9kZRgBIAEoDjIuLmFnZW50bWVzaC52MS5FeHRQcm9jUHJvY2Vzc2luZ01vZGUuSGVhZGVyTW9kZRJMChRyZXNwb25zZV9oZWFkZXJfbW9kZRgCIAEoDjIuLmFnZW50bWVzaC52MS5FeHRQcm9jUHJvY2Vzc2luZ01vZGUuSGVhZGVyTW9kZRJHChFyZXF1ZXN0X2JvZHlfbW9kZRgDIAEoDjIsLmFnZW50bWVzaC52MS5FeHRQcm9jUHJvY2Vzc2luZ01vZGUuQm9keU1vZGUSSAoScmVzcG9uc2VfYm9keV9tb2RlGAQgASgOMiwuYWdlbnRtZXNoLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZS5Cb2R5TW9kZRJMChRyZXF1ZXN0X3RyYWlsZXJfbW9kZRgFIAEoDjIuLmFnZW50bWVzaC52MS5FeHRQcm9jUHJvY2Vzc2luZ01vZGUuSGVhZGVyTW9kZRJNChVyZXNwb25zZV90cmFpbGVyX21vZGUYBiABKA4yLi5hZ2VudG1lc2gudjEuRXh0UHJvY1Byb2Nlc3NpbmdNb2RlLkhlYWRlck1vZGUiOQoKSGVhZGVyTW9kZRIXChNIRUFERVJfTU9ERV9ERUZBVUxUEAASCAoEU0VORBABEggKBFNLSVAQAiJgCghCb2R5TW9kZRIICgROT05FEAASDAoIU1RSRUFNRUQQARIMCghCVUZGRVJFRBACEhQKEEJVRkZFUkVEX1BBUlRJQUwQAxIYChRGVUxMX0RVUExFWF9TVFJFQU1FRBAEIuEBChBDcmVkZW50aWFsQnJva2VyEhcKDXN0YXRpY19zZWNyZXQYASABKAlIABI3Cg9vaWRjX2ZlZGVyYXRpb24YAiABKAsyHC5hZ2VudG1lc2gudjEuT0lEQ0ZlZGVyYXRpb25IABI2Cg9hd3NfYXNzdW1lX3JvbGUYAyABKAsyGy5hZ2VudG1lc2gudjEuQVdTQXNzdW1lUm9sZUgAEjsKEXBsYXRmb3JtX2lkZW50aXR5GAQgASgLMh4uYWdlbnRtZXNoLnYxLlBsYXRmb3JtSWRlbnRpdHlIAEIGCgRraW5kIl8KDk9JRENGZWRlcmF0aW9uEhYKDnRva2VuX2VuZHBvaW50GAEgASgJEhAKCGF1ZGllbmNlGAIgASgJEhMKC2ltcGVyc29uYXRlGAMgASgJEg4KBnNjb3BlcxgEIAMoCSI5Cg1BV1NBc3N1bWVSb2xlEhAKCHJvbGVfYXJuGAEgASgJEhYKDnNlc3Npb25fcG9saWN5GAIgASgJIiIKEFBsYXRmb3JtSWRlbnRpdHkSDgoGc2NvcGVzGAEgAygJIi4KDVBvbGljeUJpbmRpbmcSDAoEcm9sZRgBIAEoCRIPCgdtZW1iZXJzGAIgAygJIpcBCgxQb2xpY3lDb25maWcSJwoFcm9sZXMYASADKAsyGC5hZ2VudG1lc2gudjEuUG9saWN5Um9sZRItCghiaW5kaW5ncxgCIAMoCzIbLmFnZW50bWVzaC52MS5Qb2xpY3lCaW5kaW5nEi8KBmVncmVzcxgDIAMoCzIfLmFnZW50bWVzaC52MS5FZ3Jlc3NEZXN0aW5hdGlvbiIYChZQb2xpY3lDb25maWdHZXRSZXF1ZXN0IjAKF1BvbGljeUNvbmZpZ0dldFJlc3BvbnNlEhUKDWRhdGFsb2dfcnVsZXMYASADKAkiPAoaUG9saWN5Q29uZmlnVXBkYXRlUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCBINCgVlcnJvchgCIAEoCSIaChhFZ3Jlc3NBc3NpZ25tZW50c1JlcXVlc3QiTAoZRWdyZXNzQXNzaWdubWVudHNSZXNwb25zZRIvCgZlZ3Jlc3MYASADKAsyHy5hZ2VudG1lc2gudjEuRWdyZXNzRGVzdGluYXRpb24iZgoMS2V5c1Jlc3BvbnNlEhMKC3B1YmxpY19rZXlzGAEgAygMEi0KCXNpZ25fdGltZRgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASEgoKc2lnbmF0dXJlcxgDIAMoDCJrChNUb2tlblJlZnJlc2hSZXF1ZXN0EhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYASABKAwSGQoRY2hhbGxlbmdlX3VuaXhfbXMYAiABKAMSDwoHcGVlcl9pZBgDIAEoCRILCgNqd3QYBCABKAkidQoUVG9rZW5SZWZyZXNoUmVzcG9uc2USFQoNYmlzY3VpdF90b2tlbhgBIAEoDBIvCgtleHBpcmVfdGltZRgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASFQoNZXJyb3JfbWVzc2FnZRgDIAEoCSJAChFOb2RlQ2F0YWxvZ1JlcG9ydBIrCghzZXJ2aWNlcxgBIAMoCzIZLmFnZW50bWVzaC52MS5TZXJ2aWNlSW5mbyIlChJUb2tlblJldm9rZVJlcXVlc3QSDwoHcGVlcl9pZBgBIAEoCSI1ChNUb2tlblJldm9rZVJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgSDQoFZXJyb3IYAiABKAkilgEKG0Jvb3RzdHJhcFRva2VuQ3JlYXRlUmVxdWVzdBIMCgRyb2xlGAEgASgJEhAKCG93bmVyX2lkGAIgASgJEhEKCXR0bF9ob3VycxgDIAEoBRISCgptYXhfdXNhZ2VzGAQgASgFEhMKC2Rlc2NyaXB0aW9uGAUgASgJEhsKE2F1dG9ub21vdXNfcmVjb3ZlcnkYBiABKAgiigEKHEJvb3RzdHJhcFRva2VuQ3JlYXRlUmVzcG9uc2USCgoCaWQYASABKAkSDQoFdG9rZW4YAiABKAkSDAoEcm9sZRgDIAEoCRIQCghvd25lcl9pZBgEIAEoCRIvCgtleHBpcmVfdGltZRgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiqwIKDkJvb3RzdHJhcFRva2VuEgoKAmlkGAEgASgJEgwKBHJvbGUYAiABKAkSEAoIb3duZXJfaWQYAyABKAkSEgoKbWF4X3VzYWdlcxgEIAEoBRIUCgx1c2FnZXNfY291bnQYBSABKAUSEwoLZGVzY3JpcHRpb24YBiABKAkSLwoLY3JlYXRlX3RpbWUYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2V4cGlyZV90aW1lGAggASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIvCgtyZXZva2VfdGltZRgJIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGwoTYXV0b25vbW91c19yZWNvdmVyeRgKIAEoCCJKChpCb290c3RyYXBUb2tlbkxpc3RSZXNwb25zZRIsCgZ0b2tlbnMYASADKAsyHC5hZ2VudG1lc2gudjEuQm9vdHN0cmFwVG9rZW4i1gIKEUVucm9sbG1lbnRSZXF1ZXN0EgoKAmlkGAEgASgJEg8KB3BlZXJfaWQYAiABKAkSEAoIdG9rZW5faWQYAyABKAkSLgoGc3RhdHVzGAQgASgOMh4uYWdlbnRtZXNoLnYxLkVucm9sbG1lbnRTdGF0dXMSOwoGbGFiZWxzGAUgAygLMisuYWdlbnRtZXNoLnYxLkVucm9sbG1lbnRSZXF1ZXN0LkxhYmVsc0VudHJ5Ei8KC2NyZWF0ZV90aW1lGAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIwCgxyZXNvbHZlX3RpbWUYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhMKC3Jlc29sdmVkX2J5GAggASgJGi0KC0xhYmVsc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiUgodRW5yb2xsbWVudFJlcXVlc3RMaXN0UmVzcG9uc2USMQoIcmVxdWVzdHMYASADKAsyHy5hZ2VudG1lc2gudjEuRW5yb2xsbWVudFJlcXVlc3QicAoEVXNlchIKCgJpZBgBIAEoCRIOCgZpc3N1ZXIYAiABKAkSDQoFZW1haWwYAyABKAkSDAoEcm9sZRgEIAEoCRIvCgtjcmVhdGVfdGltZRgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAi4wIKDEVucm9sbGVkTm9kZRIPCgdwZWVyX2lkGAEgASgJEgwKBHJvbGUYAiABKAkSFwoPZW5yb2xsbWVudF90eXBlGAMgASgJEhMKC2NsYWltc19qc29uGAQgASgJEhAKCG93bmVyX2lkGAUgASgJEjYKBmxhYmVscxgGIAMoCzImLmFnZW50bWVzaC52MS5FbnJvbGxlZE5vZGUuTGFiZWxzRW50cnkSLwoLZW5yb2xsX3RpbWUYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2V4cGlyZV90aW1lGAggASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIOCgZiYW5uZWQYCSABKAgSGwoTYXV0b25vbW91c19yZWNvdmVyeRgKIAEoCBotCgtMYWJlbHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIsQBCgtSb3V0ZXJMZWFzZRIPCgdwZWVyX2lkGAEgASgJEhEKCWFkZHJlc3NlcxgCIAMoCRI1ChFsYXN0X3JlbmV3YWxfdGltZRgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLwoLZXhwaXJlX3RpbWUYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhcKD2Nvbm5lY3RlZF9wZWVycxgFIAMoCRIQCghkaHRfc2l6ZRgGIAEoBSJsCgxOb2RlU2VydmljZXMSKwoIc2VydmljZXMYASADKAsyGS5hZ2VudG1lc2gudjEuU2VydmljZUluZm8SLwoLcmVwb3J0X3RpbWUYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wItsDChNBZG1pblN0YXR1c1Jlc3BvbnNlEiEKBXVzZXJzGAEgAygLMhIuYWdlbnRtZXNoLnYxLlVzZXISMQoOYWN0aXZlX3JvdXRlcnMYAiADKAsyGS5hZ2VudG1lc2gudjEuUm91dGVyTGVhc2USMgoOZW5yb2xsZWRfbm9kZXMYAyADKAsyGi5hZ2VudG1lc2gudjEuRW5yb2xsZWROb2RlEjwKE2Vucm9sbG1lbnRfcmVxdWVzdHMYBCADKAsyHy5hZ2VudG1lc2gudjEuRW5yb2xsbWVudFJlcXVlc3QSNgoQYm9vdHN0cmFwX3Rva2VucxgFIAMoCzIcLmFnZW50bWVzaC52MS5Cb290c3RyYXBUb2tlbhIqCgZwb2xpY3kYBiABKAsyGi5hZ2VudG1lc2gudjEuUG9saWN5Q29uZmlnEkgKDG5vZGVfY2F0YWxvZxgHIAMoCzIyLmFnZW50bWVzaC52MS5BZG1pblN0YXR1c1Jlc3BvbnNlLk5vZGVDYXRhbG9nRW50cnkaTgoQTm9kZUNhdGFsb2dFbnRyeRILCgNrZXkYASABKAkSKQoFdmFsdWUYAiABKAsyGi5hZ2VudG1lc2gudjEuTm9kZVNlcnZpY2VzOgI4ASKBAgoSVXNlclN0YXR1c1Jlc3BvbnNlEiAKBHVzZXIYASABKAsyEi5hZ2VudG1lc2gudjEuVXNlchIyCg5lbnJvbGxlZF9ub2RlcxgCIAMoCzIaLmFnZW50bWVzaC52MS5FbnJvbGxlZE5vZGUSNgoQYm9vdHN0cmFwX3Rva2VucxgDIAMoCzIcLmFnZW50bWVzaC52MS5Cb290c3RyYXBUb2tlbhIxCg5hY3RpdmVfcm91dGVycxgEIAMoCzIZLmFnZW50bWVzaC52MS5Sb3V0ZXJMZWFzZRIqCgZwb2xpY3kYBSABKAsyGi5hZ2VudG1lc2gudjEuUG9saWN5Q29uZmlnIuQBChhJZGVudGl0eUV2aWRlbmNlUmVzcG9uc2USDwoHcGVlcl9pZBgBIAEoCRIPCgdiaXNjdWl0GAIgASgMEjcKE2Jpc2N1aXRfZXhwaXJlX3RpbWUYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhkKEWNvbnRyb2xfcGxhbmVfdXJsGAQgASgJEiIKGnRydXN0ZWRfY29udHJvbF9wbGFuZV9rZXlzGAUgAygMEi4KCmNoZWNrX3RpbWUYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIsYCChRQZWVyRXZpZGVuY2VSZXNwb25zZRIPCgdwZWVyX2lkGAEgASgJEg8KB2Jpc2N1aXQYAiABKAwSFQoNdmVyaWZ5aW5nX2tleRgDIAEoDBINCgVyb2xlcxgEIAMoCRI+CgZsYWJlbHMYBSADKAsyLi5hZ2VudG1lc2gudjEuUGVlckV2aWRlbmNlUmVzcG9uc2UuTGFiZWxzRW50cnkSLwoLZXhwaXJlX3RpbWUYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhYKDnJldm9jYXRpb25faWRzGAcgAygJEi4KCmNoZWNrX3RpbWUYCCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wGi0KC0xhYmVsc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEijAIKEE1lbWJlckNyZWRlbnRpYWwSGQoRY29udHJvbF9wbGFuZV91cmwYASABKAkSDwoHYmlzY3VpdBgCIAEoDBIvCgtleHBpcmVfdGltZRgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASNQoMdHJ1c3RlZF9rZXlzGAQgAygLMh8uYWdlbnRtZXNoLnYxLlRydXN0ZWRTaWduaW5nS2V5EhkKEWlzc3VlZF91bmRlcl9rZXlzGAUgAygMEhgKEHJvdXRlcl9hZGRyZXNzZXMYBiADKAkSLwoMb2lkY19zZXNzaW9uGAcgASgLMhkuYWdlbnRtZXNoLnYxLk9JRENTZXNzaW9uIlkKEVRydXN0ZWRTaWduaW5nS2V5EhIKCnB1YmxpY19rZXkYASABKAwSMAoMcmVjZWl2ZV90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJZCgtPSURDU2Vzc2lvbhIOCgZpc3N1ZXIYASABKAkSEQoJY2xpZW50X2lkGAIgASgJEhAKCGF1ZGllbmNlGAMgASgJEhUKDXJlZnJlc2hfdG9rZW4YBCABKAkikwEKFVRhc2tBdXRob3JpemF0aW9uUnVsZRIMCgRuYW1lGAEgASgJEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRIlCgVydWxlcxgDIAMoCzIWLmFnZW50bWVzaC52MS5UYXNrUnVsZRIvCgtleHBpcmVfdGltZRgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAihAEKCFRhc2tSdWxlEhMKC2Rlc2NyaXB0aW9uGAEgASgJEhgKEGFsbG93ZWRfc2VydmljZXMYAiADKAkSLgoJb3BlcmF0aW9uGAMgASgLMhsuYWdlbnRtZXNoLnYxLlRhc2tPcGVyYXRpb24SGQoRYWxsb3dlZF9yZXNvdXJjZXMYBCADKAkicwoNVGFza09wZXJhdGlvbhIVCg1hbGxvd2VkX3Rvb2xzGAEgAygJEhcKD2FsbG93ZWRfbWV0aG9kcxgCIAMoCRIVCg1hbGxvd2VkX3BhdGhzGAMgAygJEhsKE2FsbG93ZWRfcGVybWlzc2lvbnMYBCADKAkiqwEKFFRva2VuRXhjaGFuZ2VSZXF1ZXN0EhUKDXN1YmplY3RfdG9rZW4YASABKAkSNgoJdGFza19ydWxlGAIgASgLMiMuYWdlbnRtZXNoLnYxLlRhc2tBdXRob3JpemF0aW9uUnVsZRIMCgRzZWFsGAMgASgIEhkKEWNoYWxsZW5nZV91bml4X21zGAQgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYBSABKAwifwoVVG9rZW5FeGNoYW5nZVJlc3BvbnNlEhUKDWJpc2N1aXRfdG9rZW4YASABKAwSLwoLZXhwaXJlX3RpbWUYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEg0KBXJvbGVzGAMgAygJEg8KB3N1YmplY3QYBCABKAkigQEKD1NUU1Rva2VuUmVxdWVzdBIPCgdiaXNjdWl0GAEgASgMEhMKC2Rlc3RpbmF0aW9uGAIgASgJEhAKCGF1ZGllbmNlGAMgASgJEhkKEWNoYWxsZW5nZV91bml4X21zGAQgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYBSABKAwigwEKEFNUU1Rva2VuUmVzcG9uc2USCwoDand0GAEgASgJEi8KC2V4cGlyZV90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIPCgdzdWJqZWN0GAMgASgJEg0KBXJvbGVzGAQgAygJEhEKCXRhc2tfbmFtZRgFIAEoCSJGChNSZXZvY2F0aW9uc1Jlc3BvbnNlEhYKDnJldm9jYXRpb25faWRzGAEgAygJEhcKD2Jhbm5lZF9wZWVyX2lkcxgCIAMoCSqUAQoQRW5yb2xsbWVudFN0YXR1cxIhCh1FTlJPTExNRU5UX1NUQVRVU19VTlNQRUNJRklFRBAAEh0KGUVOUk9MTE1FTlRfU1RBVFVTX1BFTkRJTkcQARIeChpFTlJPTExNRU5UX1NUQVRVU19BUFBST1ZFRBACEh4KGkVOUk9MTE1FTlRfU1RBVFVTX1JFSkVDVEVEEAMqjAEKC1NlcnZpY2VUeXBlEhwKGFNFUlZJQ0VfVFlQRV9VTlNQRUNJRklFRBAAEhQKEFNFUlZJQ0VfVFlQRV9NQ1AQARIaChZTRVJWSUNFX1RZUEVfSU5GRVJFTkNFEAISFAoQU0VSVklDRV9UWVBFX0EyQRADEhcKE1NFUlZJQ0VfVFlQRV9FR1JFU1MQBCo3CgpFZ3Jlc3NNb2RlEhQKEEVHUkVTU19NT0RFX0hUVFAQABITCg9FR1JFU1NfTU9ERV9UQ1AQASpcChJSZXNwb25zZUluc3BlY3Rpb24SIAocUkVTUE9OU0VfSU5TUEVDVElPTl9CVUZGRVJFRBAAEiQKIFJFU1BPTlNFX0lOU1BFQ1RJT05fUkVRVUVTVF9PTkxZEAFCIVofZ2l0aHViLmNvbS9nb29nbGUvYWdlbnRtZXNoL2FwaWIGcHJvdG8z", [file_google_protobuf_duration, file_google_protobuf_timestamp]); + fileDesc("Cg9hZ2VudG1lc2gucHJvdG8SDGFnZW50bWVzaC52MSI0CglBdXRoRnJhbWUSDwoHYmlzY3VpdBgBIAEoDBIWCg50YXJnZXRfc2VydmljZRgCIAEoCSI/CgxBdXRoUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCBINCgVlcnJvchgCIAEoCRIPCgdiaXNjdWl0GAMgASgMIrEBCgxSb3V0ZXJHb0F3YXkSMQoGcmVhc29uGAEgASgOMiEuYWdlbnRtZXNoLnYxLlJvdXRlckdvQXdheS5SZWFzb24SLgoLcmV0cnlfYWZ0ZXIYAiABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24iPgoGUmVhc29uEhYKElJFQVNPTl9VTlNQRUNJRklFRBAAEgwKCERSQUlOSU5HEAESDgoKT1ZFUkxPQURFRBACItwBCglNZXNoRXZlbnQSKgoEdHlwZRgBIAEoDjIcLmFnZW50bWVzaC52MS5NZXNoRXZlbnQuVHlwZRIPCgdwZWVyX2lkGAIgASgJEi4KCmV2ZW50X3RpbWUYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhYKDm5ld19wdWJsaWNfa2V5GAQgASgMEhEKCXNpZ25hdHVyZRgFIAEoDCI3CgRUeXBlEgoKBkJBTk5FRBAAEhAKDEtFWV9ST1RBVElPThABEhEKDVBPTElDWV9VUERBVEUQAiL5AQoNRW5yb2xsUmVxdWVzdBILCgNqd3QYASABKAkSDwoHcGVlcl9pZBgCIAEoCRISCgpwdWJsaWNfa2V5GAMgASgMEhYKDnJlcXVlc3RlZF9yb2xlGAQgASgJEjcKBmxhYmVscxgFIAMoCzInLmFnZW50bWVzaC52MS5FbnJvbGxSZXF1ZXN0LkxhYmVsc0VudHJ5EhkKEWNoYWxsZW5nZV91bml4X21zGAYgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYByABKAwaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKrAQoORW5yb2xsUmVzcG9uc2USFQoNYmlzY3VpdF90b2tlbhgBIAEoDBIVCg1lcnJvcl9tZXNzYWdlGAIgASgJEiAKGGNvbnRyb2xfcGxhbmVfcHVibGljX2tleRgDIAEoDBIYChByb3V0ZXJfYWRkcmVzc2VzGAQgAygJEi8KC2V4cGlyZV90aW1lGAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCKXAgoWQm9vdHN0cmFwRW5yb2xsUmVxdWVzdBIXCg9ib290c3RyYXBfdG9rZW4YASABKAkSDwoHcGVlcl9pZBgCIAEoCRISCgpwdWJsaWNfa2V5GAMgASgMEhYKDnJlcXVlc3RlZF9yb2xlGAQgASgJEkAKBmxhYmVscxgFIAMoCzIwLmFnZW50bWVzaC52MS5Cb290c3RyYXBFbnJvbGxSZXF1ZXN0LkxhYmVsc0VudHJ5EhkKEWNoYWxsZW5nZV91bml4X21zGAYgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYByABKAwaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKDAgoXQm9vdHN0cmFwRW5yb2xsUmVzcG9uc2USLgoGc3RhdHVzGAEgASgOMh4uYWdlbnRtZXNoLnYxLkVucm9sbG1lbnRTdGF0dXMSFQoNYmlzY3VpdF90b2tlbhgCIAEoDBIdChVwb2xsX2ludGVydmFsX3NlY29uZHMYAyABKAUSFQoNZXJyb3JfbWVzc2FnZRgEIAEoCRIgChhjb250cm9sX3BsYW5lX3B1YmxpY19rZXkYBSABKAwSGAoQcm91dGVyX2FkZHJlc3NlcxgGIAMoCRIvCgtleHBpcmVfdGltZRgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiWQoLU2VydmljZUluZm8SJwoEdHlwZRgBIAEoDjIZLmFnZW50bWVzaC52MS5TZXJ2aWNlVHlwZRIMCgRuYW1lGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJIoEBCg5Db21tYW5kQmFja2VuZBIPCgdjb21tYW5kGAEgAygJEjIKA2VudhgCIAMoCzIlLmFnZW50bWVzaC52MS5Db21tYW5kQmFja2VuZC5FbnZFbnRyeRoqCghFbnZFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIpYBChZSZWdpc3RlclNlcnZpY2VSZXF1ZXN0EioKB3NlcnZpY2UYASABKAsyGS5hZ2VudG1lc2gudjEuU2VydmljZUluZm8SFAoKdGFyZ2V0X3VybBgCIAEoCUgAEi8KB2NvbW1hbmQYAyABKAsyHC5hZ2VudG1lc2gudjEuQ29tbWFuZEJhY2tlbmRIAEIJCgdiYWNrZW5kImkKEkRpc2NvdmVyZWRQcm92aWRlchIPCgdwZWVyX2lkGAEgASgJEhcKD2xvY2FsX3Byb3h5X3VybBgCIAEoCRIQCghzcnZfbmFtZRgDIAEoCRIXCg9zcnZfZGVzY3JpcHRpb24YBCABKAki6AEKGENvbnRyb2xQbGFuZUluZm9SZXNwb25zZRITCgtvaWRjX2lzc3VlchgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSEAoIYXVkaWVuY2UYAyABKAkSGAoQcm91dGVyX2FkZHJlc3NlcxgEIAMoCRIXCg9iYW5uZWRfcGVlcl9pZHMYBSADKAkSKQoHcm91dGVycxgGIAMoCzIYLmFnZW50bWVzaC52MS5Sb3V0ZXJJbmZvEjQKCWRpcmVjdG9yeRgHIAEoCzIhLmFnZW50bWVzaC52MS5EaXJlY3RvcnlQYXJhbWV0ZXJzIpABChNEaXJlY3RvcnlQYXJhbWV0ZXJzEhMKC3JlcGxpY2F0aW9uGAEgASgFEjUKEnJlcHJvdmlkZV9pbnRlcnZhbBgCIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbhItCgpyZWNvcmRfdHRsGAMgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uIvsBChBEaXJlY3RvcnlSZXF1ZXN0EjEKB3Byb3ZpZGUYASABKAsyHi5hZ2VudG1lc2gudjEuRGlyZWN0b3J5UHJvdmlkZUgAEj4KDmZpbmRfcHJvdmlkZXJzGAIgASgLMiQuYWdlbnRtZXNoLnYxLkRpcmVjdG9yeUZpbmRQcm92aWRlcnNIABI0CglmaW5kX3BlZXIYAyABKAsyHy5hZ2VudG1lc2gudjEuRGlyZWN0b3J5RmluZFBlZXJIABIzCgh3aXRoZHJhdxgEIAEoCzIfLmFnZW50bWVzaC52MS5EaXJlY3RvcnlXaXRoZHJhd0gAQgkKB3JlcXVlc3QiUQoQRGlyZWN0b3J5UHJvdmlkZRILCgNrZXkYASABKAwSDQoFYWRkcnMYAiADKAkSEAoIcHJvdmlkZXIYAyABKAkSDwoHc2VydmljZRgEIAEoCSIyChFEaXJlY3RvcnlXaXRoZHJhdxILCgNrZXkYASABKAwSEAoIcHJvdmlkZXIYAiABKAkiNAoWRGlyZWN0b3J5RmluZFByb3ZpZGVycxILCgNrZXkYASABKAwSDQoFbGltaXQYAiABKA0iJAoRRGlyZWN0b3J5RmluZFBlZXISDwoHcGVlcl9pZBgBIAEoCSJ3ChFEaXJlY3RvcnlSZXNwb25zZRIrCglwcm92aWRlcnMYASADKAsyGC5hZ2VudG1lc2gudjEuUGVlclJlY29yZBImCgRwZWVyGAIgASgLMhguYWdlbnRtZXNoLnYxLlBlZXJSZWNvcmQSDQoFZXJyb3IYAyABKAkiLAoKUGVlclJlY29yZBIPCgdwZWVyX2lkGAEgASgJEg0KBWFkZHJzGAIgAygJIsQBCgpSb3V0ZXJJbmZvEg8KB3BlZXJfaWQYASABKAkSEQoJYWRkcmVzc2VzGAIgAygJEjQKBmxhYmVscxgDIAMoCzIkLmFnZW50bWVzaC52MS5Sb3V0ZXJJbmZvLkxhYmVsc0VudHJ5EhMKC2Nvbm5lY3Rpb25zGAQgASgFEhgKEGNvbm5lY3Rpb25fbGltaXQYBSABKAUaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKDAgoSUm91dGVyTGVhc2VSZXF1ZXN0Eg8KB3BlZXJfaWQYASABKAkSEQoJYWRkcmVzc2VzGAIgAygJEg8KB2Jpc2N1aXQYAyABKAwSFwoPY29ubmVjdGVkX3BlZXJzGAQgAygJEhAKCGRodF9zaXplGAUgASgFEhkKEWNoYWxsZW5nZV91bml4X21zGAYgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYByABKAwSJgoDdHRsGAggASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uEhMKC2Nvbm5lY3Rpb25zGAkgASgFEhgKEGNvbm5lY3Rpb25fbGltaXQYCiABKAUiZgoTUm91dGVyTGVhc2VSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIEg0KBWVycm9yGAIgASgJEi8KC2V4cGlyZV90aW1lGAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCKkAQoKUG9saWN5Um9sZRIMCgRuYW1lGAEgASgJEhcKD2FsbG93ZWRfdGFyZ2V0cxgCIAMoCRIYChBhbGxvd2VkX3NlcnZpY2VzGAMgAygJEhYKDmN1c3RvbV9kYXRhbG9nGAQgAygJEhYKDmFsbG93ZWRfbGFiZWxzGAUgAygJEiUKBGh0dHAYBiADKAsyFy5hZ2VudG1lc2gudjEuSFRUUEdyYW50IjwKCUhUVFBHcmFudBIPCgdzZXJ2aWNlGAEgASgJEg8KB21ldGhvZHMYAiADKAkSDQoFcGF0aHMYAyADKAkioQIKEUVncmVzc0Rlc3RpbmF0aW9uEgwKBG5hbWUYASABKAkSEgoKdGFyZ2V0X3VybBgCIAEoCRISCgpjcmVkZW50aWFsGAMgASgJEhEKCXNlcnZlZF9ieRgEIAMoCRIuCgZicm9rZXIYBSABKAsyHi5hZ2VudG1lc2gudjEuQ3JlZGVudGlhbEJyb2tlchIsCgppbnNwZWN0aW9uGAYgASgLMhguYWdlbnRtZXNoLnYxLkluc3BlY3Rpb24SJgoEbW9kZRgHIAEoDjIYLmFnZW50bWVzaC52MS5FZ3Jlc3NNb2RlEg0KBXBvcnRzGAggAygNEhUKDXByZXNlcnZlX2hvc3QYCSABKAgSFwoPZm9yd2FyZF9jb250ZXh0GAogASgIIjkKCkluc3BlY3Rpb24SKwoKaW5zcGVjdG9ycxgBIAMoCzIXLmFnZW50bWVzaC52MS5JbnNwZWN0b3IibwoJSW5zcGVjdG9yEi8KC21vZGVsX2FybW9yGAEgASgLMhguYWdlbnRtZXNoLnYxLk1vZGVsQXJtb3JIABIpCghleHRfcHJvYxgCIAEoCzIVLmFnZW50bWVzaC52MS5FeHRQcm9jSABCBgoEa2luZCKRAQoKTW9kZWxBcm1vchIQCgh0ZW1wbGF0ZRgBIAEoCRIyCghyZXNwb25zZRgCIAEoDjIgLmFnZW50bWVzaC52MS5SZXNwb25zZUluc3BlY3Rpb24SEQoJZmFpbF9vcGVuGAMgASgIEioKB3RpbWVvdXQYBCABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24iiAIKB0V4dFByb2MSDgoGdGFyZ2V0GAEgASgJEgoKAmNhGAIgASgJEhoKEmNsaWVudF9jZXJ0aWZpY2F0ZRgDIAEoCRI8Cg9wcm9jZXNzaW5nX21vZGUYBCABKAsyIy5hZ2VudG1lc2gudjEuRXh0UHJvY1Byb2Nlc3NpbmdNb2RlEhsKE2FsbG93X21vZGVfb3ZlcnJpZGUYBSABKAgSMgoPbWVzc2FnZV90aW1lb3V0GAYgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uEhoKEmZhaWx1cmVfbW9kZV9hbGxvdxgHIAEoCBIaChJtYXhfYnVmZmVyZWRfYnl0ZXMYCCABKA0i/wQKFUV4dFByb2NQcm9jZXNzaW5nTW9kZRJLChNyZXF1ZXN0X2hlYWRlcl9tb2RlGAEgASgOMi4uYWdlbnRtZXNoLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZS5IZWFkZXJNb2RlEkwKFHJlc3BvbnNlX2hlYWRlcl9tb2RlGAIgASgOMi4uYWdlbnRtZXNoLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZS5IZWFkZXJNb2RlEkcKEXJlcXVlc3RfYm9keV9tb2RlGAMgASgOMiwuYWdlbnRtZXNoLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZS5Cb2R5TW9kZRJIChJyZXNwb25zZV9ib2R5X21vZGUYBCABKA4yLC5hZ2VudG1lc2gudjEuRXh0UHJvY1Byb2Nlc3NpbmdNb2RlLkJvZHlNb2RlEkwKFHJlcXVlc3RfdHJhaWxlcl9tb2RlGAUgASgOMi4uYWdlbnRtZXNoLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZS5IZWFkZXJNb2RlEk0KFXJlc3BvbnNlX3RyYWlsZXJfbW9kZRgGIAEoDjIuLmFnZW50bWVzaC52MS5FeHRQcm9jUHJvY2Vzc2luZ01vZGUuSGVhZGVyTW9kZSI5CgpIZWFkZXJNb2RlEhcKE0hFQURFUl9NT0RFX0RFRkFVTFQQABIICgRTRU5EEAESCAoEU0tJUBACImAKCEJvZHlNb2RlEggKBE5PTkUQABIMCghTVFJFQU1FRBABEgwKCEJVRkZFUkVEEAISFAoQQlVGRkVSRURfUEFSVElBTBADEhgKFEZVTExfRFVQTEVYX1NUUkVBTUVEEAQi4QEKEENyZWRlbnRpYWxCcm9rZXISFwoNc3RhdGljX3NlY3JldBgBIAEoCUgAEjcKD29pZGNfZmVkZXJhdGlvbhgCIAEoCzIcLmFnZW50bWVzaC52MS5PSURDRmVkZXJhdGlvbkgAEjYKD2F3c19hc3N1bWVfcm9sZRgDIAEoCzIbLmFnZW50bWVzaC52MS5BV1NBc3N1bWVSb2xlSAASOwoRcGxhdGZvcm1faWRlbnRpdHkYBCABKAsyHi5hZ2VudG1lc2gudjEuUGxhdGZvcm1JZGVudGl0eUgAQgYKBGtpbmQiXwoOT0lEQ0ZlZGVyYXRpb24SFgoOdG9rZW5fZW5kcG9pbnQYASABKAkSEAoIYXVkaWVuY2UYAiABKAkSEwoLaW1wZXJzb25hdGUYAyABKAkSDgoGc2NvcGVzGAQgAygJIjkKDUFXU0Fzc3VtZVJvbGUSEAoIcm9sZV9hcm4YASABKAkSFgoOc2Vzc2lvbl9wb2xpY3kYAiABKAkiIgoQUGxhdGZvcm1JZGVudGl0eRIOCgZzY29wZXMYASADKAkiLgoNUG9saWN5QmluZGluZxIMCgRyb2xlGAEgASgJEg8KB21lbWJlcnMYAiADKAkilwEKDFBvbGljeUNvbmZpZxInCgVyb2xlcxgBIAMoCzIYLmFnZW50bWVzaC52MS5Qb2xpY3lSb2xlEi0KCGJpbmRpbmdzGAIgAygLMhsuYWdlbnRtZXNoLnYxLlBvbGljeUJpbmRpbmcSLwoGZWdyZXNzGAMgAygLMh8uYWdlbnRtZXNoLnYxLkVncmVzc0Rlc3RpbmF0aW9uIhgKFlBvbGljeUNvbmZpZ0dldFJlcXVlc3QiMAoXUG9saWN5Q29uZmlnR2V0UmVzcG9uc2USFQoNZGF0YWxvZ19ydWxlcxgBIAMoCSI8ChpQb2xpY3lDb25maWdVcGRhdGVSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIEg0KBWVycm9yGAIgASgJIhoKGEVncmVzc0Fzc2lnbm1lbnRzUmVxdWVzdCJMChlFZ3Jlc3NBc3NpZ25tZW50c1Jlc3BvbnNlEi8KBmVncmVzcxgBIAMoCzIfLmFnZW50bWVzaC52MS5FZ3Jlc3NEZXN0aW5hdGlvbiJmCgxLZXlzUmVzcG9uc2USEwoLcHVibGljX2tleXMYASADKAwSLQoJc2lnbl90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBISCgpzaWduYXR1cmVzGAMgAygMImsKE1Rva2VuUmVmcmVzaFJlcXVlc3QSGwoTY2hhbGxlbmdlX3NpZ25hdHVyZRgBIAEoDBIZChFjaGFsbGVuZ2VfdW5peF9tcxgCIAEoAxIPCgdwZWVyX2lkGAMgASgJEgsKA2p3dBgEIAEoCSJ1ChRUb2tlblJlZnJlc2hSZXNwb25zZRIVCg1iaXNjdWl0X3Rva2VuGAEgASgMEi8KC2V4cGlyZV90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIVCg1lcnJvcl9tZXNzYWdlGAMgASgJIkAKEU5vZGVDYXRhbG9nUmVwb3J0EisKCHNlcnZpY2VzGAEgAygLMhkuYWdlbnRtZXNoLnYxLlNlcnZpY2VJbmZvIiUKElRva2VuUmV2b2tlUmVxdWVzdBIPCgdwZWVyX2lkGAEgASgJIjUKE1Rva2VuUmV2b2tlUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCBINCgVlcnJvchgCIAEoCSKWAQobQm9vdHN0cmFwVG9rZW5DcmVhdGVSZXF1ZXN0EgwKBHJvbGUYASABKAkSEAoIb3duZXJfaWQYAiABKAkSEQoJdHRsX2hvdXJzGAMgASgFEhIKCm1heF91c2FnZXMYBCABKAUSEwoLZGVzY3JpcHRpb24YBSABKAkSGwoTYXV0b25vbW91c19yZWNvdmVyeRgGIAEoCCKKAQocQm9vdHN0cmFwVG9rZW5DcmVhdGVSZXNwb25zZRIKCgJpZBgBIAEoCRINCgV0b2tlbhgCIAEoCRIMCgRyb2xlGAMgASgJEhAKCG93bmVyX2lkGAQgASgJEi8KC2V4cGlyZV90aW1lGAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCKrAgoOQm9vdHN0cmFwVG9rZW4SCgoCaWQYASABKAkSDAoEcm9sZRgCIAEoCRIQCghvd25lcl9pZBgDIAEoCRISCgptYXhfdXNhZ2VzGAQgASgFEhQKDHVzYWdlc19jb3VudBgFIAEoBRITCgtkZXNjcmlwdGlvbhgGIAEoCRIvCgtjcmVhdGVfdGltZRgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLwoLZXhwaXJlX3RpbWUYCCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC3Jldm9rZV90aW1lGAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIbChNhdXRvbm9tb3VzX3JlY292ZXJ5GAogASgIIkoKGkJvb3RzdHJhcFRva2VuTGlzdFJlc3BvbnNlEiwKBnRva2VucxgBIAMoCzIcLmFnZW50bWVzaC52MS5Cb290c3RyYXBUb2tlbiLWAgoRRW5yb2xsbWVudFJlcXVlc3QSCgoCaWQYASABKAkSDwoHcGVlcl9pZBgCIAEoCRIQCgh0b2tlbl9pZBgDIAEoCRIuCgZzdGF0dXMYBCABKA4yHi5hZ2VudG1lc2gudjEuRW5yb2xsbWVudFN0YXR1cxI7CgZsYWJlbHMYBSADKAsyKy5hZ2VudG1lc2gudjEuRW5yb2xsbWVudFJlcXVlc3QuTGFiZWxzRW50cnkSLwoLY3JlYXRlX3RpbWUYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjAKDHJlc29sdmVfdGltZRgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASEwoLcmVzb2x2ZWRfYnkYCCABKAkaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJSCh1FbnJvbGxtZW50UmVxdWVzdExpc3RSZXNwb25zZRIxCghyZXF1ZXN0cxgBIAMoCzIfLmFnZW50bWVzaC52MS5FbnJvbGxtZW50UmVxdWVzdCJwCgRVc2VyEgoKAmlkGAEgASgJEg4KBmlzc3VlchgCIAEoCRINCgVlbWFpbBgDIAEoCRIMCgRyb2xlGAQgASgJEi8KC2NyZWF0ZV90aW1lGAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCLjAgoMRW5yb2xsZWROb2RlEg8KB3BlZXJfaWQYASABKAkSDAoEcm9sZRgCIAEoCRIXCg9lbnJvbGxtZW50X3R5cGUYAyABKAkSEwoLY2xhaW1zX2pzb24YBCABKAkSEAoIb3duZXJfaWQYBSABKAkSNgoGbGFiZWxzGAYgAygLMiYuYWdlbnRtZXNoLnYxLkVucm9sbGVkTm9kZS5MYWJlbHNFbnRyeRIvCgtlbnJvbGxfdGltZRgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLwoLZXhwaXJlX3RpbWUYCCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEg4KBmJhbm5lZBgJIAEoCBIbChNhdXRvbm9tb3VzX3JlY292ZXJ5GAogASgIGi0KC0xhYmVsc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEixAEKC1JvdXRlckxlYXNlEg8KB3BlZXJfaWQYASABKAkSEQoJYWRkcmVzc2VzGAIgAygJEjUKEWxhc3RfcmVuZXdhbF90aW1lGAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIvCgtleHBpcmVfdGltZRgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASFwoPY29ubmVjdGVkX3BlZXJzGAUgAygJEhAKCGRodF9zaXplGAYgASgFImwKDE5vZGVTZXJ2aWNlcxIrCghzZXJ2aWNlcxgBIAMoCzIZLmFnZW50bWVzaC52MS5TZXJ2aWNlSW5mbxIvCgtyZXBvcnRfdGltZRgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAi2wMKE0FkbWluU3RhdHVzUmVzcG9uc2USIQoFdXNlcnMYASADKAsyEi5hZ2VudG1lc2gudjEuVXNlchIxCg5hY3RpdmVfcm91dGVycxgCIAMoCzIZLmFnZW50bWVzaC52MS5Sb3V0ZXJMZWFzZRIyCg5lbnJvbGxlZF9ub2RlcxgDIAMoCzIaLmFnZW50bWVzaC52MS5FbnJvbGxlZE5vZGUSPAoTZW5yb2xsbWVudF9yZXF1ZXN0cxgEIAMoCzIfLmFnZW50bWVzaC52MS5FbnJvbGxtZW50UmVxdWVzdBI2ChBib290c3RyYXBfdG9rZW5zGAUgAygLMhwuYWdlbnRtZXNoLnYxLkJvb3RzdHJhcFRva2VuEioKBnBvbGljeRgGIAEoCzIaLmFnZW50bWVzaC52MS5Qb2xpY3lDb25maWcSSAoMbm9kZV9jYXRhbG9nGAcgAygLMjIuYWdlbnRtZXNoLnYxLkFkbWluU3RhdHVzUmVzcG9uc2UuTm9kZUNhdGFsb2dFbnRyeRpOChBOb2RlQ2F0YWxvZ0VudHJ5EgsKA2tleRgBIAEoCRIpCgV2YWx1ZRgCIAEoCzIaLmFnZW50bWVzaC52MS5Ob2RlU2VydmljZXM6AjgBIoECChJVc2VyU3RhdHVzUmVzcG9uc2USIAoEdXNlchgBIAEoCzISLmFnZW50bWVzaC52MS5Vc2VyEjIKDmVucm9sbGVkX25vZGVzGAIgAygLMhouYWdlbnRtZXNoLnYxLkVucm9sbGVkTm9kZRI2ChBib290c3RyYXBfdG9rZW5zGAMgAygLMhwuYWdlbnRtZXNoLnYxLkJvb3RzdHJhcFRva2VuEjEKDmFjdGl2ZV9yb3V0ZXJzGAQgAygLMhkuYWdlbnRtZXNoLnYxLlJvdXRlckxlYXNlEioKBnBvbGljeRgFIAEoCzIaLmFnZW50bWVzaC52MS5Qb2xpY3lDb25maWci5AEKGElkZW50aXR5RXZpZGVuY2VSZXNwb25zZRIPCgdwZWVyX2lkGAEgASgJEg8KB2Jpc2N1aXQYAiABKAwSNwoTYmlzY3VpdF9leHBpcmVfdGltZRgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRY29udHJvbF9wbGFuZV91cmwYBCABKAkSIgoadHJ1c3RlZF9jb250cm9sX3BsYW5lX2tleXMYBSADKAwSLgoKY2hlY2tfdGltZRgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAixgIKFFBlZXJFdmlkZW5jZVJlc3BvbnNlEg8KB3BlZXJfaWQYASABKAkSDwoHYmlzY3VpdBgCIAEoDBIVCg12ZXJpZnlpbmdfa2V5GAMgASgMEg0KBXJvbGVzGAQgAygJEj4KBmxhYmVscxgFIAMoCzIuLmFnZW50bWVzaC52MS5QZWVyRXZpZGVuY2VSZXNwb25zZS5MYWJlbHNFbnRyeRIvCgtleHBpcmVfdGltZRgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASFgoOcmV2b2NhdGlvbl9pZHMYByADKAkSLgoKY2hlY2tfdGltZRgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKMAgoQTWVtYmVyQ3JlZGVudGlhbBIZChFjb250cm9sX3BsYW5lX3VybBgBIAEoCRIPCgdiaXNjdWl0GAIgASgMEi8KC2V4cGlyZV90aW1lGAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBI1Cgx0cnVzdGVkX2tleXMYBCADKAsyHy5hZ2VudG1lc2gudjEuVHJ1c3RlZFNpZ25pbmdLZXkSGQoRaXNzdWVkX3VuZGVyX2tleXMYBSADKAwSGAoQcm91dGVyX2FkZHJlc3NlcxgGIAMoCRIvCgxvaWRjX3Nlc3Npb24YByABKAsyGS5hZ2VudG1lc2gudjEuT0lEQ1Nlc3Npb24iWQoRVHJ1c3RlZFNpZ25pbmdLZXkSEgoKcHVibGljX2tleRgBIAEoDBIwCgxyZWNlaXZlX3RpbWUYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIlkKC09JRENTZXNzaW9uEg4KBmlzc3VlchgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSEAoIYXVkaWVuY2UYAyABKAkSFQoNcmVmcmVzaF90b2tlbhgEIAEoCSKTAQoVVGFza0F1dGhvcml6YXRpb25SdWxlEgwKBG5hbWUYASABKAkSFAoMZGlzcGxheV9uYW1lGAIgASgJEiUKBXJ1bGVzGAMgAygLMhYuYWdlbnRtZXNoLnYxLlRhc2tSdWxlEi8KC2V4cGlyZV90aW1lGAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCKEAQoIVGFza1J1bGUSEwoLZGVzY3JpcHRpb24YASABKAkSGAoQYWxsb3dlZF9zZXJ2aWNlcxgCIAMoCRIuCglvcGVyYXRpb24YAyABKAsyGy5hZ2VudG1lc2gudjEuVGFza09wZXJhdGlvbhIZChFhbGxvd2VkX3Jlc291cmNlcxgEIAMoCSJzCg1UYXNrT3BlcmF0aW9uEhUKDWFsbG93ZWRfdG9vbHMYASADKAkSFwoPYWxsb3dlZF9tZXRob2RzGAIgAygJEhUKDWFsbG93ZWRfcGF0aHMYAyADKAkSGwoTYWxsb3dlZF9wZXJtaXNzaW9ucxgEIAMoCSKrAQoUVG9rZW5FeGNoYW5nZVJlcXVlc3QSFQoNc3ViamVjdF90b2tlbhgBIAEoCRI2Cgl0YXNrX3J1bGUYAiABKAsyIy5hZ2VudG1lc2gudjEuVGFza0F1dGhvcml6YXRpb25SdWxlEgwKBHNlYWwYAyABKAgSGQoRY2hhbGxlbmdlX3VuaXhfbXMYBCABKAMSGwoTY2hhbGxlbmdlX3NpZ25hdHVyZRgFIAEoDCJ/ChVUb2tlbkV4Y2hhbmdlUmVzcG9uc2USFQoNYmlzY3VpdF90b2tlbhgBIAEoDBIvCgtleHBpcmVfdGltZRgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASDQoFcm9sZXMYAyADKAkSDwoHc3ViamVjdBgEIAEoCSKBAQoPU1RTVG9rZW5SZXF1ZXN0Eg8KB2Jpc2N1aXQYASABKAwSEwoLZGVzdGluYXRpb24YAiABKAkSEAoIYXVkaWVuY2UYAyABKAkSGQoRY2hhbGxlbmdlX3VuaXhfbXMYBCABKAMSGwoTY2hhbGxlbmdlX3NpZ25hdHVyZRgFIAEoDCKDAQoQU1RTVG9rZW5SZXNwb25zZRILCgNqd3QYASABKAkSLwoLZXhwaXJlX3RpbWUYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEg8KB3N1YmplY3QYAyABKAkSDQoFcm9sZXMYBCADKAkSEQoJdGFza19uYW1lGAUgASgJIkYKE1Jldm9jYXRpb25zUmVzcG9uc2USFgoOcmV2b2NhdGlvbl9pZHMYASADKAkSFwoPYmFubmVkX3BlZXJfaWRzGAIgAygJKpQBChBFbnJvbGxtZW50U3RhdHVzEiEKHUVOUk9MTE1FTlRfU1RBVFVTX1VOU1BFQ0lGSUVEEAASHQoZRU5ST0xMTUVOVF9TVEFUVVNfUEVORElORxABEh4KGkVOUk9MTE1FTlRfU1RBVFVTX0FQUFJPVkVEEAISHgoaRU5ST0xMTUVOVF9TVEFUVVNfUkVKRUNURUQQAyqMAQoLU2VydmljZVR5cGUSHAoYU0VSVklDRV9UWVBFX1VOU1BFQ0lGSUVEEAASFAoQU0VSVklDRV9UWVBFX01DUBABEhoKFlNFUlZJQ0VfVFlQRV9JTkZFUkVOQ0UQAhIUChBTRVJWSUNFX1RZUEVfQTJBEAMSFwoTU0VSVklDRV9UWVBFX0VHUkVTUxAEKjcKCkVncmVzc01vZGUSFAoQRUdSRVNTX01PREVfSFRUUBAAEhMKD0VHUkVTU19NT0RFX1RDUBABKlwKElJlc3BvbnNlSW5zcGVjdGlvbhIgChxSRVNQT05TRV9JTlNQRUNUSU9OX0JVRkZFUkVEEAASJAogUkVTUE9OU0VfSU5TUEVDVElPTl9SRVFVRVNUX09OTFkQAUIhWh9naXRodWIuY29tL2dvb2dsZS9hZ2VudG1lc2gvYXBpYgZwcm90bzM", [file_google_protobuf_duration, file_google_protobuf_timestamp]); /** * @generated from message agentmesh.v1.AuthFrame @@ -706,9 +706,12 @@ export const DirectoryRequestSchema: GenMessage = /*@__PURE__* * addrs. A key is a sha256 multihash: of "mesh:service:[:]" for * a service, of "mesh:peer:" for the member itself. A record lives * for the mesh's record TTL after its last announcement; announcing it - * again replaces its addresses. provider is set only between routers, by - * the router that holds the member, and refused from anyone else: a member - * can only announce itself. + * again replaces its addresses. service names the service a record is + * about, "://", and the router refuses a key that is not that + * service's by name or by type; a record naming no service is the + * member's own, under its peer key. provider is set only between routers, + * by the router that holds the member, and refused from anyone else: a + * member can only announce itself. * * @generated from message agentmesh.v1.DirectoryProvide */ @@ -727,6 +730,11 @@ export type DirectoryProvide = Message<"agentmesh.v1.DirectoryProvide"> & { * @generated from field: string provider = 3; */ provider: string; + + /** + * @generated from field: string service = 4; + */ + service: string; }; /** diff --git a/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.py b/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.py index 3c8dd1e9..2d9874bb 100644 --- a/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.py +++ b/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.py @@ -15,7 +15,7 @@ from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0f\x61gentmesh.proto\x12\x0c\x61gentmesh.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"4\n\tAuthFrame\x12\x0f\n\x07\x62iscuit\x18\x01 \x01(\x0c\x12\x16\n\x0etarget_service\x18\x02 \x01(\t\"?\n\x0c\x41uthResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x03 \x01(\x0c\"\xb1\x01\n\x0cRouterGoAway\x12\x31\n\x06reason\x18\x01 \x01(\x0e\x32!.agentmesh.v1.RouterGoAway.Reason\x12.\n\x0bretry_after\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\">\n\x06Reason\x12\x16\n\x12REASON_UNSPECIFIED\x10\x00\x12\x0c\n\x08\x44RAINING\x10\x01\x12\x0e\n\nOVERLOADED\x10\x02\"\xdc\x01\n\tMeshEvent\x12*\n\x04type\x18\x01 \x01(\x0e\x32\x1c.agentmesh.v1.MeshEvent.Type\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12.\n\nevent_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0enew_public_key\x18\x04 \x01(\x0c\x12\x11\n\tsignature\x18\x05 \x01(\x0c\"7\n\x04Type\x12\n\n\x06\x42\x41NNED\x10\x00\x12\x10\n\x0cKEY_ROTATION\x10\x01\x12\x11\n\rPOLICY_UPDATE\x10\x02\"\xf9\x01\n\rEnrollRequest\x12\x0b\n\x03jwt\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x12\n\npublic_key\x18\x03 \x01(\x0c\x12\x16\n\x0erequested_role\x18\x04 \x01(\t\x12\x37\n\x06labels\x18\x05 \x03(\x0b\x32\'.agentmesh.v1.EnrollRequest.LabelsEntry\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xab\x01\n\x0e\x45nrollResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12 \n\x18\x63ontrol_plane_public_key\x18\x03 \x01(\x0c\x12\x18\n\x10router_addresses\x18\x04 \x03(\t\x12/\n\x0b\x65xpire_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x97\x02\n\x16\x42ootstrapEnrollRequest\x12\x17\n\x0f\x62ootstrap_token\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x12\n\npublic_key\x18\x03 \x01(\x0c\x12\x16\n\x0erequested_role\x18\x04 \x01(\t\x12@\n\x06labels\x18\x05 \x03(\x0b\x32\x30.agentmesh.v1.BootstrapEnrollRequest.LabelsEntry\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x83\x02\n\x17\x42ootstrapEnrollResponse\x12.\n\x06status\x18\x01 \x01(\x0e\x32\x1e.agentmesh.v1.EnrollmentStatus\x12\x15\n\rbiscuit_token\x18\x02 \x01(\x0c\x12\x1d\n\x15poll_interval_seconds\x18\x03 \x01(\x05\x12\x15\n\rerror_message\x18\x04 \x01(\t\x12 \n\x18\x63ontrol_plane_public_key\x18\x05 \x01(\x0c\x12\x18\n\x10router_addresses\x18\x06 \x03(\t\x12/\n\x0b\x65xpire_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"Y\n\x0bServiceInfo\x12\'\n\x04type\x18\x01 \x01(\x0e\x32\x19.agentmesh.v1.ServiceType\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\"\x81\x01\n\x0e\x43ommandBackend\x12\x0f\n\x07\x63ommand\x18\x01 \x03(\t\x12\x32\n\x03\x65nv\x18\x02 \x03(\x0b\x32%.agentmesh.v1.CommandBackend.EnvEntry\x1a*\n\x08\x45nvEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x96\x01\n\x16RegisterServiceRequest\x12*\n\x07service\x18\x01 \x01(\x0b\x32\x19.agentmesh.v1.ServiceInfo\x12\x14\n\ntarget_url\x18\x02 \x01(\tH\x00\x12/\n\x07\x63ommand\x18\x03 \x01(\x0b\x32\x1c.agentmesh.v1.CommandBackendH\x00\x42\t\n\x07\x62\x61\x63kend\"i\n\x12\x44iscoveredProvider\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x17\n\x0flocal_proxy_url\x18\x02 \x01(\t\x12\x10\n\x08srv_name\x18\x03 \x01(\t\x12\x17\n\x0fsrv_description\x18\x04 \x01(\t\"\xe8\x01\n\x18\x43ontrolPlaneInfoResponse\x12\x13\n\x0boidc_issuer\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x18\n\x10router_addresses\x18\x04 \x03(\t\x12\x17\n\x0f\x62\x61nned_peer_ids\x18\x05 \x03(\t\x12)\n\x07routers\x18\x06 \x03(\x0b\x32\x18.agentmesh.v1.RouterInfo\x12\x34\n\tdirectory\x18\x07 \x01(\x0b\x32!.agentmesh.v1.DirectoryParameters\"\x90\x01\n\x13\x44irectoryParameters\x12\x13\n\x0breplication\x18\x01 \x01(\x05\x12\x35\n\x12reprovide_interval\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\x12-\n\nrecord_ttl\x18\x03 \x01(\x0b\x32\x19.google.protobuf.Duration\"\xfb\x01\n\x10\x44irectoryRequest\x12\x31\n\x07provide\x18\x01 \x01(\x0b\x32\x1e.agentmesh.v1.DirectoryProvideH\x00\x12>\n\x0e\x66ind_providers\x18\x02 \x01(\x0b\x32$.agentmesh.v1.DirectoryFindProvidersH\x00\x12\x34\n\tfind_peer\x18\x03 \x01(\x0b\x32\x1f.agentmesh.v1.DirectoryFindPeerH\x00\x12\x33\n\x08withdraw\x18\x04 \x01(\x0b\x32\x1f.agentmesh.v1.DirectoryWithdrawH\x00\x42\t\n\x07request\"@\n\x10\x44irectoryProvide\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\r\n\x05\x61\x64\x64rs\x18\x02 \x03(\t\x12\x10\n\x08provider\x18\x03 \x01(\t\"2\n\x11\x44irectoryWithdraw\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\x10\n\x08provider\x18\x02 \x01(\t\"4\n\x16\x44irectoryFindProviders\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\r\n\x05limit\x18\x02 \x01(\r\"$\n\x11\x44irectoryFindPeer\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\"w\n\x11\x44irectoryResponse\x12+\n\tproviders\x18\x01 \x03(\x0b\x32\x18.agentmesh.v1.PeerRecord\x12&\n\x04peer\x18\x02 \x01(\x0b\x32\x18.agentmesh.v1.PeerRecord\x12\r\n\x05\x65rror\x18\x03 \x01(\t\",\n\nPeerRecord\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\r\n\x05\x61\x64\x64rs\x18\x02 \x03(\t\"\xc4\x01\n\nRouterInfo\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x11\n\taddresses\x18\x02 \x03(\t\x12\x34\n\x06labels\x18\x03 \x03(\x0b\x32$.agentmesh.v1.RouterInfo.LabelsEntry\x12\x13\n\x0b\x63onnections\x18\x04 \x01(\x05\x12\x18\n\x10\x63onnection_limit\x18\x05 \x01(\x05\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x83\x02\n\x12RouterLeaseRequest\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x11\n\taddresses\x18\x02 \x03(\t\x12\x0f\n\x07\x62iscuit\x18\x03 \x01(\x0c\x12\x17\n\x0f\x63onnected_peers\x18\x04 \x03(\t\x12\x10\n\x08\x64ht_size\x18\x05 \x01(\x05\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\x12&\n\x03ttl\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x13\n\x0b\x63onnections\x18\t \x01(\x05\x12\x18\n\x10\x63onnection_limit\x18\n \x01(\x05\"f\n\x13RouterLeaseResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xa4\x01\n\nPolicyRole\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x17\n\x0f\x61llowed_targets\x18\x02 \x03(\t\x12\x18\n\x10\x61llowed_services\x18\x03 \x03(\t\x12\x16\n\x0e\x63ustom_datalog\x18\x04 \x03(\t\x12\x16\n\x0e\x61llowed_labels\x18\x05 \x03(\t\x12%\n\x04http\x18\x06 \x03(\x0b\x32\x17.agentmesh.v1.HTTPGrant\"<\n\tHTTPGrant\x12\x0f\n\x07service\x18\x01 \x01(\t\x12\x0f\n\x07methods\x18\x02 \x03(\t\x12\r\n\x05paths\x18\x03 \x03(\t\"\xa1\x02\n\x11\x45gressDestination\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x12\n\ntarget_url\x18\x02 \x01(\t\x12\x12\n\ncredential\x18\x03 \x01(\t\x12\x11\n\tserved_by\x18\x04 \x03(\t\x12.\n\x06\x62roker\x18\x05 \x01(\x0b\x32\x1e.agentmesh.v1.CredentialBroker\x12,\n\ninspection\x18\x06 \x01(\x0b\x32\x18.agentmesh.v1.Inspection\x12&\n\x04mode\x18\x07 \x01(\x0e\x32\x18.agentmesh.v1.EgressMode\x12\r\n\x05ports\x18\x08 \x03(\r\x12\x15\n\rpreserve_host\x18\t \x01(\x08\x12\x17\n\x0f\x66orward_context\x18\n \x01(\x08\"9\n\nInspection\x12+\n\ninspectors\x18\x01 \x03(\x0b\x32\x17.agentmesh.v1.Inspector\"o\n\tInspector\x12/\n\x0bmodel_armor\x18\x01 \x01(\x0b\x32\x18.agentmesh.v1.ModelArmorH\x00\x12)\n\x08\x65xt_proc\x18\x02 \x01(\x0b\x32\x15.agentmesh.v1.ExtProcH\x00\x42\x06\n\x04kind\"\x91\x01\n\nModelArmor\x12\x10\n\x08template\x18\x01 \x01(\t\x12\x32\n\x08response\x18\x02 \x01(\x0e\x32 .agentmesh.v1.ResponseInspection\x12\x11\n\tfail_open\x18\x03 \x01(\x08\x12*\n\x07timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\"\x88\x02\n\x07\x45xtProc\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\n\n\x02\x63\x61\x18\x02 \x01(\t\x12\x1a\n\x12\x63lient_certificate\x18\x03 \x01(\t\x12<\n\x0fprocessing_mode\x18\x04 \x01(\x0b\x32#.agentmesh.v1.ExtProcProcessingMode\x12\x1b\n\x13\x61llow_mode_override\x18\x05 \x01(\x08\x12\x32\n\x0fmessage_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x1a\n\x12\x66\x61ilure_mode_allow\x18\x07 \x01(\x08\x12\x1a\n\x12max_buffered_bytes\x18\x08 \x01(\r\"\xff\x04\n\x15\x45xtProcProcessingMode\x12K\n\x13request_header_mode\x18\x01 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\x12L\n\x14response_header_mode\x18\x02 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\x12G\n\x11request_body_mode\x18\x03 \x01(\x0e\x32,.agentmesh.v1.ExtProcProcessingMode.BodyMode\x12H\n\x12response_body_mode\x18\x04 \x01(\x0e\x32,.agentmesh.v1.ExtProcProcessingMode.BodyMode\x12L\n\x14request_trailer_mode\x18\x05 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\x12M\n\x15response_trailer_mode\x18\x06 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\"9\n\nHeaderMode\x12\x17\n\x13HEADER_MODE_DEFAULT\x10\x00\x12\x08\n\x04SEND\x10\x01\x12\x08\n\x04SKIP\x10\x02\"`\n\x08\x42odyMode\x12\x08\n\x04NONE\x10\x00\x12\x0c\n\x08STREAMED\x10\x01\x12\x0c\n\x08\x42UFFERED\x10\x02\x12\x14\n\x10\x42UFFERED_PARTIAL\x10\x03\x12\x18\n\x14\x46ULL_DUPLEX_STREAMED\x10\x04\"\xe1\x01\n\x10\x43redentialBroker\x12\x17\n\rstatic_secret\x18\x01 \x01(\tH\x00\x12\x37\n\x0foidc_federation\x18\x02 \x01(\x0b\x32\x1c.agentmesh.v1.OIDCFederationH\x00\x12\x36\n\x0f\x61ws_assume_role\x18\x03 \x01(\x0b\x32\x1b.agentmesh.v1.AWSAssumeRoleH\x00\x12;\n\x11platform_identity\x18\x04 \x01(\x0b\x32\x1e.agentmesh.v1.PlatformIdentityH\x00\x42\x06\n\x04kind\"_\n\x0eOIDCFederation\x12\x16\n\x0etoken_endpoint\x18\x01 \x01(\t\x12\x10\n\x08\x61udience\x18\x02 \x01(\t\x12\x13\n\x0bimpersonate\x18\x03 \x01(\t\x12\x0e\n\x06scopes\x18\x04 \x03(\t\"9\n\rAWSAssumeRole\x12\x10\n\x08role_arn\x18\x01 \x01(\t\x12\x16\n\x0esession_policy\x18\x02 \x01(\t\"\"\n\x10PlatformIdentity\x12\x0e\n\x06scopes\x18\x01 \x03(\t\".\n\rPolicyBinding\x12\x0c\n\x04role\x18\x01 \x01(\t\x12\x0f\n\x07members\x18\x02 \x03(\t\"\x97\x01\n\x0cPolicyConfig\x12\'\n\x05roles\x18\x01 \x03(\x0b\x32\x18.agentmesh.v1.PolicyRole\x12-\n\x08\x62indings\x18\x02 \x03(\x0b\x32\x1b.agentmesh.v1.PolicyBinding\x12/\n\x06\x65gress\x18\x03 \x03(\x0b\x32\x1f.agentmesh.v1.EgressDestination\"\x18\n\x16PolicyConfigGetRequest\"0\n\x17PolicyConfigGetResponse\x12\x15\n\rdatalog_rules\x18\x01 \x03(\t\"<\n\x1aPolicyConfigUpdateResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x1a\n\x18\x45gressAssignmentsRequest\"L\n\x19\x45gressAssignmentsResponse\x12/\n\x06\x65gress\x18\x01 \x03(\x0b\x32\x1f.agentmesh.v1.EgressDestination\"f\n\x0cKeysResponse\x12\x13\n\x0bpublic_keys\x18\x01 \x03(\x0c\x12-\n\tsign_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x12\n\nsignatures\x18\x03 \x03(\x0c\"k\n\x13TokenRefreshRequest\x12\x1b\n\x13\x63hallenge_signature\x18\x01 \x01(\x0c\x12\x19\n\x11\x63hallenge_unix_ms\x18\x02 \x01(\x03\x12\x0f\n\x07peer_id\x18\x03 \x01(\t\x12\x0b\n\x03jwt\x18\x04 \x01(\t\"u\n\x14TokenRefreshResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x15\n\rerror_message\x18\x03 \x01(\t\"@\n\x11NodeCatalogReport\x12+\n\x08services\x18\x01 \x03(\x0b\x32\x19.agentmesh.v1.ServiceInfo\"%\n\x12TokenRevokeRequest\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\"5\n\x13TokenRevokeResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x96\x01\n\x1b\x42ootstrapTokenCreateRequest\x12\x0c\n\x04role\x18\x01 \x01(\t\x12\x10\n\x08owner_id\x18\x02 \x01(\t\x12\x11\n\tttl_hours\x18\x03 \x01(\x05\x12\x12\n\nmax_usages\x18\x04 \x01(\x05\x12\x13\n\x0b\x64\x65scription\x18\x05 \x01(\t\x12\x1b\n\x13\x61utonomous_recovery\x18\x06 \x01(\x08\"\x8a\x01\n\x1c\x42ootstrapTokenCreateResponse\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\t\x12\x0c\n\x04role\x18\x03 \x01(\t\x12\x10\n\x08owner_id\x18\x04 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xab\x02\n\x0e\x42ootstrapToken\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04role\x18\x02 \x01(\t\x12\x10\n\x08owner_id\x18\x03 \x01(\t\x12\x12\n\nmax_usages\x18\x04 \x01(\x05\x12\x14\n\x0cusages_count\x18\x05 \x01(\x05\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12/\n\x0b\x63reate_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0brevoke_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x1b\n\x13\x61utonomous_recovery\x18\n \x01(\x08\"J\n\x1a\x42ootstrapTokenListResponse\x12,\n\x06tokens\x18\x01 \x03(\x0b\x32\x1c.agentmesh.v1.BootstrapToken\"\xd6\x02\n\x11\x45nrollmentRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x10\n\x08token_id\x18\x03 \x01(\t\x12.\n\x06status\x18\x04 \x01(\x0e\x32\x1e.agentmesh.v1.EnrollmentStatus\x12;\n\x06labels\x18\x05 \x03(\x0b\x32+.agentmesh.v1.EnrollmentRequest.LabelsEntry\x12/\n\x0b\x63reate_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0cresolve_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x13\n\x0bresolved_by\x18\x08 \x01(\t\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"R\n\x1d\x45nrollmentRequestListResponse\x12\x31\n\x08requests\x18\x01 \x03(\x0b\x32\x1f.agentmesh.v1.EnrollmentRequest\"p\n\x04User\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0e\n\x06issuer\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12\x0c\n\x04role\x18\x04 \x01(\t\x12/\n\x0b\x63reate_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xe3\x02\n\x0c\x45nrolledNode\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0c\n\x04role\x18\x02 \x01(\t\x12\x17\n\x0f\x65nrollment_type\x18\x03 \x01(\t\x12\x13\n\x0b\x63laims_json\x18\x04 \x01(\t\x12\x10\n\x08owner_id\x18\x05 \x01(\t\x12\x36\n\x06labels\x18\x06 \x03(\x0b\x32&.agentmesh.v1.EnrolledNode.LabelsEntry\x12/\n\x0b\x65nroll_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06\x62\x61nned\x18\t \x01(\x08\x12\x1b\n\x13\x61utonomous_recovery\x18\n \x01(\x08\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc4\x01\n\x0bRouterLease\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x11\n\taddresses\x18\x02 \x03(\t\x12\x35\n\x11last_renewal_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x17\n\x0f\x63onnected_peers\x18\x05 \x03(\t\x12\x10\n\x08\x64ht_size\x18\x06 \x01(\x05\"l\n\x0cNodeServices\x12+\n\x08services\x18\x01 \x03(\x0b\x32\x19.agentmesh.v1.ServiceInfo\x12/\n\x0breport_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xdb\x03\n\x13\x41\x64minStatusResponse\x12!\n\x05users\x18\x01 \x03(\x0b\x32\x12.agentmesh.v1.User\x12\x31\n\x0e\x61\x63tive_routers\x18\x02 \x03(\x0b\x32\x19.agentmesh.v1.RouterLease\x12\x32\n\x0e\x65nrolled_nodes\x18\x03 \x03(\x0b\x32\x1a.agentmesh.v1.EnrolledNode\x12<\n\x13\x65nrollment_requests\x18\x04 \x03(\x0b\x32\x1f.agentmesh.v1.EnrollmentRequest\x12\x36\n\x10\x62ootstrap_tokens\x18\x05 \x03(\x0b\x32\x1c.agentmesh.v1.BootstrapToken\x12*\n\x06policy\x18\x06 \x01(\x0b\x32\x1a.agentmesh.v1.PolicyConfig\x12H\n\x0cnode_catalog\x18\x07 \x03(\x0b\x32\x32.agentmesh.v1.AdminStatusResponse.NodeCatalogEntry\x1aN\n\x10NodeCatalogEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.agentmesh.v1.NodeServices:\x02\x38\x01\"\x81\x02\n\x12UserStatusResponse\x12 \n\x04user\x18\x01 \x01(\x0b\x32\x12.agentmesh.v1.User\x12\x32\n\x0e\x65nrolled_nodes\x18\x02 \x03(\x0b\x32\x1a.agentmesh.v1.EnrolledNode\x12\x36\n\x10\x62ootstrap_tokens\x18\x03 \x03(\x0b\x32\x1c.agentmesh.v1.BootstrapToken\x12\x31\n\x0e\x61\x63tive_routers\x18\x04 \x03(\x0b\x32\x19.agentmesh.v1.RouterLease\x12*\n\x06policy\x18\x05 \x01(\x0b\x32\x1a.agentmesh.v1.PolicyConfig\"\xe4\x01\n\x18IdentityEvidenceResponse\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12\x37\n\x13\x62iscuit_expire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x19\n\x11\x63ontrol_plane_url\x18\x04 \x01(\t\x12\"\n\x1atrusted_control_plane_keys\x18\x05 \x03(\x0c\x12.\n\ncheck_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xc6\x02\n\x14PeerEvidenceResponse\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12\x15\n\rverifying_key\x18\x03 \x01(\x0c\x12\r\n\x05roles\x18\x04 \x03(\t\x12>\n\x06labels\x18\x05 \x03(\x0b\x32..agentmesh.v1.PeerEvidenceResponse.LabelsEntry\x12/\n\x0b\x65xpire_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0erevocation_ids\x18\x07 \x03(\t\x12.\n\ncheck_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8c\x02\n\x10MemberCredential\x12\x19\n\x11\x63ontrol_plane_url\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x35\n\x0ctrusted_keys\x18\x04 \x03(\x0b\x32\x1f.agentmesh.v1.TrustedSigningKey\x12\x19\n\x11issued_under_keys\x18\x05 \x03(\x0c\x12\x18\n\x10router_addresses\x18\x06 \x03(\t\x12/\n\x0coidc_session\x18\x07 \x01(\x0b\x32\x19.agentmesh.v1.OIDCSession\"Y\n\x11TrustedSigningKey\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\x12\x30\n\x0creceive_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"Y\n\x0bOIDCSession\x12\x0e\n\x06issuer\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x15\n\rrefresh_token\x18\x04 \x01(\t\"\x93\x01\n\x15TaskAuthorizationRule\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12%\n\x05rules\x18\x03 \x03(\x0b\x32\x16.agentmesh.v1.TaskRule\x12/\n\x0b\x65xpire_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x84\x01\n\x08TaskRule\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x18\n\x10\x61llowed_services\x18\x02 \x03(\t\x12.\n\toperation\x18\x03 \x01(\x0b\x32\x1b.agentmesh.v1.TaskOperation\x12\x19\n\x11\x61llowed_resources\x18\x04 \x03(\t\"s\n\rTaskOperation\x12\x15\n\rallowed_tools\x18\x01 \x03(\t\x12\x17\n\x0f\x61llowed_methods\x18\x02 \x03(\t\x12\x15\n\rallowed_paths\x18\x03 \x03(\t\x12\x1b\n\x13\x61llowed_permissions\x18\x04 \x03(\t\"\xab\x01\n\x14TokenExchangeRequest\x12\x15\n\rsubject_token\x18\x01 \x01(\t\x12\x36\n\ttask_rule\x18\x02 \x01(\x0b\x32#.agentmesh.v1.TaskAuthorizationRule\x12\x0c\n\x04seal\x18\x03 \x01(\x08\x12\x19\n\x11\x63hallenge_unix_ms\x18\x04 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x05 \x01(\x0c\"\x7f\n\x15TokenExchangeResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\r\n\x05roles\x18\x03 \x03(\t\x12\x0f\n\x07subject\x18\x04 \x01(\t\"\x81\x01\n\x0fSTSTokenRequest\x12\x0f\n\x07\x62iscuit\x18\x01 \x01(\x0c\x12\x13\n\x0b\x64\x65stination\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x19\n\x11\x63hallenge_unix_ms\x18\x04 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x05 \x01(\x0c\"\x83\x01\n\x10STSTokenResponse\x12\x0b\n\x03jwt\x18\x01 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0f\n\x07subject\x18\x03 \x01(\t\x12\r\n\x05roles\x18\x04 \x03(\t\x12\x11\n\ttask_name\x18\x05 \x01(\t\"F\n\x13RevocationsResponse\x12\x16\n\x0erevocation_ids\x18\x01 \x03(\t\x12\x17\n\x0f\x62\x61nned_peer_ids\x18\x02 \x03(\t*\x94\x01\n\x10\x45nrollmentStatus\x12!\n\x1d\x45NROLLMENT_STATUS_UNSPECIFIED\x10\x00\x12\x1d\n\x19\x45NROLLMENT_STATUS_PENDING\x10\x01\x12\x1e\n\x1a\x45NROLLMENT_STATUS_APPROVED\x10\x02\x12\x1e\n\x1a\x45NROLLMENT_STATUS_REJECTED\x10\x03*\x8c\x01\n\x0bServiceType\x12\x1c\n\x18SERVICE_TYPE_UNSPECIFIED\x10\x00\x12\x14\n\x10SERVICE_TYPE_MCP\x10\x01\x12\x1a\n\x16SERVICE_TYPE_INFERENCE\x10\x02\x12\x14\n\x10SERVICE_TYPE_A2A\x10\x03\x12\x17\n\x13SERVICE_TYPE_EGRESS\x10\x04*7\n\nEgressMode\x12\x14\n\x10\x45GRESS_MODE_HTTP\x10\x00\x12\x13\n\x0f\x45GRESS_MODE_TCP\x10\x01*\\\n\x12ResponseInspection\x12 \n\x1cRESPONSE_INSPECTION_BUFFERED\x10\x00\x12$\n RESPONSE_INSPECTION_REQUEST_ONLY\x10\x01\x42!Z\x1fgithub.com/google/agentmesh/apib\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0f\x61gentmesh.proto\x12\x0c\x61gentmesh.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"4\n\tAuthFrame\x12\x0f\n\x07\x62iscuit\x18\x01 \x01(\x0c\x12\x16\n\x0etarget_service\x18\x02 \x01(\t\"?\n\x0c\x41uthResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x03 \x01(\x0c\"\xb1\x01\n\x0cRouterGoAway\x12\x31\n\x06reason\x18\x01 \x01(\x0e\x32!.agentmesh.v1.RouterGoAway.Reason\x12.\n\x0bretry_after\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\">\n\x06Reason\x12\x16\n\x12REASON_UNSPECIFIED\x10\x00\x12\x0c\n\x08\x44RAINING\x10\x01\x12\x0e\n\nOVERLOADED\x10\x02\"\xdc\x01\n\tMeshEvent\x12*\n\x04type\x18\x01 \x01(\x0e\x32\x1c.agentmesh.v1.MeshEvent.Type\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12.\n\nevent_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0enew_public_key\x18\x04 \x01(\x0c\x12\x11\n\tsignature\x18\x05 \x01(\x0c\"7\n\x04Type\x12\n\n\x06\x42\x41NNED\x10\x00\x12\x10\n\x0cKEY_ROTATION\x10\x01\x12\x11\n\rPOLICY_UPDATE\x10\x02\"\xf9\x01\n\rEnrollRequest\x12\x0b\n\x03jwt\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x12\n\npublic_key\x18\x03 \x01(\x0c\x12\x16\n\x0erequested_role\x18\x04 \x01(\t\x12\x37\n\x06labels\x18\x05 \x03(\x0b\x32\'.agentmesh.v1.EnrollRequest.LabelsEntry\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xab\x01\n\x0e\x45nrollResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12 \n\x18\x63ontrol_plane_public_key\x18\x03 \x01(\x0c\x12\x18\n\x10router_addresses\x18\x04 \x03(\t\x12/\n\x0b\x65xpire_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x97\x02\n\x16\x42ootstrapEnrollRequest\x12\x17\n\x0f\x62ootstrap_token\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x12\n\npublic_key\x18\x03 \x01(\x0c\x12\x16\n\x0erequested_role\x18\x04 \x01(\t\x12@\n\x06labels\x18\x05 \x03(\x0b\x32\x30.agentmesh.v1.BootstrapEnrollRequest.LabelsEntry\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x83\x02\n\x17\x42ootstrapEnrollResponse\x12.\n\x06status\x18\x01 \x01(\x0e\x32\x1e.agentmesh.v1.EnrollmentStatus\x12\x15\n\rbiscuit_token\x18\x02 \x01(\x0c\x12\x1d\n\x15poll_interval_seconds\x18\x03 \x01(\x05\x12\x15\n\rerror_message\x18\x04 \x01(\t\x12 \n\x18\x63ontrol_plane_public_key\x18\x05 \x01(\x0c\x12\x18\n\x10router_addresses\x18\x06 \x03(\t\x12/\n\x0b\x65xpire_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"Y\n\x0bServiceInfo\x12\'\n\x04type\x18\x01 \x01(\x0e\x32\x19.agentmesh.v1.ServiceType\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\"\x81\x01\n\x0e\x43ommandBackend\x12\x0f\n\x07\x63ommand\x18\x01 \x03(\t\x12\x32\n\x03\x65nv\x18\x02 \x03(\x0b\x32%.agentmesh.v1.CommandBackend.EnvEntry\x1a*\n\x08\x45nvEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x96\x01\n\x16RegisterServiceRequest\x12*\n\x07service\x18\x01 \x01(\x0b\x32\x19.agentmesh.v1.ServiceInfo\x12\x14\n\ntarget_url\x18\x02 \x01(\tH\x00\x12/\n\x07\x63ommand\x18\x03 \x01(\x0b\x32\x1c.agentmesh.v1.CommandBackendH\x00\x42\t\n\x07\x62\x61\x63kend\"i\n\x12\x44iscoveredProvider\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x17\n\x0flocal_proxy_url\x18\x02 \x01(\t\x12\x10\n\x08srv_name\x18\x03 \x01(\t\x12\x17\n\x0fsrv_description\x18\x04 \x01(\t\"\xe8\x01\n\x18\x43ontrolPlaneInfoResponse\x12\x13\n\x0boidc_issuer\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x18\n\x10router_addresses\x18\x04 \x03(\t\x12\x17\n\x0f\x62\x61nned_peer_ids\x18\x05 \x03(\t\x12)\n\x07routers\x18\x06 \x03(\x0b\x32\x18.agentmesh.v1.RouterInfo\x12\x34\n\tdirectory\x18\x07 \x01(\x0b\x32!.agentmesh.v1.DirectoryParameters\"\x90\x01\n\x13\x44irectoryParameters\x12\x13\n\x0breplication\x18\x01 \x01(\x05\x12\x35\n\x12reprovide_interval\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\x12-\n\nrecord_ttl\x18\x03 \x01(\x0b\x32\x19.google.protobuf.Duration\"\xfb\x01\n\x10\x44irectoryRequest\x12\x31\n\x07provide\x18\x01 \x01(\x0b\x32\x1e.agentmesh.v1.DirectoryProvideH\x00\x12>\n\x0e\x66ind_providers\x18\x02 \x01(\x0b\x32$.agentmesh.v1.DirectoryFindProvidersH\x00\x12\x34\n\tfind_peer\x18\x03 \x01(\x0b\x32\x1f.agentmesh.v1.DirectoryFindPeerH\x00\x12\x33\n\x08withdraw\x18\x04 \x01(\x0b\x32\x1f.agentmesh.v1.DirectoryWithdrawH\x00\x42\t\n\x07request\"Q\n\x10\x44irectoryProvide\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\r\n\x05\x61\x64\x64rs\x18\x02 \x03(\t\x12\x10\n\x08provider\x18\x03 \x01(\t\x12\x0f\n\x07service\x18\x04 \x01(\t\"2\n\x11\x44irectoryWithdraw\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\x10\n\x08provider\x18\x02 \x01(\t\"4\n\x16\x44irectoryFindProviders\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\r\n\x05limit\x18\x02 \x01(\r\"$\n\x11\x44irectoryFindPeer\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\"w\n\x11\x44irectoryResponse\x12+\n\tproviders\x18\x01 \x03(\x0b\x32\x18.agentmesh.v1.PeerRecord\x12&\n\x04peer\x18\x02 \x01(\x0b\x32\x18.agentmesh.v1.PeerRecord\x12\r\n\x05\x65rror\x18\x03 \x01(\t\",\n\nPeerRecord\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\r\n\x05\x61\x64\x64rs\x18\x02 \x03(\t\"\xc4\x01\n\nRouterInfo\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x11\n\taddresses\x18\x02 \x03(\t\x12\x34\n\x06labels\x18\x03 \x03(\x0b\x32$.agentmesh.v1.RouterInfo.LabelsEntry\x12\x13\n\x0b\x63onnections\x18\x04 \x01(\x05\x12\x18\n\x10\x63onnection_limit\x18\x05 \x01(\x05\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x83\x02\n\x12RouterLeaseRequest\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x11\n\taddresses\x18\x02 \x03(\t\x12\x0f\n\x07\x62iscuit\x18\x03 \x01(\x0c\x12\x17\n\x0f\x63onnected_peers\x18\x04 \x03(\t\x12\x10\n\x08\x64ht_size\x18\x05 \x01(\x05\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\x12&\n\x03ttl\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x13\n\x0b\x63onnections\x18\t \x01(\x05\x12\x18\n\x10\x63onnection_limit\x18\n \x01(\x05\"f\n\x13RouterLeaseResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xa4\x01\n\nPolicyRole\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x17\n\x0f\x61llowed_targets\x18\x02 \x03(\t\x12\x18\n\x10\x61llowed_services\x18\x03 \x03(\t\x12\x16\n\x0e\x63ustom_datalog\x18\x04 \x03(\t\x12\x16\n\x0e\x61llowed_labels\x18\x05 \x03(\t\x12%\n\x04http\x18\x06 \x03(\x0b\x32\x17.agentmesh.v1.HTTPGrant\"<\n\tHTTPGrant\x12\x0f\n\x07service\x18\x01 \x01(\t\x12\x0f\n\x07methods\x18\x02 \x03(\t\x12\r\n\x05paths\x18\x03 \x03(\t\"\xa1\x02\n\x11\x45gressDestination\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x12\n\ntarget_url\x18\x02 \x01(\t\x12\x12\n\ncredential\x18\x03 \x01(\t\x12\x11\n\tserved_by\x18\x04 \x03(\t\x12.\n\x06\x62roker\x18\x05 \x01(\x0b\x32\x1e.agentmesh.v1.CredentialBroker\x12,\n\ninspection\x18\x06 \x01(\x0b\x32\x18.agentmesh.v1.Inspection\x12&\n\x04mode\x18\x07 \x01(\x0e\x32\x18.agentmesh.v1.EgressMode\x12\r\n\x05ports\x18\x08 \x03(\r\x12\x15\n\rpreserve_host\x18\t \x01(\x08\x12\x17\n\x0f\x66orward_context\x18\n \x01(\x08\"9\n\nInspection\x12+\n\ninspectors\x18\x01 \x03(\x0b\x32\x17.agentmesh.v1.Inspector\"o\n\tInspector\x12/\n\x0bmodel_armor\x18\x01 \x01(\x0b\x32\x18.agentmesh.v1.ModelArmorH\x00\x12)\n\x08\x65xt_proc\x18\x02 \x01(\x0b\x32\x15.agentmesh.v1.ExtProcH\x00\x42\x06\n\x04kind\"\x91\x01\n\nModelArmor\x12\x10\n\x08template\x18\x01 \x01(\t\x12\x32\n\x08response\x18\x02 \x01(\x0e\x32 .agentmesh.v1.ResponseInspection\x12\x11\n\tfail_open\x18\x03 \x01(\x08\x12*\n\x07timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\"\x88\x02\n\x07\x45xtProc\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\n\n\x02\x63\x61\x18\x02 \x01(\t\x12\x1a\n\x12\x63lient_certificate\x18\x03 \x01(\t\x12<\n\x0fprocessing_mode\x18\x04 \x01(\x0b\x32#.agentmesh.v1.ExtProcProcessingMode\x12\x1b\n\x13\x61llow_mode_override\x18\x05 \x01(\x08\x12\x32\n\x0fmessage_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x1a\n\x12\x66\x61ilure_mode_allow\x18\x07 \x01(\x08\x12\x1a\n\x12max_buffered_bytes\x18\x08 \x01(\r\"\xff\x04\n\x15\x45xtProcProcessingMode\x12K\n\x13request_header_mode\x18\x01 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\x12L\n\x14response_header_mode\x18\x02 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\x12G\n\x11request_body_mode\x18\x03 \x01(\x0e\x32,.agentmesh.v1.ExtProcProcessingMode.BodyMode\x12H\n\x12response_body_mode\x18\x04 \x01(\x0e\x32,.agentmesh.v1.ExtProcProcessingMode.BodyMode\x12L\n\x14request_trailer_mode\x18\x05 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\x12M\n\x15response_trailer_mode\x18\x06 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\"9\n\nHeaderMode\x12\x17\n\x13HEADER_MODE_DEFAULT\x10\x00\x12\x08\n\x04SEND\x10\x01\x12\x08\n\x04SKIP\x10\x02\"`\n\x08\x42odyMode\x12\x08\n\x04NONE\x10\x00\x12\x0c\n\x08STREAMED\x10\x01\x12\x0c\n\x08\x42UFFERED\x10\x02\x12\x14\n\x10\x42UFFERED_PARTIAL\x10\x03\x12\x18\n\x14\x46ULL_DUPLEX_STREAMED\x10\x04\"\xe1\x01\n\x10\x43redentialBroker\x12\x17\n\rstatic_secret\x18\x01 \x01(\tH\x00\x12\x37\n\x0foidc_federation\x18\x02 \x01(\x0b\x32\x1c.agentmesh.v1.OIDCFederationH\x00\x12\x36\n\x0f\x61ws_assume_role\x18\x03 \x01(\x0b\x32\x1b.agentmesh.v1.AWSAssumeRoleH\x00\x12;\n\x11platform_identity\x18\x04 \x01(\x0b\x32\x1e.agentmesh.v1.PlatformIdentityH\x00\x42\x06\n\x04kind\"_\n\x0eOIDCFederation\x12\x16\n\x0etoken_endpoint\x18\x01 \x01(\t\x12\x10\n\x08\x61udience\x18\x02 \x01(\t\x12\x13\n\x0bimpersonate\x18\x03 \x01(\t\x12\x0e\n\x06scopes\x18\x04 \x03(\t\"9\n\rAWSAssumeRole\x12\x10\n\x08role_arn\x18\x01 \x01(\t\x12\x16\n\x0esession_policy\x18\x02 \x01(\t\"\"\n\x10PlatformIdentity\x12\x0e\n\x06scopes\x18\x01 \x03(\t\".\n\rPolicyBinding\x12\x0c\n\x04role\x18\x01 \x01(\t\x12\x0f\n\x07members\x18\x02 \x03(\t\"\x97\x01\n\x0cPolicyConfig\x12\'\n\x05roles\x18\x01 \x03(\x0b\x32\x18.agentmesh.v1.PolicyRole\x12-\n\x08\x62indings\x18\x02 \x03(\x0b\x32\x1b.agentmesh.v1.PolicyBinding\x12/\n\x06\x65gress\x18\x03 \x03(\x0b\x32\x1f.agentmesh.v1.EgressDestination\"\x18\n\x16PolicyConfigGetRequest\"0\n\x17PolicyConfigGetResponse\x12\x15\n\rdatalog_rules\x18\x01 \x03(\t\"<\n\x1aPolicyConfigUpdateResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x1a\n\x18\x45gressAssignmentsRequest\"L\n\x19\x45gressAssignmentsResponse\x12/\n\x06\x65gress\x18\x01 \x03(\x0b\x32\x1f.agentmesh.v1.EgressDestination\"f\n\x0cKeysResponse\x12\x13\n\x0bpublic_keys\x18\x01 \x03(\x0c\x12-\n\tsign_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x12\n\nsignatures\x18\x03 \x03(\x0c\"k\n\x13TokenRefreshRequest\x12\x1b\n\x13\x63hallenge_signature\x18\x01 \x01(\x0c\x12\x19\n\x11\x63hallenge_unix_ms\x18\x02 \x01(\x03\x12\x0f\n\x07peer_id\x18\x03 \x01(\t\x12\x0b\n\x03jwt\x18\x04 \x01(\t\"u\n\x14TokenRefreshResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x15\n\rerror_message\x18\x03 \x01(\t\"@\n\x11NodeCatalogReport\x12+\n\x08services\x18\x01 \x03(\x0b\x32\x19.agentmesh.v1.ServiceInfo\"%\n\x12TokenRevokeRequest\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\"5\n\x13TokenRevokeResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x96\x01\n\x1b\x42ootstrapTokenCreateRequest\x12\x0c\n\x04role\x18\x01 \x01(\t\x12\x10\n\x08owner_id\x18\x02 \x01(\t\x12\x11\n\tttl_hours\x18\x03 \x01(\x05\x12\x12\n\nmax_usages\x18\x04 \x01(\x05\x12\x13\n\x0b\x64\x65scription\x18\x05 \x01(\t\x12\x1b\n\x13\x61utonomous_recovery\x18\x06 \x01(\x08\"\x8a\x01\n\x1c\x42ootstrapTokenCreateResponse\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\t\x12\x0c\n\x04role\x18\x03 \x01(\t\x12\x10\n\x08owner_id\x18\x04 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xab\x02\n\x0e\x42ootstrapToken\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04role\x18\x02 \x01(\t\x12\x10\n\x08owner_id\x18\x03 \x01(\t\x12\x12\n\nmax_usages\x18\x04 \x01(\x05\x12\x14\n\x0cusages_count\x18\x05 \x01(\x05\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12/\n\x0b\x63reate_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0brevoke_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x1b\n\x13\x61utonomous_recovery\x18\n \x01(\x08\"J\n\x1a\x42ootstrapTokenListResponse\x12,\n\x06tokens\x18\x01 \x03(\x0b\x32\x1c.agentmesh.v1.BootstrapToken\"\xd6\x02\n\x11\x45nrollmentRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x10\n\x08token_id\x18\x03 \x01(\t\x12.\n\x06status\x18\x04 \x01(\x0e\x32\x1e.agentmesh.v1.EnrollmentStatus\x12;\n\x06labels\x18\x05 \x03(\x0b\x32+.agentmesh.v1.EnrollmentRequest.LabelsEntry\x12/\n\x0b\x63reate_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0cresolve_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x13\n\x0bresolved_by\x18\x08 \x01(\t\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"R\n\x1d\x45nrollmentRequestListResponse\x12\x31\n\x08requests\x18\x01 \x03(\x0b\x32\x1f.agentmesh.v1.EnrollmentRequest\"p\n\x04User\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0e\n\x06issuer\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12\x0c\n\x04role\x18\x04 \x01(\t\x12/\n\x0b\x63reate_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xe3\x02\n\x0c\x45nrolledNode\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0c\n\x04role\x18\x02 \x01(\t\x12\x17\n\x0f\x65nrollment_type\x18\x03 \x01(\t\x12\x13\n\x0b\x63laims_json\x18\x04 \x01(\t\x12\x10\n\x08owner_id\x18\x05 \x01(\t\x12\x36\n\x06labels\x18\x06 \x03(\x0b\x32&.agentmesh.v1.EnrolledNode.LabelsEntry\x12/\n\x0b\x65nroll_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06\x62\x61nned\x18\t \x01(\x08\x12\x1b\n\x13\x61utonomous_recovery\x18\n \x01(\x08\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc4\x01\n\x0bRouterLease\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x11\n\taddresses\x18\x02 \x03(\t\x12\x35\n\x11last_renewal_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x17\n\x0f\x63onnected_peers\x18\x05 \x03(\t\x12\x10\n\x08\x64ht_size\x18\x06 \x01(\x05\"l\n\x0cNodeServices\x12+\n\x08services\x18\x01 \x03(\x0b\x32\x19.agentmesh.v1.ServiceInfo\x12/\n\x0breport_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xdb\x03\n\x13\x41\x64minStatusResponse\x12!\n\x05users\x18\x01 \x03(\x0b\x32\x12.agentmesh.v1.User\x12\x31\n\x0e\x61\x63tive_routers\x18\x02 \x03(\x0b\x32\x19.agentmesh.v1.RouterLease\x12\x32\n\x0e\x65nrolled_nodes\x18\x03 \x03(\x0b\x32\x1a.agentmesh.v1.EnrolledNode\x12<\n\x13\x65nrollment_requests\x18\x04 \x03(\x0b\x32\x1f.agentmesh.v1.EnrollmentRequest\x12\x36\n\x10\x62ootstrap_tokens\x18\x05 \x03(\x0b\x32\x1c.agentmesh.v1.BootstrapToken\x12*\n\x06policy\x18\x06 \x01(\x0b\x32\x1a.agentmesh.v1.PolicyConfig\x12H\n\x0cnode_catalog\x18\x07 \x03(\x0b\x32\x32.agentmesh.v1.AdminStatusResponse.NodeCatalogEntry\x1aN\n\x10NodeCatalogEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.agentmesh.v1.NodeServices:\x02\x38\x01\"\x81\x02\n\x12UserStatusResponse\x12 \n\x04user\x18\x01 \x01(\x0b\x32\x12.agentmesh.v1.User\x12\x32\n\x0e\x65nrolled_nodes\x18\x02 \x03(\x0b\x32\x1a.agentmesh.v1.EnrolledNode\x12\x36\n\x10\x62ootstrap_tokens\x18\x03 \x03(\x0b\x32\x1c.agentmesh.v1.BootstrapToken\x12\x31\n\x0e\x61\x63tive_routers\x18\x04 \x03(\x0b\x32\x19.agentmesh.v1.RouterLease\x12*\n\x06policy\x18\x05 \x01(\x0b\x32\x1a.agentmesh.v1.PolicyConfig\"\xe4\x01\n\x18IdentityEvidenceResponse\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12\x37\n\x13\x62iscuit_expire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x19\n\x11\x63ontrol_plane_url\x18\x04 \x01(\t\x12\"\n\x1atrusted_control_plane_keys\x18\x05 \x03(\x0c\x12.\n\ncheck_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xc6\x02\n\x14PeerEvidenceResponse\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12\x15\n\rverifying_key\x18\x03 \x01(\x0c\x12\r\n\x05roles\x18\x04 \x03(\t\x12>\n\x06labels\x18\x05 \x03(\x0b\x32..agentmesh.v1.PeerEvidenceResponse.LabelsEntry\x12/\n\x0b\x65xpire_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0erevocation_ids\x18\x07 \x03(\t\x12.\n\ncheck_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8c\x02\n\x10MemberCredential\x12\x19\n\x11\x63ontrol_plane_url\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x35\n\x0ctrusted_keys\x18\x04 \x03(\x0b\x32\x1f.agentmesh.v1.TrustedSigningKey\x12\x19\n\x11issued_under_keys\x18\x05 \x03(\x0c\x12\x18\n\x10router_addresses\x18\x06 \x03(\t\x12/\n\x0coidc_session\x18\x07 \x01(\x0b\x32\x19.agentmesh.v1.OIDCSession\"Y\n\x11TrustedSigningKey\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\x12\x30\n\x0creceive_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"Y\n\x0bOIDCSession\x12\x0e\n\x06issuer\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x15\n\rrefresh_token\x18\x04 \x01(\t\"\x93\x01\n\x15TaskAuthorizationRule\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12%\n\x05rules\x18\x03 \x03(\x0b\x32\x16.agentmesh.v1.TaskRule\x12/\n\x0b\x65xpire_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x84\x01\n\x08TaskRule\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x18\n\x10\x61llowed_services\x18\x02 \x03(\t\x12.\n\toperation\x18\x03 \x01(\x0b\x32\x1b.agentmesh.v1.TaskOperation\x12\x19\n\x11\x61llowed_resources\x18\x04 \x03(\t\"s\n\rTaskOperation\x12\x15\n\rallowed_tools\x18\x01 \x03(\t\x12\x17\n\x0f\x61llowed_methods\x18\x02 \x03(\t\x12\x15\n\rallowed_paths\x18\x03 \x03(\t\x12\x1b\n\x13\x61llowed_permissions\x18\x04 \x03(\t\"\xab\x01\n\x14TokenExchangeRequest\x12\x15\n\rsubject_token\x18\x01 \x01(\t\x12\x36\n\ttask_rule\x18\x02 \x01(\x0b\x32#.agentmesh.v1.TaskAuthorizationRule\x12\x0c\n\x04seal\x18\x03 \x01(\x08\x12\x19\n\x11\x63hallenge_unix_ms\x18\x04 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x05 \x01(\x0c\"\x7f\n\x15TokenExchangeResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\r\n\x05roles\x18\x03 \x03(\t\x12\x0f\n\x07subject\x18\x04 \x01(\t\"\x81\x01\n\x0fSTSTokenRequest\x12\x0f\n\x07\x62iscuit\x18\x01 \x01(\x0c\x12\x13\n\x0b\x64\x65stination\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x19\n\x11\x63hallenge_unix_ms\x18\x04 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x05 \x01(\x0c\"\x83\x01\n\x10STSTokenResponse\x12\x0b\n\x03jwt\x18\x01 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0f\n\x07subject\x18\x03 \x01(\t\x12\r\n\x05roles\x18\x04 \x03(\t\x12\x11\n\ttask_name\x18\x05 \x01(\t\"F\n\x13RevocationsResponse\x12\x16\n\x0erevocation_ids\x18\x01 \x03(\t\x12\x17\n\x0f\x62\x61nned_peer_ids\x18\x02 \x03(\t*\x94\x01\n\x10\x45nrollmentStatus\x12!\n\x1d\x45NROLLMENT_STATUS_UNSPECIFIED\x10\x00\x12\x1d\n\x19\x45NROLLMENT_STATUS_PENDING\x10\x01\x12\x1e\n\x1a\x45NROLLMENT_STATUS_APPROVED\x10\x02\x12\x1e\n\x1a\x45NROLLMENT_STATUS_REJECTED\x10\x03*\x8c\x01\n\x0bServiceType\x12\x1c\n\x18SERVICE_TYPE_UNSPECIFIED\x10\x00\x12\x14\n\x10SERVICE_TYPE_MCP\x10\x01\x12\x1a\n\x16SERVICE_TYPE_INFERENCE\x10\x02\x12\x14\n\x10SERVICE_TYPE_A2A\x10\x03\x12\x17\n\x13SERVICE_TYPE_EGRESS\x10\x04*7\n\nEgressMode\x12\x14\n\x10\x45GRESS_MODE_HTTP\x10\x00\x12\x13\n\x0f\x45GRESS_MODE_TCP\x10\x01*\\\n\x12ResponseInspection\x12 \n\x1cRESPONSE_INSPECTION_BUFFERED\x10\x00\x12$\n RESPONSE_INSPECTION_REQUEST_ONLY\x10\x01\x42!Z\x1fgithub.com/google/agentmesh/apib\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'agentmesh_pb2', globals()) @@ -39,14 +39,14 @@ _ADMINSTATUSRESPONSE_NODECATALOGENTRY._serialized_options = b'8\001' _PEEREVIDENCERESPONSE_LABELSENTRY._options = None _PEEREVIDENCERESPONSE_LABELSENTRY._serialized_options = b'8\001' - _ENROLLMENTSTATUS._serialized_start=11436 - _ENROLLMENTSTATUS._serialized_end=11584 - _SERVICETYPE._serialized_start=11587 - _SERVICETYPE._serialized_end=11727 - _EGRESSMODE._serialized_start=11729 - _EGRESSMODE._serialized_end=11784 - _RESPONSEINSPECTION._serialized_start=11786 - _RESPONSEINSPECTION._serialized_end=11878 + _ENROLLMENTSTATUS._serialized_start=11453 + _ENROLLMENTSTATUS._serialized_end=11601 + _SERVICETYPE._serialized_start=11604 + _SERVICETYPE._serialized_end=11744 + _EGRESSMODE._serialized_start=11746 + _EGRESSMODE._serialized_end=11801 + _RESPONSEINSPECTION._serialized_start=11803 + _RESPONSEINSPECTION._serialized_end=11895 _AUTHFRAME._serialized_start=98 _AUTHFRAME._serialized_end=150 _AUTHRESPONSE._serialized_start=152 @@ -88,135 +88,135 @@ _DIRECTORYREQUEST._serialized_start=2456 _DIRECTORYREQUEST._serialized_end=2707 _DIRECTORYPROVIDE._serialized_start=2709 - _DIRECTORYPROVIDE._serialized_end=2773 - _DIRECTORYWITHDRAW._serialized_start=2775 - _DIRECTORYWITHDRAW._serialized_end=2825 - _DIRECTORYFINDPROVIDERS._serialized_start=2827 - _DIRECTORYFINDPROVIDERS._serialized_end=2879 - _DIRECTORYFINDPEER._serialized_start=2881 - _DIRECTORYFINDPEER._serialized_end=2917 - _DIRECTORYRESPONSE._serialized_start=2919 - _DIRECTORYRESPONSE._serialized_end=3038 - _PEERRECORD._serialized_start=3040 - _PEERRECORD._serialized_end=3084 - _ROUTERINFO._serialized_start=3087 - _ROUTERINFO._serialized_end=3283 + _DIRECTORYPROVIDE._serialized_end=2790 + _DIRECTORYWITHDRAW._serialized_start=2792 + _DIRECTORYWITHDRAW._serialized_end=2842 + _DIRECTORYFINDPROVIDERS._serialized_start=2844 + _DIRECTORYFINDPROVIDERS._serialized_end=2896 + _DIRECTORYFINDPEER._serialized_start=2898 + _DIRECTORYFINDPEER._serialized_end=2934 + _DIRECTORYRESPONSE._serialized_start=2936 + _DIRECTORYRESPONSE._serialized_end=3055 + _PEERRECORD._serialized_start=3057 + _PEERRECORD._serialized_end=3101 + _ROUTERINFO._serialized_start=3104 + _ROUTERINFO._serialized_end=3300 _ROUTERINFO_LABELSENTRY._serialized_start=825 _ROUTERINFO_LABELSENTRY._serialized_end=870 - _ROUTERLEASEREQUEST._serialized_start=3286 - _ROUTERLEASEREQUEST._serialized_end=3545 - _ROUTERLEASERESPONSE._serialized_start=3547 - _ROUTERLEASERESPONSE._serialized_end=3649 - _POLICYROLE._serialized_start=3652 - _POLICYROLE._serialized_end=3816 - _HTTPGRANT._serialized_start=3818 - _HTTPGRANT._serialized_end=3878 - _EGRESSDESTINATION._serialized_start=3881 - _EGRESSDESTINATION._serialized_end=4170 - _INSPECTION._serialized_start=4172 - _INSPECTION._serialized_end=4229 - _INSPECTOR._serialized_start=4231 - _INSPECTOR._serialized_end=4342 - _MODELARMOR._serialized_start=4345 - _MODELARMOR._serialized_end=4490 - _EXTPROC._serialized_start=4493 - _EXTPROC._serialized_end=4757 - _EXTPROCPROCESSINGMODE._serialized_start=4760 - _EXTPROCPROCESSINGMODE._serialized_end=5399 - _EXTPROCPROCESSINGMODE_HEADERMODE._serialized_start=5244 - _EXTPROCPROCESSINGMODE_HEADERMODE._serialized_end=5301 - _EXTPROCPROCESSINGMODE_BODYMODE._serialized_start=5303 - _EXTPROCPROCESSINGMODE_BODYMODE._serialized_end=5399 - _CREDENTIALBROKER._serialized_start=5402 - _CREDENTIALBROKER._serialized_end=5627 - _OIDCFEDERATION._serialized_start=5629 - _OIDCFEDERATION._serialized_end=5724 - _AWSASSUMEROLE._serialized_start=5726 - _AWSASSUMEROLE._serialized_end=5783 - _PLATFORMIDENTITY._serialized_start=5785 - _PLATFORMIDENTITY._serialized_end=5819 - _POLICYBINDING._serialized_start=5821 - _POLICYBINDING._serialized_end=5867 - _POLICYCONFIG._serialized_start=5870 - _POLICYCONFIG._serialized_end=6021 - _POLICYCONFIGGETREQUEST._serialized_start=6023 - _POLICYCONFIGGETREQUEST._serialized_end=6047 - _POLICYCONFIGGETRESPONSE._serialized_start=6049 - _POLICYCONFIGGETRESPONSE._serialized_end=6097 - _POLICYCONFIGUPDATERESPONSE._serialized_start=6099 - _POLICYCONFIGUPDATERESPONSE._serialized_end=6159 - _EGRESSASSIGNMENTSREQUEST._serialized_start=6161 - _EGRESSASSIGNMENTSREQUEST._serialized_end=6187 - _EGRESSASSIGNMENTSRESPONSE._serialized_start=6189 - _EGRESSASSIGNMENTSRESPONSE._serialized_end=6265 - _KEYSRESPONSE._serialized_start=6267 - _KEYSRESPONSE._serialized_end=6369 - _TOKENREFRESHREQUEST._serialized_start=6371 - _TOKENREFRESHREQUEST._serialized_end=6478 - _TOKENREFRESHRESPONSE._serialized_start=6480 - _TOKENREFRESHRESPONSE._serialized_end=6597 - _NODECATALOGREPORT._serialized_start=6599 - _NODECATALOGREPORT._serialized_end=6663 - _TOKENREVOKEREQUEST._serialized_start=6665 - _TOKENREVOKEREQUEST._serialized_end=6702 - _TOKENREVOKERESPONSE._serialized_start=6704 - _TOKENREVOKERESPONSE._serialized_end=6757 - _BOOTSTRAPTOKENCREATEREQUEST._serialized_start=6760 - _BOOTSTRAPTOKENCREATEREQUEST._serialized_end=6910 - _BOOTSTRAPTOKENCREATERESPONSE._serialized_start=6913 - _BOOTSTRAPTOKENCREATERESPONSE._serialized_end=7051 - _BOOTSTRAPTOKEN._serialized_start=7054 - _BOOTSTRAPTOKEN._serialized_end=7353 - _BOOTSTRAPTOKENLISTRESPONSE._serialized_start=7355 - _BOOTSTRAPTOKENLISTRESPONSE._serialized_end=7429 - _ENROLLMENTREQUEST._serialized_start=7432 - _ENROLLMENTREQUEST._serialized_end=7774 + _ROUTERLEASEREQUEST._serialized_start=3303 + _ROUTERLEASEREQUEST._serialized_end=3562 + _ROUTERLEASERESPONSE._serialized_start=3564 + _ROUTERLEASERESPONSE._serialized_end=3666 + _POLICYROLE._serialized_start=3669 + _POLICYROLE._serialized_end=3833 + _HTTPGRANT._serialized_start=3835 + _HTTPGRANT._serialized_end=3895 + _EGRESSDESTINATION._serialized_start=3898 + _EGRESSDESTINATION._serialized_end=4187 + _INSPECTION._serialized_start=4189 + _INSPECTION._serialized_end=4246 + _INSPECTOR._serialized_start=4248 + _INSPECTOR._serialized_end=4359 + _MODELARMOR._serialized_start=4362 + _MODELARMOR._serialized_end=4507 + _EXTPROC._serialized_start=4510 + _EXTPROC._serialized_end=4774 + _EXTPROCPROCESSINGMODE._serialized_start=4777 + _EXTPROCPROCESSINGMODE._serialized_end=5416 + _EXTPROCPROCESSINGMODE_HEADERMODE._serialized_start=5261 + _EXTPROCPROCESSINGMODE_HEADERMODE._serialized_end=5318 + _EXTPROCPROCESSINGMODE_BODYMODE._serialized_start=5320 + _EXTPROCPROCESSINGMODE_BODYMODE._serialized_end=5416 + _CREDENTIALBROKER._serialized_start=5419 + _CREDENTIALBROKER._serialized_end=5644 + _OIDCFEDERATION._serialized_start=5646 + _OIDCFEDERATION._serialized_end=5741 + _AWSASSUMEROLE._serialized_start=5743 + _AWSASSUMEROLE._serialized_end=5800 + _PLATFORMIDENTITY._serialized_start=5802 + _PLATFORMIDENTITY._serialized_end=5836 + _POLICYBINDING._serialized_start=5838 + _POLICYBINDING._serialized_end=5884 + _POLICYCONFIG._serialized_start=5887 + _POLICYCONFIG._serialized_end=6038 + _POLICYCONFIGGETREQUEST._serialized_start=6040 + _POLICYCONFIGGETREQUEST._serialized_end=6064 + _POLICYCONFIGGETRESPONSE._serialized_start=6066 + _POLICYCONFIGGETRESPONSE._serialized_end=6114 + _POLICYCONFIGUPDATERESPONSE._serialized_start=6116 + _POLICYCONFIGUPDATERESPONSE._serialized_end=6176 + _EGRESSASSIGNMENTSREQUEST._serialized_start=6178 + _EGRESSASSIGNMENTSREQUEST._serialized_end=6204 + _EGRESSASSIGNMENTSRESPONSE._serialized_start=6206 + _EGRESSASSIGNMENTSRESPONSE._serialized_end=6282 + _KEYSRESPONSE._serialized_start=6284 + _KEYSRESPONSE._serialized_end=6386 + _TOKENREFRESHREQUEST._serialized_start=6388 + _TOKENREFRESHREQUEST._serialized_end=6495 + _TOKENREFRESHRESPONSE._serialized_start=6497 + _TOKENREFRESHRESPONSE._serialized_end=6614 + _NODECATALOGREPORT._serialized_start=6616 + _NODECATALOGREPORT._serialized_end=6680 + _TOKENREVOKEREQUEST._serialized_start=6682 + _TOKENREVOKEREQUEST._serialized_end=6719 + _TOKENREVOKERESPONSE._serialized_start=6721 + _TOKENREVOKERESPONSE._serialized_end=6774 + _BOOTSTRAPTOKENCREATEREQUEST._serialized_start=6777 + _BOOTSTRAPTOKENCREATEREQUEST._serialized_end=6927 + _BOOTSTRAPTOKENCREATERESPONSE._serialized_start=6930 + _BOOTSTRAPTOKENCREATERESPONSE._serialized_end=7068 + _BOOTSTRAPTOKEN._serialized_start=7071 + _BOOTSTRAPTOKEN._serialized_end=7370 + _BOOTSTRAPTOKENLISTRESPONSE._serialized_start=7372 + _BOOTSTRAPTOKENLISTRESPONSE._serialized_end=7446 + _ENROLLMENTREQUEST._serialized_start=7449 + _ENROLLMENTREQUEST._serialized_end=7791 _ENROLLMENTREQUEST_LABELSENTRY._serialized_start=825 _ENROLLMENTREQUEST_LABELSENTRY._serialized_end=870 - _ENROLLMENTREQUESTLISTRESPONSE._serialized_start=7776 - _ENROLLMENTREQUESTLISTRESPONSE._serialized_end=7858 - _USER._serialized_start=7860 - _USER._serialized_end=7972 - _ENROLLEDNODE._serialized_start=7975 - _ENROLLEDNODE._serialized_end=8330 + _ENROLLMENTREQUESTLISTRESPONSE._serialized_start=7793 + _ENROLLMENTREQUESTLISTRESPONSE._serialized_end=7875 + _USER._serialized_start=7877 + _USER._serialized_end=7989 + _ENROLLEDNODE._serialized_start=7992 + _ENROLLEDNODE._serialized_end=8347 _ENROLLEDNODE_LABELSENTRY._serialized_start=825 _ENROLLEDNODE_LABELSENTRY._serialized_end=870 - _ROUTERLEASE._serialized_start=8333 - _ROUTERLEASE._serialized_end=8529 - _NODESERVICES._serialized_start=8531 - _NODESERVICES._serialized_end=8639 - _ADMINSTATUSRESPONSE._serialized_start=8642 - _ADMINSTATUSRESPONSE._serialized_end=9117 - _ADMINSTATUSRESPONSE_NODECATALOGENTRY._serialized_start=9039 - _ADMINSTATUSRESPONSE_NODECATALOGENTRY._serialized_end=9117 - _USERSTATUSRESPONSE._serialized_start=9120 - _USERSTATUSRESPONSE._serialized_end=9377 - _IDENTITYEVIDENCERESPONSE._serialized_start=9380 - _IDENTITYEVIDENCERESPONSE._serialized_end=9608 - _PEEREVIDENCERESPONSE._serialized_start=9611 - _PEEREVIDENCERESPONSE._serialized_end=9937 + _ROUTERLEASE._serialized_start=8350 + _ROUTERLEASE._serialized_end=8546 + _NODESERVICES._serialized_start=8548 + _NODESERVICES._serialized_end=8656 + _ADMINSTATUSRESPONSE._serialized_start=8659 + _ADMINSTATUSRESPONSE._serialized_end=9134 + _ADMINSTATUSRESPONSE_NODECATALOGENTRY._serialized_start=9056 + _ADMINSTATUSRESPONSE_NODECATALOGENTRY._serialized_end=9134 + _USERSTATUSRESPONSE._serialized_start=9137 + _USERSTATUSRESPONSE._serialized_end=9394 + _IDENTITYEVIDENCERESPONSE._serialized_start=9397 + _IDENTITYEVIDENCERESPONSE._serialized_end=9625 + _PEEREVIDENCERESPONSE._serialized_start=9628 + _PEEREVIDENCERESPONSE._serialized_end=9954 _PEEREVIDENCERESPONSE_LABELSENTRY._serialized_start=825 _PEEREVIDENCERESPONSE_LABELSENTRY._serialized_end=870 - _MEMBERCREDENTIAL._serialized_start=9940 - _MEMBERCREDENTIAL._serialized_end=10208 - _TRUSTEDSIGNINGKEY._serialized_start=10210 - _TRUSTEDSIGNINGKEY._serialized_end=10299 - _OIDCSESSION._serialized_start=10301 - _OIDCSESSION._serialized_end=10390 - _TASKAUTHORIZATIONRULE._serialized_start=10393 - _TASKAUTHORIZATIONRULE._serialized_end=10540 - _TASKRULE._serialized_start=10543 - _TASKRULE._serialized_end=10675 - _TASKOPERATION._serialized_start=10677 - _TASKOPERATION._serialized_end=10792 - _TOKENEXCHANGEREQUEST._serialized_start=10795 - _TOKENEXCHANGEREQUEST._serialized_end=10966 - _TOKENEXCHANGERESPONSE._serialized_start=10968 - _TOKENEXCHANGERESPONSE._serialized_end=11095 - _STSTOKENREQUEST._serialized_start=11098 - _STSTOKENREQUEST._serialized_end=11227 - _STSTOKENRESPONSE._serialized_start=11230 - _STSTOKENRESPONSE._serialized_end=11361 - _REVOCATIONSRESPONSE._serialized_start=11363 - _REVOCATIONSRESPONSE._serialized_end=11433 + _MEMBERCREDENTIAL._serialized_start=9957 + _MEMBERCREDENTIAL._serialized_end=10225 + _TRUSTEDSIGNINGKEY._serialized_start=10227 + _TRUSTEDSIGNINGKEY._serialized_end=10316 + _OIDCSESSION._serialized_start=10318 + _OIDCSESSION._serialized_end=10407 + _TASKAUTHORIZATIONRULE._serialized_start=10410 + _TASKAUTHORIZATIONRULE._serialized_end=10557 + _TASKRULE._serialized_start=10560 + _TASKRULE._serialized_end=10692 + _TASKOPERATION._serialized_start=10694 + _TASKOPERATION._serialized_end=10809 + _TOKENEXCHANGEREQUEST._serialized_start=10812 + _TOKENEXCHANGEREQUEST._serialized_end=10983 + _TOKENEXCHANGERESPONSE._serialized_start=10985 + _TOKENEXCHANGERESPONSE._serialized_end=11112 + _STSTOKENREQUEST._serialized_start=11115 + _STSTOKENREQUEST._serialized_end=11244 + _STSTOKENRESPONSE._serialized_start=11247 + _STSTOKENRESPONSE._serialized_end=11378 + _REVOCATIONSRESPONSE._serialized_start=11380 + _REVOCATIONSRESPONSE._serialized_end=11450 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.pyi b/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.pyi index 309f027f..26bc72f6 100644 --- a/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.pyi +++ b/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.pyi @@ -245,14 +245,16 @@ class DirectoryParameters(_message.Message): def __init__(self, replication: _Optional[int] = ..., reprovide_interval: _Optional[_Union[_duration_pb2.Duration, _Mapping]] = ..., record_ttl: _Optional[_Union[_duration_pb2.Duration, _Mapping]] = ...) -> None: ... class DirectoryProvide(_message.Message): - __slots__ = ["addrs", "key", "provider"] + __slots__ = ["addrs", "key", "provider", "service"] ADDRS_FIELD_NUMBER: _ClassVar[int] KEY_FIELD_NUMBER: _ClassVar[int] PROVIDER_FIELD_NUMBER: _ClassVar[int] + SERVICE_FIELD_NUMBER: _ClassVar[int] addrs: _containers.RepeatedScalarFieldContainer[str] key: bytes provider: str - def __init__(self, key: _Optional[bytes] = ..., addrs: _Optional[_Iterable[str]] = ..., provider: _Optional[str] = ...) -> None: ... + service: str + def __init__(self, key: _Optional[bytes] = ..., addrs: _Optional[_Iterable[str]] = ..., provider: _Optional[str] = ..., service: _Optional[str] = ...) -> None: ... class DirectoryRequest(_message.Message): __slots__ = ["find_peer", "find_providers", "provide", "withdraw"] From 0eecc86abc4750ebc50301f1c81ac338cad0ed69 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Wed, 14 Oct 2026 13:46:50 +0000 Subject: [PATCH 5/7] policy: allowed_publish grants what a member may publish, deny by default A role said what a member may call (allowed_services) and where it may send (allowed_targets), but nothing said what a member may serve. Any enrolled node could publish mcp://github or inference://gpt-4 and callers that asked the directory would be routed to it: a member of the mesh could stand in for any service, which the record check in the previous commit does not prevent, it only ties a key to the name the record itself claims. PolicyRole gains allowed_publish, the same patterns as allowed_services ("*", "mcp://*", "mcp://github", "mcp://team-*"), empty by default: a member publishes nothing until a role it holds grants the service. The control plane validates and stores the list, and mints it into the authority block as granted_publish_* facts (api.BuildPublishDatalogFacts) beside the granted_* facts of allowed_services, with the same budget. A member of a served egress destination gets egress:// granted at mint from the destination's served_by (api.WithEgressPublish), so the egress policy is the single place an operator names who serves it. Two verifiers enforce the grant, from the same Datalog allow policies (api.PublishPolicies, exported to the SDK artifact as publish_policies): * The router that holds a member keeps its credential from the auth handshake and refuses a DirectoryProvide whose service the credential does not grant (requirePublish; the peer record is always allowed); the refusal is a "Record denied" warning and the "denied" result of agentmesh_router_directory_lookups_total. * The caller checks the provider it is about to use: node (VerifyProvider at the egress proxy, A2A, egress tunnel, OpenAI facade and MCP connect, 403 "provider is not granted "), JS and Python (requirePublish / require_publish on the provider's verified biscuit before the MCP session or egress stream carries a request). The catalog service system://mesh.catalog needs no grant. The default policies grant "*" to the node role where every node is the operator's (standalone, the testnets, the integration fixtures); the chart takes bootstrap.nodePublish, empty by default, and the container e2e mesh grants exactly the services its nodes run. --- .github/workflows/deploy.yaml | 4 +- api/agentmesh.pb.go | 26 +- api/agentmesh.proto | 8 + api/datalog.go | 72 ++++++ api/egress.go | 38 +++ api/egress_test.go | 33 +++ charts/agentmesh/templates/bootstrap-job.yaml | 4 +- .../agentmesh/tests/bootstrap-job_test.yaml | 5 +- charts/agentmesh/values.yaml | 6 + .../multi-tier-guardrails/policy.json | 1 + .../examples/sandboxed-agent/policy.json | 1 + development/kind/run.sh | 8 +- hack/gen-sdk-datalog/main.go | 4 + internal/controlplane/server.go | 31 ++- internal/identity/biscuit.go | 30 +++ internal/identity/biscuit_test.go | 58 +++++ internal/node/a2a_service.go | 2 +- internal/node/egress_tunnel.go | 2 +- internal/node/labels_gate.go | 47 +++- internal/node/labels_gate_test.go | 38 +++ internal/node/mcp.go | 12 +- internal/node/mcp_handlers_test.go | 15 +- internal/node/openai_facade.go | 8 +- internal/node/openai_facade_test.go | 12 +- internal/node/proxy_test.go | 2 +- internal/node/sidecar.go | 11 +- internal/router/directory.go | 57 ++++- internal/router/directory_test.go | 99 +++++++- internal/router/router.go | 14 +- internal/standalone/standalone.go | 2 +- internal/storage/mesh_policy_test.go | 1 + internal/storage/sql_store.go | 7 + mobile/mobile_e2e.sh | 2 +- sdk/js/src/biscuit.ts | 32 +++ sdk/js/src/credential.ts | 6 + sdk/js/src/gen/agentmesh_pb.ts | 15 +- sdk/js/src/gen/datalog.ts | 10 + sdk/js/src/mcp.test.ts | 47 +++- sdk/js/src/mcp.ts | 6 +- sdk/js/src/session.test.ts | 4 + sdk/js/src/session.ts | 21 +- sdk/python/src/agent_mesh/_gen/datalog.json | 10 + .../src/agent_mesh/_proto/agentmesh_pb2.py | 228 +++++++++--------- .../src/agent_mesh/_proto/agentmesh_pb2.pyi | 6 +- sdk/python/src/agent_mesh/biscuit.py | 27 +++ sdk/python/src/agent_mesh/httpx_transport.py | 13 +- sdk/python/src/agent_mesh/mcp_client.py | 4 +- sdk/python/src/agent_mesh/session.py | 15 +- sdk/python/tests/test_libp2p_http.py | 6 +- sdk/python/tests/test_mcp.py | 37 ++- sdk/python/tests/test_session.py | 3 + site/content/docs/concepts/authorization.md | 11 +- site/content/docs/concepts/networking.md | 12 +- .../contributing/security-architecture.md | 11 + site/content/docs/guides/exposing-services.md | 21 +- site/content/docs/guides/kubernetes.md | 8 +- site/content/docs/guides/native-sdks.md | 9 +- site/content/docs/reference/node-api.md | 7 +- site/content/docs/reference/policy.md | 26 ++ site/content/docs/reference/router.md | 7 +- tests/e2e/lib/container_mesh.bash | 1 + tests/integration/biscuit_expiry_test.go | 2 +- tests/integration/minimal_helpers_test.go | 11 +- tests/integration/readmission_test.go | 2 +- tests/integration/router_attachment_test.go | 1 + tests/integration/sdk_mesh_test.go | 1 + tests/integration/sts_cuj_test.go | 1 + tests/ui/dev.sh | 2 +- 68 files changed, 1039 insertions(+), 244 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 9dbc00ae..f22b8b36 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -469,10 +469,10 @@ jobs: -H "Authorization: Bearer ${ADMIN_TOKEN}" \ -d '{ "roles": [ - {"name": "agentmesh-canary", "allowed_services": ["*"], "allowed_targets": ["*"]}, + {"name": "agentmesh-canary", "allowed_services": ["*"], "allowed_publish": ["*"], "allowed_targets": ["*"]}, {"name": "mesh:role:router", "allowed_services": ["*"], "allowed_targets": ["*"]}, {"name": "mesh:role:node"}, - {"name": "public-mesh", "allowed_services": ["*"], "allowed_targets": ["*"]} + {"name": "public-mesh", "allowed_services": ["*"], "allowed_publish": ["*"], "allowed_targets": ["*"]} ], "bindings": [ {"role": "agentmesh-canary", "members": ["user:system:serviceaccount:agentmesh-canary-'"${ENV_NAME}"':agentmesh-node-sa"]}, diff --git a/api/agentmesh.pb.go b/api/agentmesh.pb.go index 04c52851..f75184d7 100644 --- a/api/agentmesh.pb.go +++ b/api/agentmesh.pb.go @@ -2275,9 +2275,17 @@ type PolicyRole struct { // declaration into something the control plane is willing to sign. AllowedLabels []string `protobuf:"bytes,5,rep,name=allowed_labels,json=allowedLabels,proto3" json:"allowed_labels,omitempty"` // HTTP narrowing of allowed_services entries; see HTTPGrant. - Http []*HTTPGrant `protobuf:"bytes,6,rep,name=http,proto3" json:"http,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Http []*HTTPGrant `protobuf:"bytes,6,rep,name=http,proto3" json:"http,omitempty"` + // Services a member with this role may serve, in the syntax of + // allowed_services ("mcp://calc", "inference://*", "*"). A role serves + // nothing it is not granted here: a router keeps a member's record only + // for a service its credential grants, and a caller sends a request to + // a provider it found by name only when the provider's credential does. + // An egress destination needs no entry: served_by grants it to the + // nodes it selects. + AllowedPublish []string `protobuf:"bytes,7,rep,name=allowed_publish,json=allowedPublish,proto3" json:"allowed_publish,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PolicyRole) Reset() { @@ -2352,6 +2360,13 @@ func (x *PolicyRole) GetHttp() []*HTTPGrant { return nil } +func (x *PolicyRole) GetAllowedPublish() []string { + if x != nil { + return x.AllowedPublish + } + return nil +} + // HTTPGrant narrows one allowed_services entry to HTTP methods and paths. // The control plane compiles it into granted_method and granted_path_* facts // in the holder's credential and withholds the plain service grant for that @@ -6127,7 +6142,7 @@ const file_api_agentmesh_proto_rawDesc = "" + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x14\n" + "\x05error\x18\x02 \x01(\tR\x05error\x12;\n" + "\vexpire_time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\n" + - "expireTime\"\xef\x01\n" + + "expireTime\"\x98\x02\n" + "\n" + "PolicyRole\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12'\n" + @@ -6135,7 +6150,8 @@ const file_api_agentmesh_proto_rawDesc = "" + "\x10allowed_services\x18\x03 \x03(\tR\x0fallowedServices\x12%\n" + "\x0ecustom_datalog\x18\x04 \x03(\tR\rcustomDatalog\x12%\n" + "\x0eallowed_labels\x18\x05 \x03(\tR\rallowedLabels\x12+\n" + - "\x04http\x18\x06 \x03(\v2\x17.agentmesh.v1.HTTPGrantR\x04http\"U\n" + + "\x04http\x18\x06 \x03(\v2\x17.agentmesh.v1.HTTPGrantR\x04http\x12'\n" + + "\x0fallowed_publish\x18\a \x03(\tR\x0eallowedPublish\"U\n" + "\tHTTPGrant\x12\x18\n" + "\aservice\x18\x01 \x01(\tR\aservice\x12\x18\n" + "\amethods\x18\x02 \x03(\tR\amethods\x12\x14\n" + diff --git a/api/agentmesh.proto b/api/agentmesh.proto index 192a1fbb..7396648f 100644 --- a/api/agentmesh.proto +++ b/api/agentmesh.proto @@ -351,6 +351,14 @@ message PolicyRole { repeated string allowed_labels = 5; // HTTP narrowing of allowed_services entries; see HTTPGrant. repeated HTTPGrant http = 6; + // Services a member with this role may serve, in the syntax of + // allowed_services ("mcp://calc", "inference://*", "*"). A role serves + // nothing it is not granted here: a router keeps a member's record only + // for a service its credential grants, and a caller sends a request to + // a provider it found by name only when the provider's credential does. + // An egress destination needs no entry: served_by grants it to the + // nodes it selects. + repeated string allowed_publish = 7; } // HTTPGrant narrows one allowed_services entry to HTTP methods and paths. diff --git a/api/datalog.go b/api/datalog.go index 3af85e44..a9dc15d4 100644 --- a/api/datalog.go +++ b/api/datalog.go @@ -165,6 +165,23 @@ const ( // Example Datalog: allow if service("mcp", "calculator"), granted_service_set("mcp", $set), $set.contains("calculator") FactGrantedServiceSet = "granted_service_set" + // The granted_publish_* facts are the granted_service_* facts for the + // other direction: what a member may serve, as a role's allowed_publish + // grants it. A router verifies them before it keeps a member's record and + // a caller before it sends a request to a provider it found by name, with + // PublishPolicies over a publish($type, $name) fact. They carry the same + // terms as their granted_service_* counterpart. + FactGrantedPublishAllTypes = "granted_publish_all_types" + FactGrantedPublishAll = "granted_publish_all" + FactGrantedPublishSuffix = "granted_publish_suffix" + FactGrantedPublishPrefix = "granted_publish_prefix" + FactGrantedPublishExact = "granted_publish_exact" + FactGrantedPublishSet = "granted_publish_set" + + // FactPublish is the service a verifier asks about: publish("mcp", "calc"). + // Added by the verifier, never carried in a token. + FactPublish = "publish" + // FactGrantedTargetAllTypes allows target access to all network targets (unrestricted). // Contains: biscuit.Bool(true) (marker fact) FactGrantedTargetAllTypes = "granted_target_all_types" @@ -419,6 +436,13 @@ var ( // AllowIfTruePolicy is the static policy "allow if true" used during token verification. AllowIfTruePolicy biscuit.Policy + // PublishPolicies decide whether a member may serve a service: over its + // own credential and a publish($type, $name) fact, one allows when a + // granted_publish_* fact covers the service, and no policy matching is a + // refusal. The router runs them before it keeps a member's record, a + // caller before it sends a request to a provider it found by name. + PublishPolicies []biscuit.Policy + // BaselineSources is the Datalog text every variable above is parsed from. // The SDKs embed this text (see hack/gen-sdk-datalog) so that a provider // written in another language evaluates the same authorizer as agentmesh-node. @@ -430,6 +454,8 @@ var ( type DatalogSources struct { // Policies are the service allow policies (BaselinePolicies). Policies []string `json:"policies"` + // PublishPolicies decide whether a member may serve a service (PublishPolicies). + PublishPolicies []string `json:"publish_policies"` // Rules derive allow_network_target from target grants (BaselineRules). Rules []string `json:"rules"` // HTTPRules derive service grants from narrowed grants (BaselineHTTPRules). @@ -482,6 +508,24 @@ func init() { BaselinePolicies = append(BaselinePolicies, p) } + // Publish policies: the service policies with publish() for service() + // and granted_publish_* for granted_service_*. + BaselineSources.PublishPolicies = []string{ + fmt.Sprintf(`allow if %s($type, $name), %s($type, $name)`, FactPublish, FactGrantedPublishExact), + fmt.Sprintf(`allow if %s($type, $name), %s($type, $set), $set.contains($name)`, FactPublish, FactGrantedPublishSet), + fmt.Sprintf(`allow if %s($type, $name), %s($type, $prefix), $name.starts_with($prefix)`, FactPublish, FactGrantedPublishPrefix), + fmt.Sprintf(`allow if %s($type, $name), %s($type, $suffix), $name.ends_with($suffix)`, FactPublish, FactGrantedPublishSuffix), + fmt.Sprintf(`allow if %s($type, $name), %s($type)`, FactPublish, FactGrantedPublishAll), + fmt.Sprintf(`allow if %s($type, $name), %s(true)`, FactPublish, FactGrantedPublishAllTypes), + } + for i, pStr := range BaselineSources.PublishPolicies { + p, err := parser.FromStringPolicy(pStr) + if err != nil { + panic(fmt.Sprintf("failed to parse publish policy %d: %v", i, err)) + } + PublishPolicies = append(PublishPolicies, p) + } + // 2. Target Evaluation Rules // These rules satisfy the check if allow_network_target($fact, $val) injected by the control plane. BaselineSources.Rules = []string{ @@ -761,6 +805,34 @@ func isExactTarget(targetStr string) (tFact, tVal string, exact bool) { return tFact, tVal, BuildTargetDatalogFact(targetStr).Name == FactGrantedTargetExact } +// publishFactNames maps each granted_service_* fact to its granted_publish_* +// counterpart. +var publishFactNames = map[string]string{ + FactGrantedServiceAllTypes: FactGrantedPublishAllTypes, + FactGrantedServiceAll: FactGrantedPublishAll, + FactGrantedServiceSuffix: FactGrantedPublishSuffix, + FactGrantedServicePrefix: FactGrantedPublishPrefix, + FactGrantedServiceExact: FactGrantedPublishExact, + FactGrantedServiceSet: FactGrantedPublishSet, +} + +// BuildPublishDatalogFacts translates a role's allowed_publish patterns into +// granted_publish_* facts, the same way and with the same terms as +// BuildServiceDatalogFacts does for allowed_services. +func BuildPublishDatalogFacts(services []string) []biscuit.Fact { + facts := BuildServiceDatalogFacts(services) + for i := range facts { + facts[i].Name = publishFactNames[facts[i].Name] + } + return facts +} + +// PublishFact is the fact a verifier adds to ask whether a credential grants +// serving a service: publish("", ""). +func PublishFact(svcType, svcName string) biscuit.Fact { + return biscuit.Fact{Predicate: biscuit.Predicate{Name: FactPublish, IDs: []biscuit.Term{biscuit.String(svcType), biscuit.String(svcName)}}} +} + // BuildServiceDatalogFacts translates a list of service patterns into a minimal set of Datalog facts. // Exact-match entries are grouped by service type into a single granted_service_set fact each, so // token/world fact counts stay flat regardless of how many exact services a role grants. Wildcard, diff --git a/api/egress.go b/api/egress.go index 578cbfaa..95ecbfd7 100644 --- a/api/egress.go +++ b/api/egress.go @@ -21,6 +21,7 @@ import ( "strings" "github.com/eclipse-biscuit/biscuit-go/v2" + "google.golang.org/protobuf/proto" ) // credentialNameSyntax bounds a credential name to one safe file name: the @@ -228,6 +229,43 @@ func EgressServedBy(d *EgressDestination, roles []string, labels map[string]stri return false } +// WithEgressPublish returns policyRoles with the egress destinations a node +// with these roles and labels serves added to what it may publish: served_by +// is who may serve a destination, and the record the node announces for it +// needs the grant like any other. The role entries are copied, never +// changed in place; a role the policy does not define gets an entry that +// grants publishing alone. +func WithEgressPublish(policyRoles []*PolicyRole, egress []*EgressDestination, roles []string, labels map[string]string) []*PolicyRole { + var served []string + for _, d := range egress { + if d == nil || ValidateEgressName(d.GetName()) != nil { + continue + } + if EgressServedBy(d, roles, labels) { + served = append(served, ServiceTypeStringEgress+"://"+d.GetName()) + } + } + if len(served) == 0 || len(roles) == 0 { + return policyRoles + } + out := make([]*PolicyRole, 0, len(policyRoles)+1) + found := false + for _, pr := range policyRoles { + if pr != nil && pr.GetName() == roles[0] { + cp := proto.Clone(pr).(*PolicyRole) + cp.AllowedPublish = append(cp.AllowedPublish, served...) + out = append(out, cp) + found = true + continue + } + out = append(out, pr) + } + if !found { + out = append(out, &PolicyRole{Name: roles[0], AllowedPublish: served}) + } + return out +} + // BuildEgressServingRules grants each destination to the nodes that serve it: // granted_service_exact("egress", name) <- role(r) or <- label(k, v) for every // served_by entry. The serving node evaluates its own credential when a local diff --git a/api/egress_test.go b/api/egress_test.go index c10cd053..de1a98b7 100644 --- a/api/egress_test.go +++ b/api/egress_test.go @@ -151,3 +151,36 @@ func TestBuildEgressServingRules(t *testing.T) { t.Fatalf("rendered rules do not parse back: %v", err) } } + +// served_by is who serves a destination, so the nodes it selects get the +// publish grant for egress:// with their roles, and nobody else. +func TestWithEgressPublish(t *testing.T) { + policyRoles := []*PolicyRole{{Name: "node", AllowedPublish: []string{"mcp://calc"}}, {Name: "other"}} + egress := []*EgressDestination{ + {Name: "api.github.com", ServedBy: []string{"node"}}, + {Name: "mam.internal.example.com", ServedBy: []string{"site=eu"}}, + {Name: "nobody.example", ServedBy: []string{"pep"}}, + } + got := WithEgressPublish(policyRoles, egress, []string{"node"}, map[string]string{"site": "eu"}) + var node *PolicyRole + for _, r := range got { + if r.GetName() == "node" { + node = r + } + } + want := []string{"mcp://calc", "egress://api.github.com", "egress://mam.internal.example.com"} + if node == nil || !slices.Equal(node.AllowedPublish, want) { + t.Fatalf("node role publishes %v, want %v", node.GetAllowedPublish(), want) + } + if slices.Equal(policyRoles[0].AllowedPublish, want) { + t.Fatal("the policy's own role entry was changed in place") + } + if len(WithEgressPublish(policyRoles, egress, []string{"other"}, nil)) != len(policyRoles) || len(WithEgressPublish(policyRoles, egress, []string{"other"}, nil)[1].AllowedPublish) != 0 { + t.Fatal("a role no destination selects got a grant") + } + // A role the policy does not define gets an entry with the grant alone. + got = WithEgressPublish(nil, egress, []string{"mesh:role:node"}, map[string]string{"site": "eu"}) + if len(got) != 1 || got[0].GetName() != "mesh:role:node" || !slices.Equal(got[0].AllowedPublish, []string{"egress://mam.internal.example.com"}) { + t.Fatalf("undefined role: got %v", got) + } +} diff --git a/charts/agentmesh/templates/bootstrap-job.yaml b/charts/agentmesh/templates/bootstrap-job.yaml index b722b4cc..fd422455 100644 --- a/charts/agentmesh/templates/bootstrap-job.yaml +++ b/charts/agentmesh/templates/bootstrap-job.yaml @@ -118,9 +118,9 @@ spec: -d '{ "roles": [ {"name": "mesh-admin", "allowed_services": ["*"], "allowed_targets": ["*"]}, - {"name": "mesh:role:agentmesh-box", "allowed_services": ["*"], "allowed_targets": ["*"]}, + {"name": "mesh:role:agentmesh-box", "allowed_services": ["*"], "allowed_publish": ["*"], "allowed_targets": ["*"]}, {"name": "mesh:role:router", "allowed_services": ["*"], "allowed_targets": ["*"]}, - {"name": "mesh:role:node", "allowed_services": {{ toJson .Values.bootstrap.nodeServices }}, "allowed_targets": ["*"], "allowed_labels": {{ toJson .Values.bootstrap.nodeLabels }}} + {"name": "mesh:role:node", "allowed_services": {{ toJson .Values.bootstrap.nodeServices }}, "allowed_publish": {{ toJson .Values.bootstrap.nodePublish }}, "allowed_targets": ["*"], "allowed_labels": {{ toJson .Values.bootstrap.nodeLabels }}} ], "bindings": {{ toJson $bindings }} }' \ diff --git a/charts/agentmesh/tests/bootstrap-job_test.yaml b/charts/agentmesh/tests/bootstrap-job_test.yaml index 4c8e064c..251d0e51 100644 --- a/charts/agentmesh/tests/bootstrap-job_test.yaml +++ b/charts/agentmesh/tests/bootstrap-job_test.yaml @@ -63,7 +63,7 @@ tests: asserts: - matchRegex: path: spec.template.spec.containers[0].args[0] - pattern: '"name": "mesh:role:node", "allowed_services": \[\]' + pattern: '"name": "mesh:role:node", "allowed_services": \[\], "allowed_publish": \[\]' - it: no identity may enroll as a node by default (fail closed) documentSelector: @@ -93,10 +93,11 @@ tests: set: bootstrap: nodeServices: ["mcp://calculator", "system://mesh.catalog"] + nodePublish: ["mcp://calculator"] asserts: - matchRegex: path: spec.template.spec.containers[0].args[0] - pattern: '"name": "mesh:role:node", "allowed_services": \["mcp://calculator","system://mesh.catalog"\]' + pattern: '"name": "mesh:role:node", "allowed_services": \["mcp://calculator","system://mesh.catalog"\], "allowed_publish": \["mcp://calculator"\]' - it: node role grants only the configured label patterns documentSelector: diff --git a/charts/agentmesh/values.yaml b/charts/agentmesh/values.yaml index 86acf8ac..7e158efc 100644 --- a/charts/agentmesh/values.yaml +++ b/charts/agentmesh/values.yaml @@ -213,6 +213,12 @@ bootstrap: # until a policy grants it (fail closed); never use ["*"] outside an # intentionally public mesh. nodeServices: [] + # Services mesh:role:node may serve, as "type://name" entries (e.g. + # "mcp://calculator"): what a node may announce to the directory and what + # callers accept it as a provider of. Empty means nodes can publish nothing + # until a policy grants it (fail closed); the same caution as nodeServices + # applies to ["*"]. + nodePublish: [] # Label patterns ("key=value" or "key=*") mesh:role:node may attest with the # labels in its agentmesh-node.yaml. Empty means nodes cannot enroll with any label # (fail closed). diff --git a/development/examples/multi-tier-guardrails/policy.json b/development/examples/multi-tier-guardrails/policy.json index 6c618acd..d8c2b056 100644 --- a/development/examples/multi-tier-guardrails/policy.json +++ b/development/examples/multi-tier-guardrails/policy.json @@ -9,6 +9,7 @@ "mcp://orders-db", "egress://api.github.com" ], + "allowed_publish": ["a2a://support.acme", "inference://office-llm", "mcp://orders-db"], "allowed_targets": ["*"], "allowed_labels": [ "env=staging", diff --git a/development/examples/sandboxed-agent/policy.json b/development/examples/sandboxed-agent/policy.json index a501732c..7c31b3aa 100644 --- a/development/examples/sandboxed-agent/policy.json +++ b/development/examples/sandboxed-agent/policy.json @@ -3,6 +3,7 @@ { "name": "mesh:role:node", "allowed_services": ["system://mesh.catalog"], + "allowed_publish": ["inference://office-llm", "mcp://tools"], "allowed_targets": ["*"], "allowed_labels": ["site=office"] }, diff --git a/development/kind/run.sh b/development/kind/run.sh index 0b11a6ad..8225f359 100755 --- a/development/kind/run.sh +++ b/development/kind/run.sh @@ -93,9 +93,10 @@ gateway_ip() { # deploy_chart [extra --set flags]: later flags win, so phase 2 overrides phase 1. # TLS verification is skipped because one issuer is the kind cluster's own API server, # served with a self-signed cert. -# bootstrap.nodeServices is fail closed by default; this is a local dev mesh whose -# nodes host whatever example service is being tested, so grant them everything -# explicitly here rather than weakening the chart default. +# bootstrap.nodeServices and bootstrap.nodePublish are fail closed by default; +# this is a local dev mesh whose nodes host whatever example service is being +# tested, so grant them everything explicitly here rather than weakening the +# chart default. deploy_chart() { "${HELM}" --kube-context "${KCTX}" upgrade --install agentmesh "${PROJECT_ROOT}/charts/agentmesh" --timeout 10m \ --namespace "${NAMESPACE}" \ @@ -104,6 +105,7 @@ deploy_chart() { --set controlPlane.allowedAudiences="${ALLOWED_AUDIENCES//,/\\,}" \ --set controlPlane.insecureSkipTlsVerify=true \ --set 'bootstrap.nodeServices={*}' \ + --set 'bootstrap.nodePublish={*}' \ --set 'bootstrap.nodeLabels={*}' \ --set 'bootstrap.nodeMembers={mesh:system:authenticated}' \ --set gateway.enabled=true \ diff --git a/hack/gen-sdk-datalog/main.go b/hack/gen-sdk-datalog/main.go index b44ac193..972cf8c7 100644 --- a/hack/gen-sdk-datalog/main.go +++ b/hack/gen-sdk-datalog/main.go @@ -58,6 +58,8 @@ type artifact struct { FactTime string `json:"fact_time"` FactRole string `json:"fact_role"` FactTargetFact string `json:"fact_target_fact"` + FactPublish string `json:"fact_publish"` + CatalogTarget string `json:"catalog_target"` FactTARBlock string `json:"fact_tar_block"` TARBlockSourcePattern string `json:"tar_block_source_pattern"` HTTPMethodSyntax string `json:"http_method_syntax"` @@ -714,6 +716,8 @@ func main() { FactTime: api.FactTime, FactRole: api.FactRole, FactTargetFact: api.FactTargetFact, + FactPublish: api.FactPublish, + CatalogTarget: api.CatalogTarget, FactTARBlock: api.FactTARBlock, TARBlockSourcePattern: api.TARBlockSourcePattern, HTTPMethodSyntax: api.HTTPMethodSyntaxPattern(), diff --git a/internal/controlplane/server.go b/internal/controlplane/server.go index ce2ac2d5..f90697c8 100644 --- a/internal/controlplane/server.go +++ b/internal/controlplane/server.go @@ -862,7 +862,7 @@ func (s *Server) HandleRegister(w http.ResponseWriter, r *http.Request) { if sessionExpiresAt.Before(biscuitExpiry) { biscuitExpiry = sessionExpiresAt } - biscuitData, _, err := identity.MintBiscuitToken(privKey, claims, token, pID, biscuitExpiry, finalRoles, policyRoles, req.Labels) + biscuitData, _, err := identity.MintBiscuitToken(privKey, claims, token, pID, biscuitExpiry, finalRoles, s.mintRoles(r.Context(), policyRoles, finalRoles, req.Labels), req.Labels) if err != nil { logger.Errorw("Biscuit minting failed", "peer_id", canonical, "error", err) http.Error(w, "Failed to mint biscuit: "+err.Error(), http.StatusForbidden) @@ -1226,7 +1226,7 @@ func (s *Server) HandleRefresh(w http.ResponseWriter, r *http.Request) { finalRoles = append(finalRoles, customAccessRoles...) nodeRecord.Labels = labelsStillAllowed(canonical, allowedLabelPatterns(finalRoles, policyRoles), nodeRecord.Labels) - bBytes, _, err := identity.MintBiscuitToken(privKey, claims, nil, pID, biscuitExpiry, finalRoles, policyRoles, nodeRecord.Labels) + bBytes, _, err := identity.MintBiscuitToken(privKey, claims, nil, pID, biscuitExpiry, finalRoles, s.mintRoles(r.Context(), policyRoles, finalRoles, nodeRecord.Labels), nodeRecord.Labels) if err != nil { logger.Errorf("Failed to mint refreshed token for node %s: %v", nodeRecord.PeerID, err) http.Error(w, "Internal server error", http.StatusInternalServerError) @@ -1237,7 +1237,7 @@ func (s *Server) HandleRefresh(w http.ResponseWriter, r *http.Request) { // Bootstrap node finalRoles, _ := nodeRoles(nodeRecord, bindings) nodeRecord.Labels = labelsStillAllowed(canonical, allowedLabelPatterns(finalRoles, policyRoles), nodeRecord.Labels) - bBytes, err := identity.MintBootstrapBiscuitToken(privKey, pID, nodeRecord.Role, biscuitExpiry, policyRoles, nodeRecord.Labels) + bBytes, err := identity.MintBootstrapBiscuitToken(privKey, pID, nodeRecord.Role, biscuitExpiry, s.mintRoles(r.Context(), policyRoles, []string{nodeRecord.Role}, nodeRecord.Labels), nodeRecord.Labels) if err != nil { logger.Errorf("Failed to mint refreshed token for node %s: %v", nodeRecord.PeerID, err) http.Error(w, "Internal server error", http.StatusInternalServerError) @@ -1763,6 +1763,19 @@ func (s *Server) HandleEgress(w http.ResponseWriter, r *http.Request) { _, _ = w.Write(respData) } +// mintRoles is policyRoles as a credential for a member with these roles +// and labels is minted from: with the egress destinations served_by grants +// it added to what it may publish. A failure to read the destinations +// mints without them; the next refresh tries again. +func (s *Server) mintRoles(ctx context.Context, policyRoles []*api.PolicyRole, roles []string, labels map[string]string) []*api.PolicyRole { + egress, err := s.store.GetEgressDestinations(ctx) + if err != nil { + logger.Warnf("Minting without egress publish grants: destinations not read: %v", err) + return policyRoles + } + return api.WithEgressPublish(policyRoles, egress, roles, labels) +} + // nodeRoles is the role set a refresh would mint for the node: the enrolled // role plus the custom roles its identity resolves to, from the bindings. func nodeRoles(nodeRecord *storage.EnrolledNode, bindings []*api.PolicyBinding) ([]string, error) { @@ -2128,7 +2141,7 @@ func (s *Server) HandleEnroll(w http.ResponseWriter, r *http.Request) { http.Error(w, "Internal server error", http.StatusInternalServerError) return } - biscuitBytes, err := identity.MintBootstrapBiscuitToken(privKey, pID, tokenRecord.Role, time.Now().Add(s.config.BiscuitTTL), policyRoles, req.Labels) + biscuitBytes, err := identity.MintBootstrapBiscuitToken(privKey, pID, tokenRecord.Role, time.Now().Add(s.config.BiscuitTTL), s.mintRoles(ctx, policyRoles, []string{tokenRecord.Role}, req.Labels), req.Labels) if err != nil { logger.Errorf("Failed to mint bootstrap biscuit: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) @@ -2740,7 +2753,7 @@ func (s *Server) HandleAdminEnrollmentAction(w http.ResponseWriter, r *http.Requ return } - biscuitBytes, err := identity.MintBootstrapBiscuitToken(privKey, pID, tokenRecord.Role, time.Now().Add(s.config.BiscuitTTL), policyRoles, enrollReq.Labels) + biscuitBytes, err := identity.MintBootstrapBiscuitToken(privKey, pID, tokenRecord.Role, time.Now().Add(s.config.BiscuitTTL), s.mintRoles(ctx, policyRoles, []string{tokenRecord.Role}, enrollReq.Labels), enrollReq.Labels) if err != nil { logger.Errorf("Failed to mint bootstrap biscuit: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) @@ -3019,7 +3032,7 @@ func (s *Server) remintApprovedBootstrapBiscuit(ctx context.Context, existingReq biscuitExpiry := time.Now().Add(s.config.BiscuitTTL) allowedLabels := filterAllowedLabels(allowedLabelPatterns(s.nodeRoles(ctx, nodeRecord), policyRoles), nodeRecord.Labels) - biscuitBytes, err := identity.MintBootstrapBiscuitToken(privKey, pID, nodeRecord.Role, biscuitExpiry, policyRoles, allowedLabels) + biscuitBytes, err := identity.MintBootstrapBiscuitToken(privKey, pID, nodeRecord.Role, biscuitExpiry, s.mintRoles(ctx, policyRoles, []string{nodeRecord.Role}, allowedLabels), allowedLabels) if err != nil { return nil, nil, fmt.Errorf("failed to mint refreshed bootstrap biscuit: %w", err) } @@ -3557,6 +3570,11 @@ func validatePolicyConfig(req *api.PolicyConfig) error { return fmt.Errorf("in role %s: %w", r.Name, err) } } + for _, svc := range r.AllowedPublish { + if err := api.ValidateServiceFormat(svc); err != nil { + return fmt.Errorf("invalid allowed_publish %q in role %s: %w", svc, r.Name, err) + } + } if err := api.ValidateHTTPGrants(r.Http, r.AllowedServices); err != nil { return fmt.Errorf("in role %s: %w", r.Name, err) } @@ -3577,6 +3595,7 @@ func validatePolicyConfig(req *api.PolicyConfig) error { // sum each role's contribution rather than relying on assumptions about // which roles are mutually exclusive for a given identity. factBudget += len(api.BuildServiceDatalogFacts(r.AllowedServices)) + factBudget += len(api.BuildPublishDatalogFacts(r.AllowedPublish)) factBudget += len(api.BuildTargetDatalogFacts(r.AllowedTargets)) factBudget += len(r.CustomDatalog) for _, g := range r.Http { diff --git a/internal/identity/biscuit.go b/internal/identity/biscuit.go index 6d206ae7..9a5de97d 100644 --- a/internal/identity/biscuit.go +++ b/internal/identity/biscuit.go @@ -436,6 +436,9 @@ func mintBiscuitWithBindingAndRand(rng io.Reader, signingKey ed25519.PrivateKey, // Narrowed grants (PolicyRole.http) are minted per entry: they are keyed // by the entry they narrow, so there is nothing to merge across roles. var allHTTP []*api.HTTPGrant + // What the holder may serve. A delegated subject serves nothing: it is a + // caller the actor node speaks for, not a member that publishes. + var allPublish []string for _, role := range roles { if err := addFact(biscuit.Fact{Predicate: biscuit.Predicate{ Name: api.FactRole, @@ -463,6 +466,9 @@ func mintBiscuitWithBindingAndRand(rng io.Reader, signingKey ed25519.PrivateKey, allServices = append(allServices, plainServices...) allHTTP = append(allHTTP, narrowed...) allTargets = append(allTargets, pr.AllowedTargets...) + if !delegated { + allPublish = append(allPublish, pr.AllowedPublish...) + } for _, customEntry := range pr.CustomDatalog { trimmed := strings.TrimRight(strings.TrimSpace(customEntry), ";") @@ -504,6 +510,11 @@ func mintBiscuitWithBindingAndRand(rng io.Reader, signingKey ed25519.PrivateKey, errs = append(errs, fmt.Errorf("failed to add target fact: %w", err)) } } + for _, fact := range api.BuildPublishDatalogFacts(allPublish) { + if err := addFact(fact); err != nil { + errs = append(errs, fmt.Errorf("failed to add publish fact: %w", err)) + } + } // No policy means no grants. A mesh with no roles defined used to mint // every non-router an unrestricted token; a fresh control plane, or one @@ -805,6 +816,25 @@ func RequireRole(b *biscuit.Biscuit, key ed25519.PublicKey, expectedRole string, return nil } +// RequirePublish reports whether the credential grants serving the service +// svcType://svcName: api.PublishPolicies over the token's granted_publish_* +// facts and a publish() fact. b must have passed verification and key must +// be the key that verified it; what is decided here is the grant alone. +func RequirePublish(b *biscuit.Biscuit, key ed25519.PublicKey, svcType, svcName string, timeout time.Duration) error { + authorizer, err := b.Authorizer(key, AuthorizerOptions(timeout)...) + if err != nil { + return fmt.Errorf("failed to create authorizer: %w", err) + } + authorizer.AddFact(api.PublishFact(svcType, svcName)) + for _, p := range api.PublishPolicies { + authorizer.AddPolicy(p) + } + if err := authorizer.Authorize(); err != nil { + return fmt.Errorf("credential does not grant serving %s://%s", svcType, svcName) + } + return nil +} + // VerifiedBiscuitClaims holds the authority facts, effective expiration, // revocation IDs, and TaskAuthorizationRule chain extracted from a verified // Biscuit token. diff --git a/internal/identity/biscuit_test.go b/internal/identity/biscuit_test.go index 38f3fe4e..5285eff4 100644 --- a/internal/identity/biscuit_test.go +++ b/internal/identity/biscuit_test.go @@ -764,3 +764,61 @@ func TestVerifyBiscuitAndGetExpiry(t *testing.T) { t.Error("token bound to another peer admitted") } } + +// A role's allowed_publish is minted as granted_publish_* facts, which +// RequirePublish decides with api.PublishPolicies: the same patterns as +// allowed_services, and the same matching. A delegated subject gets none: +// it is a caller the actor speaks for, not a member that serves. +func TestMintBiscuitToken_PublishFacts(t *testing.T) { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + p := peer.ID("publisher") + roles := []*api.PolicyRole{{Name: "publisher", AllowedPublish: []string{"mcp://calc", "mcp://weather", "inference://*", "a2a://*.agents", "mcp://tools.*"}}} + data, err := MintBootstrapBiscuitToken(priv, p, "publisher", time.Now().Add(time.Hour), roles, nil) + if err != nil { + t.Fatal(err) + } + b, err := biscuit.Unmarshal(data) + if err != nil { + t.Fatal(err) + } + for _, tc := range []struct { + svcType, svcName string + granted bool + }{ + {"mcp", "calc", true}, + {"mcp", "weather", true}, + {"mcp", "clock", false}, + {"inference", "anything", true}, + {"a2a", "helper.agents", true}, + {"a2a", "helper", false}, + {"mcp", "tools.v2", true}, + {"mcp", "tools", false}, + {"egress", "api.github.com", false}, + } { + err := RequirePublish(b, pub, tc.svcType, tc.svcName, testTimeout) + if (err == nil) != tc.granted { + t.Errorf("RequirePublish(%s://%s) = %v, want granted %v", tc.svcType, tc.svcName, err, tc.granted) + } + } + + none, err := MintBootstrapBiscuitToken(priv, p, "publisher", time.Now().Add(time.Hour), nil, nil) + if err != nil { + t.Fatal(err) + } + nb, _ := biscuit.Unmarshal(none) + if err := RequirePublish(nb, pub, "mcp", "calc", testTimeout); err == nil { + t.Error("a credential minted under no policy role grants publishing nothing") + } + + delegated, err := MintDelegatedBiscuitToken(priv, jwt.MapClaims{"sub": "user"}, p, time.Now().Add(time.Hour), []string{"publisher"}, roles, nil, false) + if err != nil { + t.Fatal(err) + } + db, _ := biscuit.Unmarshal(delegated) + if err := RequirePublish(db, pub, "mcp", "calc", testTimeout); err == nil { + t.Error("a delegated credential grants publishing nothing") + } +} diff --git a/internal/node/a2a_service.go b/internal/node/a2a_service.go index 6f1a9617..14c1101a 100644 --- a/internal/node/a2a_service.go +++ b/internal/node/a2a_service.go @@ -115,7 +115,7 @@ func a2aEgressGate(node *AgentMeshNode, w http.ResponseWriter, r *http.Request, http.Error(w, "Invalid peer ID", http.StatusBadRequest) return r, false } - if err := node.VerifyPeerLabels(r.Context(), pid, required); err != nil { + if err := node.VerifyProvider(r.Context(), pid, required, route.serviceType+"://"+route.serviceName); err != nil { logger.Warnf("[A2A] label gate refused egress to %s: %v", route.peerID, err) text, status := labelGateStatus(err, "Required labels not attested by provider") http.Error(w, text, status) diff --git a/internal/node/egress_tunnel.go b/internal/node/egress_tunnel.go index 1d5ced50..551c1d13 100644 --- a/internal/node/egress_tunnel.go +++ b/internal/node/egress_tunnel.go @@ -429,7 +429,7 @@ func handleConnectTunnel(node *AgentMeshNode, w http.ResponseWriter, r *http.Req if decErr != nil { continue } - if err := node.VerifyPeerLabels(r.Context(), targetPeer, requiredLabels); err != nil { + if err := node.VerifyProvider(r.Context(), targetPeer, requiredLabels, api.ServiceTypeStringEgress+"://"+host); err != nil { logger.Warnf("[Egress] CONNECT label gate refused egress to %s: %v", targetPeer, err) denied = denied || !errors.Is(err, ErrProviderUnreachable) continue diff --git a/internal/node/labels_gate.go b/internal/node/labels_gate.go index 293e9bf2..80ecee36 100644 --- a/internal/node/labels_gate.go +++ b/internal/node/labels_gate.go @@ -99,6 +99,11 @@ const ( // is the mesh's state and a caller may retry it; the second is a verdict. var ErrProviderUnreachable = errors.New("provider unreachable") +// ErrProviderNotGranted marks a provider whose credential does not grant +// serving the service the call names: it is enrolled and reachable, and +// not who the policy says serves that name. +var ErrProviderNotGranted = errors.New("provider not granted the service") + // How reaching a provider for its credential went; the label values of // providerReachTotal. The caller sees one 502 for all of the failures, so // the counter tells them apart: the relay refusing the circuit, the dial @@ -198,19 +203,29 @@ func (n *AgentMeshNode) egressFloor() map[string]string { return n.nodeConfig.EgressRequireLabels } -// VerifyPeerLabels ensures the peer holds control-plane-attested labels +// VerifyPeerLabels is VerifyProvider for a peer reached for no named +// service: its catalog, or a call that names the peer alone. +func (n *AgentMeshNode) VerifyPeerLabels(ctx context.Context, peerID peer.ID, required map[string]string) error { + return n.VerifyProvider(ctx, peerID, required, "") +} + +// VerifyProvider ensures the peer is a provider this node may send a request +// to: it holds a control-plane-attested credential bound to it, with labels // satisfying every pair of the caller's requirement and every pair of the -// operator's egress floor. Positive verdicts are cached. +// operator's egress floor, and, when the call names a service +// ("://"), one that grants serving it: a member that announced a +// name it may not serve is refused here whatever the directory says. +// Positive verdicts are cached. // // The gate runs whenever either exists. A caller that requires nothing is // still held to the floor — the whole point of a floor is that the party being // constrained does not get to opt out of it by saying nothing. -func (n *AgentMeshNode) VerifyPeerLabels(ctx context.Context, peerID peer.ID, required map[string]string) error { +func (n *AgentMeshNode) VerifyProvider(ctx context.Context, peerID peer.ID, required map[string]string, service string) error { floor := n.egressFloor() // No early return for an empty requirement: with nothing to attest this // still verifies that the peer holds a control-plane-signed biscuit bound // to it, which is what makes a discovered peer a provider at all. - key := labelGateKey(peerID, required, floor) + key := labelGateKey(peerID, required, floor) + "=svc=" + service if until, ok := n.peerLabelGate.Get(key); ok && time.Now().Before(until) { return nil } @@ -221,7 +236,7 @@ func (n *AgentMeshNode) VerifyPeerLabels(ctx context.Context, peerID peer.ID, re return fmt.Errorf("provider %s labels unverifiable: %w: %w", peerID, ErrProviderUnreachable, err) } providerReachTotal.WithLabelValues(reachOK).Inc() - if err := n.checkPeerLabels(providerBiscuit, peerID, required); err != nil { + if err := n.checkProvider(providerBiscuit, peerID, required, service); err != nil { return err } @@ -229,10 +244,16 @@ func (n *AgentMeshNode) VerifyPeerLabels(ctx context.Context, peerID peer.ID, re return nil } -// checkPeerLabels verifies the provider's biscuit (control-plane signature, -// expiry, binding to peerID) and evaluates the caller's requirement and the -// operator's egress floor against its attested facts. +// checkPeerLabels is checkProvider for no named service. func (n *AgentMeshNode) checkPeerLabels(providerBiscuit []byte, peerID peer.ID, required map[string]string) error { + return n.checkProvider(providerBiscuit, peerID, required, "") +} + +// checkProvider verifies the provider's biscuit (control-plane signature, +// expiry, binding to peerID) and evaluates the caller's requirement, the +// operator's egress floor and, for a named service, the publish grant +// against its attested facts. +func (n *AgentMeshNode) checkProvider(providerBiscuit []byte, peerID peer.ID, required map[string]string, service string) error { floor := n.egressFloor() if len(providerBiscuit) == 0 { return fmt.Errorf("provider %s returned no identity biscuit; not an enrolled peer (required labels %v, egress floor %v)", peerID, required, floor) @@ -286,6 +307,16 @@ func (n *AgentMeshNode) checkPeerLabels(providerBiscuit []byte, peerID peer.ID, } return fmt.Errorf("provider %s does not attest every label of %v: %w", peerID, required, err) } + if service != "" && service != "system://"+api.CatalogTarget { + // The catalog is every node's own and names no published service. + svcType, svcName := api.ParseServiceTarget(service) + if svcType == "" || svcName == "" { + return fmt.Errorf("service %q must be ://", service) + } + if err := identity.RequirePublish(b, key, svcType, svcName, n.BiscuitTimeout); err != nil { + return fmt.Errorf("%w: %w", ErrProviderNotGranted, err) + } + } return nil } diff --git a/internal/node/labels_gate_test.go b/internal/node/labels_gate_test.go index 5ef50921..e3694619 100644 --- a/internal/node/labels_gate_test.go +++ b/internal/node/labels_gate_test.go @@ -122,6 +122,44 @@ func TestCheckPeerLabels(t *testing.T) { t.Errorf("a node's biscuit with no requirement must pass: %v", err) } }) + + // A call that names a service is sent only to a provider whose credential + // grants serving it, whatever the directory said; a call that names none + // (the peer's catalog) needs no grant. + t.Run("the provider must be granted the service the call names", func(t *testing.T) { + mintPublish := func(publish ...string) []byte { + role := &api.PolicyRole{Name: api.RoleNode, AllowedPublish: publish} + b, err := identity.MintBootstrapBiscuitToken(cpPriv, providerPeer, api.RoleNode, expiry, []*api.PolicyRole{role}, nil) + if err != nil { + t.Fatal(err) + } + return b + } + for name, tc := range map[string]struct { + publish []string + service string + wantErr bool + }{ + "granted by name": {publish: []string{"mcp://calc"}, service: "mcp://calc"}, + "granted by type": {publish: []string{"mcp://*"}, service: "mcp://calc"}, + "granted everything": {publish: []string{"*"}, service: "inference://llm"}, + "another service": {publish: []string{"mcp://calc"}, service: "mcp://weather", wantErr: true}, + "another type": {publish: []string{"mcp://*"}, service: "inference://calc", wantErr: true}, + "no grant at all": {publish: nil, service: "mcp://calc", wantErr: true}, + "no grant, no service named": {publish: nil, service: ""}, + "a service that is not one": {publish: []string{"*"}, service: "calc", wantErr: true}, + } { + t.Run(name, func(t *testing.T) { + err := node.checkProvider(mintPublish(tc.publish...), providerPeer, nil, tc.service) + if (err != nil) != tc.wantErr { + t.Fatalf("checkProvider(%v, %q) = %v, wantErr %v", tc.publish, tc.service, err, tc.wantErr) + } + if tc.wantErr && tc.service != "calc" && !errors.Is(err, ErrProviderNotGranted) { + t.Fatalf("err = %v, want ErrProviderNotGranted so the caller is told the provider is enrolled and not granted", err) + } + }) + } + }) } // The egress floor is the operator's, and a caller cannot waive it by asking diff --git a/internal/node/mcp.go b/internal/node/mcp.go index 21435a6a..c6c455ab 100644 --- a/internal/node/mcp.go +++ b/internal/node/mcp.go @@ -340,11 +340,13 @@ func (n *AgentMeshNode) ConnectMCPSession(ctx context.Context, targetPeer peer.I return nil, nil, fmt.Errorf("%w by %s: %s", ErrAuthRejected, targetPeer, resp.Error) } - // The provider's biscuit is verified unconditionally: signature, expiry and - // binding to targetPeer. Discovery (DHT, gossip) names whoever announced - // the service; only this says the peer is enrolled. Caller-required labels - // and the operator's egress floor are additional checks on the same token. - if err := n.checkPeerLabels(resp.Biscuit, targetPeer, requiredLabels); err != nil { + // The provider's biscuit is verified unconditionally: signature, expiry, + // binding to targetPeer and, for a named service, the grant to serve it. + // The directory names whoever announced the service; only this says the + // peer is enrolled and may serve it. Caller-required labels and the + // operator's egress floor are additional checks on the same token. An + // empty target is the peer's own catalog, which names no service. + if err := n.checkProvider(resp.Biscuit, targetPeer, requiredLabels, targetService); err != nil { cleanup() return nil, nil, err } diff --git a/internal/node/mcp_handlers_test.go b/internal/node/mcp_handlers_test.go index ad03b44e..dfedc2f6 100644 --- a/internal/node/mcp_handlers_test.go +++ b/internal/node/mcp_handlers_test.go @@ -34,9 +34,13 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" ) +// publishEverything is the node role as a test mesh's policy grants it: +// serving every service, so a node's record and its calls pass the gates. +var publishEverything = []*api.PolicyRole{{Name: api.RoleNode, AllowedPublish: []string{"*"}}} + // buildAndSaveBiscuit builds a biscuit signed with rootPriv that identifies -// node as caller, grants allow_mcp_server("*"), carries the node role as a -// real enrollment does, and saves it to node's store. +// node as caller, grants every service to call and to serve, carries the +// node role as a real enrollment does, and saves it to node's store. func buildAndSaveBiscuit(node *AgentMeshNode, rootPriv ed25519.PrivateKey) error { callerID := node.Host.ID().String() builder := biscuit.NewBuilder(rootPriv) @@ -62,6 +66,9 @@ func buildAndSaveBiscuit(node *AgentMeshNode, rootPriv ed25519.PrivateKey) error if err := builder.AddAuthorityFact(api.MarkerFact(api.FactGrantedServiceAllTypes)); err != nil { return err } + if err := builder.AddAuthorityFact(api.MarkerFact(api.FactGrantedPublishAllTypes)); err != nil { + return err + } if err := builder.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ Name: api.FactExpiration, IDs: []biscuit.Term{biscuit.Date(time.Now().Add(time.Hour))}, @@ -556,7 +563,7 @@ func TestCallMCPTool_LabelEnforcement(t *testing.T) { // nodeB's own identity is control-plane-attested with region=eu-de; nodeA // must trust the same root to verify it. - nodeBIdentity, err := identity.MintBootstrapBiscuitToken(rootPriv, nodeB.Host.ID(), api.RoleNode, time.Now().Add(time.Hour), nil, map[string]string{"region": "eu-de"}) + nodeBIdentity, err := identity.MintBootstrapBiscuitToken(rootPriv, nodeB.Host.ID(), api.RoleNode, time.Now().Add(time.Hour), publishEverything, map[string]string{"region": "eu-de"}) if err != nil { t.Fatalf("mint nodeB identity: %v", err) } @@ -618,7 +625,7 @@ func TestCallMCPTool_EgressFloorEnforcement(t *testing.T) { nodeB.keysMu.Unlock() // nodeB attests region=eu-de and nothing else. - nodeBIdentity, err := identity.MintBootstrapBiscuitToken(rootPriv, nodeB.Host.ID(), api.RoleNode, time.Now().Add(time.Hour), nil, map[string]string{"region": "eu-de"}) + nodeBIdentity, err := identity.MintBootstrapBiscuitToken(rootPriv, nodeB.Host.ID(), api.RoleNode, time.Now().Add(time.Hour), publishEverything, map[string]string{"region": "eu-de"}) if err != nil { t.Fatalf("mint nodeB identity: %v", err) } diff --git a/internal/node/openai_facade.go b/internal/node/openai_facade.go index 5c50f456..52c8da7e 100644 --- a/internal/node/openai_facade.go +++ b/internal/node/openai_facade.go @@ -90,7 +90,7 @@ type openAIFacade struct { // when a requirement exists): verifies the provider's // control-plane-attested labels before any request data is sent // (see labels_gate.go). - verifyPeerLabels func(ctx context.Context, peerID string, required map[string]string) error + verifyPeerLabels func(ctx context.Context, peerID, service string, required map[string]string) error authorizeLocal func(ctx context.Context, serviceName, method, path string) error ttl time.Duration @@ -128,12 +128,12 @@ func newOpenAIFacade(node *AgentMeshNode, egress http.Handler) *openAIFacade { }, localLabels: node.labels, egressFloor: node.egressFloor, - verifyPeerLabels: func(ctx context.Context, peerID string, required map[string]string) error { + verifyPeerLabels: func(ctx context.Context, peerID, service string, required map[string]string) error { pid, err := peer.Decode(peerID) if err != nil { return fmt.Errorf("invalid peer ID %q: %w", peerID, err) } - return node.VerifyPeerLabels(ctx, pid, required) + return node.VerifyProvider(ctx, pid, required, api.ServiceTypeStringInference+"://"+service) }, authorizeLocal: func(ctx context.Context, serviceName, method, path string) error { callerBiscuit := CallerBiscuitFromContext(ctx) @@ -410,7 +410,7 @@ func (f *openAIFacade) handleCompletions(w http.ResponseWriter, r *http.Request) } // No backoff on failure: the verdict is requirement-scoped, // the provider stays eligible for unconstrained requests. - if err := f.verifyPeerLabels(r.Context(), p.peerID, requiredLabels); err != nil { + if err := f.verifyPeerLabels(r.Context(), p.peerID, p.service, requiredLabels); err != nil { // With no caller requirement what failed is the floor or the // peer's own identity; attribute it so the operator can tell // their floor apart from a caller's requirement. diff --git a/internal/node/openai_facade_test.go b/internal/node/openai_facade_test.go index 5d925ffc..a0fcb560 100644 --- a/internal/node/openai_facade_test.go +++ b/internal/node/openai_facade_test.go @@ -64,7 +64,7 @@ func newTestFacade() *openAIFacade { remoteModels: func(_ context.Context, _, _ string) ([]string, error) { return nil, nil }, - verifyPeerLabels: func(context.Context, string, map[string]string) error { return nil }, + verifyPeerLabels: func(context.Context, string, string, map[string]string) error { return nil }, authorizeLocal: func(context.Context, string, string, string) error { return nil }, } } @@ -704,7 +704,7 @@ func TestFacade_Completions_LabelRequirement(t *testing.T) { } } var attested [][2]string - f.verifyPeerLabels = func(_ context.Context, peerID string, required map[string]string) error { + f.verifyPeerLabels = func(_ context.Context, peerID, _ string, required map[string]string) error { attested = append(attested, [2]string{peerID, required["region"]}) return nil } @@ -762,7 +762,7 @@ func TestFacade_Completions_LabelAttestation(t *testing.T) { t.Run("unattested provider is skipped, attested one serves", func(t *testing.T) { f := newLabelFacade() - f.verifyPeerLabels = func(_ context.Context, peerID string, _ map[string]string) error { + f.verifyPeerLabels = func(_ context.Context, peerID, _ string, _ map[string]string) error { if peerID == "peerEU1" { return fmt.Errorf("no attested label") } @@ -806,7 +806,7 @@ func TestFacade_Completions_LabelAttestation(t *testing.T) { // provider fails verification, so nothing may be forwarded. f := newLabelFacade() verified := 0 - f.verifyPeerLabels = func(_ context.Context, _ string, required map[string]string) error { + f.verifyPeerLabels = func(_ context.Context, _, _ string, required map[string]string) error { verified++ if len(required) != 0 { t.Errorf("required = %v, want none", required) @@ -984,7 +984,7 @@ func TestFacade_Completions_FloorGatesSilentCaller(t *testing.T) { f := newFloorFacade() var gateRequired map[string]string gateCalled := false - f.verifyPeerLabels = func(_ context.Context, _ string, required map[string]string) error { + f.verifyPeerLabels = func(_ context.Context, _, _ string, required map[string]string) error { gateCalled = true gateRequired = required return fmt.Errorf("floor not attested") @@ -1011,7 +1011,7 @@ func TestFacade_Completions_FloorGatesSilentCaller(t *testing.T) { t.Run("an attested floor forwards", func(t *testing.T) { f := newFloorFacade() - f.verifyPeerLabels = func(context.Context, string, map[string]string) error { return nil } + f.verifyPeerLabels = func(context.Context, string, string, map[string]string) error { return nil } forwarded := false f.forward = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { forwarded = true diff --git a/internal/node/proxy_test.go b/internal/node/proxy_test.go index 802f25c2..161f6c11 100644 --- a/internal/node/proxy_test.go +++ b/internal/node/proxy_test.go @@ -828,7 +828,7 @@ func TestEgressProxyEnforcesFloor(t *testing.T) { nodeA.keysMu.Unlock() // nodeA attests jurisdiction=eu and nothing else. - nodeAIdentity, err := identity.MintBootstrapBiscuitToken(rootPriv, nodeA.Host.ID(), api.RoleNode, time.Now().Add(time.Hour), nil, map[string]string{"jurisdiction": "eu"}) + nodeAIdentity, err := identity.MintBootstrapBiscuitToken(rootPriv, nodeA.Host.ID(), api.RoleNode, time.Now().Add(time.Hour), publishEverything, map[string]string{"jurisdiction": "eu"}) if err != nil { t.Fatalf("mint nodeA identity: %v", err) } diff --git a/internal/node/sidecar.go b/internal/node/sidecar.go index 5ba57f41..79f3fd7b 100644 --- a/internal/node/sidecar.go +++ b/internal/node/sidecar.go @@ -22,6 +22,7 @@ import ( "crypto/x509" "encoding/base64" "encoding/json" + "errors" "fmt" "net" "net/http" @@ -810,10 +811,16 @@ func createEgressProxy(node *AgentMeshNode) http.Handler { r.URL.Path = strings.Join(parts, "/") } } - if err := node.VerifyPeerLabels(r.Context(), pid, requiredLabels); err != nil { + service := "" + if route.serviceName != "" { + service = route.serviceType + "://" + route.serviceName + } + if err := node.VerifyProvider(r.Context(), pid, requiredLabels, service); err != nil { logger.Warnf("[Egress] refused egress to %s: %v", pid, err) denied := "Forbidden: destination is not an enrolled peer" - if len(requiredLabels) > 0 { + if errors.Is(err, ErrProviderNotGranted) { + denied = "Forbidden: provider is not granted " + service + } else if len(requiredLabels) > 0 { denied = "Required labels not attested by provider" } else if len(node.egressFloor()) > 0 { denied = "Forbidden: provider does not attest the egress floor" diff --git a/internal/router/directory.go b/internal/router/directory.go index 0417655e..e6866bf4 100644 --- a/internal/router/directory.go +++ b/internal/router/directory.go @@ -31,6 +31,7 @@ import ( "google.golang.org/protobuf/proto" "github.com/google/agentmesh/api" + "github.com/google/agentmesh/internal/identity" ) const ( @@ -60,13 +61,14 @@ const ( directoryResultOK = "ok" directoryResultEmpty = "empty" directoryResultInvalid = "invalid" + directoryResultDenied = "denied" directoryResultError = "error" ) var directoryRequestsTotal = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "agentmesh_router_directory_requests_total", - Help: "Directory requests served to members and routers, by operation and result; place is a record or withdrawal another router put here, lookup a find another router ran here", + Help: "Directory requests served to members and routers, by operation and result; denied is a record for a service the member's credential does not grant; place is a record or withdrawal another router put here, lookup a find another router ran here", }, []string{"op", "result"}, ) @@ -98,7 +100,7 @@ var directoryLookupsTotal = promauto.NewCounterVec( func init() { for _, op := range []string{directoryOpProvide, directoryOpWithdraw, directoryOpFindProviders, directoryOpFindPeer, directoryOpPlace, directoryOpLookup} { - for _, result := range []string{directoryResultOK, directoryResultEmpty, directoryResultInvalid, directoryResultError} { + for _, result := range []string{directoryResultOK, directoryResultEmpty, directoryResultInvalid, directoryResultDenied, directoryResultError} { directoryRequestsTotal.WithLabelValues(op, result) } } @@ -203,12 +205,17 @@ var errInvalidDirectoryRequest = errors.New("request is not provide, withdraw, f // directoryRefused answers a request the router did not run, and says why // at the level the cause deserves: a malformed request is the peer's -// doing, a failed lookup is the mesh's. +// doing, a record its credential does not grant is a policy decision, a +// failed lookup is the mesh's. func (r *Router) directoryRefused(op string, remote peer.ID, err error) *api.DirectoryResponse { var invalid *directoryInvalidError + var denied *directoryDeniedError if errors.As(err, &invalid) { directoryRequestsTotal.WithLabelValues(op, directoryResultInvalid).Inc() logger.Warnf("[Directory] Refusing %s from %s: %v", op, remote, err) + } else if errors.As(err, &denied) { + directoryRequestsTotal.WithLabelValues(op, directoryResultDenied).Inc() + logger.Warnw("[Directory] Record denied", "op", op, "peer_id", remote.String(), "service", denied.service, "reason", denied.err.Error()) } else { directoryRequestsTotal.WithLabelValues(op, directoryResultError).Inc() logger.Warnf("[Directory] %s from %s failed: %v", op, remote, err) @@ -226,6 +233,39 @@ func invalidDirectoryRequest(format string, args ...any) error { return &directoryInvalidError{err: fmt.Errorf(format, args...)} } +// directoryDeniedError is a record for a service the member's credential +// does not grant serving. +type directoryDeniedError struct { + service string + err error +} + +func (e *directoryDeniedError) Error() string { return e.err.Error() } +func (e *directoryDeniedError) Unwrap() error { return e.err } + +// requirePublish checks the admitted member's credential grants serving +// the service a record names. A record under the member's own peer key +// names no service and needs no grant: a member may always say where it +// is reached. +func (r *Router) requirePublish(provider peer.ID, svcType api.ServiceType, svcName string) error { + if svcName == "" { + return nil + } + typeStr, err := api.ServiceTypeToString(svcType) + if err != nil { + return invalidDirectoryRequest("%w", err) + } + v, ok := r.credentials.Load(provider) + if !ok { + return &directoryDeniedError{service: typeStr + "://" + svcName, err: errors.New("no credential held for the member")} + } + cred := v.(memberCredential) + if err := identity.RequirePublish(cred.biscuit, cred.key, typeStr, svcName, r.config.BiscuitTimeout); err != nil { + return &directoryDeniedError{service: typeStr + "://" + svcName, err: err} + } + return nil +} + // recordProvider is whose record a provide or withdraw is about: the // sender's own, or the member a peer router names, which only a router may. func (r *Router) recordProvider(remote peer.ID, named string) (provider peer.ID, placed bool, err error) { @@ -259,11 +299,16 @@ func (r *Router) directoryProvide(ctx context.Context, remote peer.ID, q *api.Di return nil, invalidDirectoryRequest("%w", err) } if !placed { - // The member's own router checks what the record is about; a placed - // copy was checked by the router that holds the member. - if _, _, err := api.ServiceOfRecord(provider, q.GetKey(), q.GetService()); err != nil { + // The member's own router checks what the record is about and that + // the member may serve it; a placed copy was checked by the router + // that holds the member. + svcType, svcName, err := api.ServiceOfRecord(provider, q.GetKey(), q.GetService()) + if err != nil { return nil, invalidDirectoryRequest("%w", err) } + if err := r.requirePublish(provider, svcType, svcName); err != nil { + return nil, err + } } if r.records == nil { return nil, errDirectoryNotUp diff --git a/internal/router/directory_test.go b/internal/router/directory_test.go index 7c32c0fc..aefc1433 100644 --- a/internal/router/directory_test.go +++ b/internal/router/directory_test.go @@ -16,10 +16,13 @@ package router import ( "context" + "crypto/ed25519" "slices" "testing" "time" + "github.com/eclipse-biscuit/biscuit-go/v2" + "github.com/golang-jwt/jwt/v5" dht "github.com/libp2p/go-libp2p-kad-dht" kb "github.com/libp2p/go-libp2p-kbucket" "github.com/libp2p/go-libp2p/core/host" @@ -29,6 +32,7 @@ import ( "google.golang.org/protobuf/proto" "github.com/google/agentmesh/api" + "github.com/google/agentmesh/internal/identity" ) // newDirectoryRouter is a router with its DHT and the directory handler @@ -68,11 +72,27 @@ func federate(t *testing.T, a, b *Router) { } } -// attach connects a member host to a router and marks it authenticated there. -func attach(t *testing.T, r *Router) host.Host { +// attach connects a member host to a router and marks it authenticated +// there, holding a credential that grants publishing the given services +// (allowed_publish syntax), as the handshake would have recorded. +func attach(t *testing.T, r *Router, publish ...string) host.Host { t.Helper() m := newClient(t, r) r.authenticatedPeers.Store(m.ID(), time.Now().Add(time.Hour)) + pub, priv, err := ed25519.GenerateKey(nil) + if err != nil { + t.Fatal(err) + } + role := &api.PolicyRole{Name: "member", AllowedPublish: publish} + tok, _, err := identity.MintBiscuitToken(priv, jwt.MapClaims{"sub": "member"}, nil, m.ID(), time.Now().Add(time.Hour), []string{"member"}, []*api.PolicyRole{role}, nil) + if err != nil { + t.Fatal(err) + } + b, err := biscuit.Unmarshal(tok) + if err != nil { + t.Fatal(err) + } + r.credentials.Store(m.ID(), memberCredential{biscuit: b, key: pub}) return m } @@ -141,7 +161,7 @@ func TestDirectoryRecordsAreFoundFromAnyRouter(t *testing.T) { // One copy besides the member's own router, on the closest of the others. a.directory.Replication = 1 - member := attach(t, a) + member := attach(t, a, "mcp://calculator") key, err := api.ServiceKey(api.ServiceType_SERVICE_TYPE_MCP, "calculator") if err != nil { t.Fatal(err) @@ -228,7 +248,7 @@ func TestDirectoryRecordsAreFoundFromAnyRouter(t *testing.T) { // as invalid, and nothing is stored. func TestDirectoryRefusesWhatAMemberMayNotAnnounce(t *testing.T) { a := newDirectoryRouter(t) - member := attach(t, a) + member := attach(t, a, "mcp://*") stranger := newClient(t, a) key, _ := api.ServiceKey(api.ServiceType_SERVICE_TYPE_MCP, "calculator") @@ -275,7 +295,7 @@ func TestDirectoryRefusesWhatAMemberMayNotAnnounce(t *testing.T) { func TestDirectoryPlacementNamesTheProvider(t *testing.T) { a, b := newDirectoryRouter(t), newDirectoryRouter(t) federate(t, a, b) - member := attach(t, a) + member := attach(t, a, "inference://llm") key, _ := api.ServiceKey(api.ServiceType_SERVICE_TYPE_INFERENCE, "") memberAddr := member.Addrs()[0].String() + "/p2p/" + member.ID().String() if resp := ask(t, member, a.Host.ID(), provide(key, "inference://llm", memberAddr)); resp.GetError() != "" { @@ -302,7 +322,7 @@ func withdraw(key []byte) *api.DirectoryRequest { func TestDirectoryWithdrawAndReplace(t *testing.T) { a, b := newDirectoryRouter(t), newDirectoryRouter(t) federate(t, a, b) - member := attach(t, a) + member := attach(t, a, "mcp://calculator") key, _ := api.ServiceKey(api.ServiceType_SERVICE_TYPE_MCP, "calculator") oldAddr := "/ip4/10.0.0.1/tcp/4001/p2p/" + member.ID().String() newAddr := "/ip4/10.0.0.2/tcp/4001/p2p/" + member.ID().String() @@ -380,3 +400,70 @@ func TestRecordStoreExpiry(t *testing.T) { t.Fatalf("sweep interval for a 6s TTL = %s, want 2s", got) } } + +// A member serves only what its credential grants: a record for a service +// its role does not allow publishing is refused and counted as denied, and +// nothing is stored; its own record under its peer key needs no grant. The +// grant is checked as a service grant is: by name, by type, or for all. +func TestDirectoryDeniesWhatTheCredentialDoesNotGrant(t *testing.T) { + a := newDirectoryRouter(t) + calc, _ := api.ServiceKey(api.ServiceType_SERVICE_TYPE_MCP, "calculator") + mcpAll, _ := api.ServiceKey(api.ServiceType_SERVICE_TYPE_MCP, "") + weather, _ := api.ServiceKey(api.ServiceType_SERVICE_TYPE_MCP, "weather") + llm, _ := api.ServiceKey(api.ServiceType_SERVICE_TYPE_INFERENCE, "llm") + calcV2, _ := api.ServiceKey(api.ServiceType_SERVICE_TYPE_MCP, "calc.v2") + + for name, tc := range map[string]struct { + publish []string + req *api.DirectoryRequest + denied bool + }{ + "no grant, a service": {publish: nil, req: provide(calc, "mcp://calculator"), denied: true}, + "no grant, its own peer key": {publish: nil, req: nil}, + "exact grant, that service": {publish: []string{"mcp://calculator"}, req: provide(calc, "mcp://calculator")}, + "exact grant, by type too": {publish: []string{"mcp://calculator"}, req: provide(mcpAll, "mcp://calculator")}, + "exact grant, another service": {publish: []string{"mcp://calculator"}, req: provide(weather, "mcp://weather"), denied: true}, + "type grant, any of the type": {publish: []string{"mcp://*"}, req: provide(weather, "mcp://weather")}, + "type grant, another type": {publish: []string{"mcp://*"}, req: provide(llm, "inference://llm"), denied: true}, + "prefix grant": {publish: []string{"mcp://calc.*"}, req: provide(calc, "mcp://calculator"), denied: true}, + "prefix grant matching": {publish: []string{"mcp://calc.*"}, req: provide(calcV2, "mcp://calc.v2")}, + "everything": {publish: []string{"*"}, req: provide(llm, "inference://llm")}, + "a set of exact grants": {publish: []string{"mcp://weather", "mcp://calculator"}, req: provide(calc, "mcp://calculator")}, + "a set of exact grants, another": {publish: []string{"mcp://weather", "mcp://clock"}, req: provide(calc, "mcp://calculator"), denied: true}, + } { + t.Run(name, func(t *testing.T) { + member := attach(t, a, tc.publish...) + req := tc.req + if req == nil { + req = provide(api.PeerKey(member.ID()), "") + } + before := directoryCount(t, directoryOpProvide, directoryResultDenied) + resp := ask(t, member, a.Host.ID(), req) + if tc.denied { + if resp.GetError() == "" { + t.Fatal("stored; want a refusal") + } + if got := directoryCount(t, directoryOpProvide, directoryResultDenied); got != before+1 { + t.Fatalf("denied counted = %v, want %v", got, before+1) + } + if provs := held(a, req.GetProvide().GetKey()); slices.ContainsFunc(provs, func(p peer.AddrInfo) bool { return p.ID == member.ID() }) { + t.Fatalf("a denied record was stored: %v", provs) + } + return + } + if resp.GetError() != "" { + t.Fatalf("refused: %s", resp.GetError()) + } + if provs := held(a, req.GetProvide().GetKey()); !slices.ContainsFunc(provs, func(p peer.AddrInfo) bool { return p.ID == member.ID() }) { + t.Fatalf("the record was not stored: %v", provs) + } + }) + } + + // A member whose credential the router no longer holds is refused too. + member := attach(t, a, "mcp://calculator") + a.credentials.Delete(member.ID()) + if resp := ask(t, member, a.Host.ID(), provide(calc, "mcp://calculator")); resp.GetError() == "" { + t.Fatal("stored with no credential held; want a refusal") + } +} diff --git a/internal/router/router.go b/internal/router/router.go index 4d610081..f1a957b5 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -32,6 +32,7 @@ import ( "sync/atomic" "time" + "github.com/eclipse-biscuit/biscuit-go/v2" "github.com/google/agentmesh/api" cpclient "github.com/google/agentmesh/internal/controlplane/client" "github.com/google/agentmesh/internal/identity" @@ -243,11 +244,18 @@ func (r *Router) admitPeer(p peer.ID, expiry time.Time) { } } +// memberCredential is a verified credential and the key that verified it. +type memberCredential struct { + biscuit *biscuit.Biscuit + key ed25519.PublicKey +} + // forgetPeer is the reverse of admitPeer, and drops the circuit addresses // recordReservation put in the peerstore: the relay drops the reservation // with the connection. func (r *Router) forgetPeer(p peer.ID) { r.authenticatedPeers.Delete(p) + r.credentials.Delete(p) if r.Host != nil { r.Host.ConnManager().UntagPeer(p, memberTag) r.Host.Peerstore().UpdateAddrs(p, r.reservationTTL(), 0) @@ -331,7 +339,10 @@ type Router struct { PubSub *pubsub.PubSub EventTopic *pubsub.Topic authenticatedPeers sync.Map - bannedPeers sync.Map + // credentials is the credential each admitted member presented, as + // memberCredential, for what the directory checks against it. + credentials sync.Map + bannedPeers sync.Map // directory is how the mesh keeps its directory, read from /info at // start (syncDirectory); records is the records this router holds. directory api.DirectorySettings @@ -1619,6 +1630,7 @@ func (r *Router) HandleAuthHandshake(s network.Stream) { } r.admitPeer(remotePeer, expiry) + r.credentials.Store(remotePeer, memberCredential{biscuit: b, key: verifyingKey}) authHandshakesTotal.WithLabelValues(handshakeOK).Inc() logger.Infof("[AuthN] Successfully authenticated peer %s", remotePeer) // A router that proves the router role is a peer router from here on, diff --git a/internal/standalone/standalone.go b/internal/standalone/standalone.go index a66d3f31..82277fca 100644 --- a/internal/standalone/standalone.go +++ b/internal/standalone/standalone.go @@ -536,7 +536,7 @@ func defaultDevPolicyRoles() []*api.PolicyRole { return []*api.PolicyRole{ {Name: "mesh-admin", AllowedServices: []string{"*"}, AllowedTargets: []string{"*"}}, {Name: api.RoleRouter, AllowedServices: []string{"*"}, AllowedTargets: []string{"*"}}, - {Name: api.RoleNode, AllowedServices: []string{"*"}, AllowedTargets: []string{"*"}, AllowedLabels: []string{"*"}}, + {Name: api.RoleNode, AllowedServices: []string{"*"}, AllowedPublish: []string{"*"}, AllowedTargets: []string{"*"}, AllowedLabels: []string{"*"}}, } } diff --git a/internal/storage/mesh_policy_test.go b/internal/storage/mesh_policy_test.go index 1b416947..0e17aacc 100644 --- a/internal/storage/mesh_policy_test.go +++ b/internal/storage/mesh_policy_test.go @@ -46,6 +46,7 @@ func TestMeshPolicyRoundTripsEveryRoleField(t *testing.T) { AllowedServices: []string{"mcp://tool"}, CustomDatalog: []string{`region("emea")`}, AllowedLabels: []string{"region=*"}, + AllowedPublish: []string{"mcp://tool", "inference://*"}, Http: []*api.HTTPGrant{{Service: "mcp://tool", Methods: []string{"GET"}, Paths: []string{"/v1/*"}}}, } diff --git a/internal/storage/sql_store.go b/internal/storage/sql_store.go index da6e3c43..71676d81 100644 --- a/internal/storage/sql_store.go +++ b/internal/storage/sql_store.go @@ -1237,6 +1237,11 @@ func (s *SQLStore) saveMeshPolicyTx(ctx context.Context, tx *sql.Tx, roles []*ap return err } } + for _, svc := range r.AllowedPublish { + if _, err := tx.ExecContext(ctx, s.rebind("INSERT INTO role_permissions (role_name, resource_type, resource_value) VALUES (?, 'publish', ?)"), r.Name, svc); err != nil { + return err + } + } // One row per narrowed entry, as protojson: the shape is the proto's // and a new HTTPGrant field needs no schema change here. for _, g := range r.Http { @@ -1312,6 +1317,8 @@ func (s *SQLStore) GetMeshPolicy(ctx context.Context) ([]*api.PolicyRole, []*api r.CustomDatalog = append(r.CustomDatalog, resValue) case "label": r.AllowedLabels = append(r.AllowedLabels, resValue) + case "publish": + r.AllowedPublish = append(r.AllowedPublish, resValue) case "http": g := &api.HTTPGrant{} if err := protojson.Unmarshal([]byte(resValue), g); err != nil { diff --git a/mobile/mobile_e2e.sh b/mobile/mobile_e2e.sh index c05f1932..bebc6feb 100755 --- a/mobile/mobile_e2e.sh +++ b/mobile/mobile_e2e.sh @@ -127,7 +127,7 @@ curl -s -X POST \ "roles": [ {"name": "admin", "allowed_services": ["*"], "allowed_targets": ["*"]}, {"name": "mesh:role:router", "allowed_services": ["*"], "allowed_targets": ["*"]}, - {"name": "mesh:role:node", "allowed_services": ["*"], "allowed_targets": ["*"]} + {"name": "mesh:role:node", "allowed_services": ["*"], "allowed_publish": ["*"], "allowed_targets": ["*"]} ], "bindings": [ {"role": "admin", "members": ["mesh:system:authenticated"]}, diff --git a/sdk/js/src/biscuit.ts b/sdk/js/src/biscuit.ts index 104dce92..793a0a94 100644 --- a/sdk/js/src/biscuit.ts +++ b/sdk/js/src/biscuit.ts @@ -83,6 +83,8 @@ export interface VerifiedBiscuit { labels: Record; /** Verified TaskAuthorizationRule chain from blocks 1..N (empty for unattenuated tokens). */ taskRules: TaskAuthorizationRule[]; + /** The token as received, for what is decided over its facts after verification (requirePublish). */ + token: Uint8Array; } function describe(err: unknown): string { @@ -219,6 +221,7 @@ export async function verifyPeerBiscuit( roles: strings(query("r($r) <- role($r)")), labels, taskRules, + token: biscuitBytes, }; } @@ -271,3 +274,32 @@ export function requireRole(verified: VerifiedBiscuit, role: string): void { } } +/** + * Requires that an already verified provider credential grants serving + * targetService ("://"), as identity.RequirePublish: the + * directory names whoever announced the service, only the credential says + * who may serve it. The provider's own catalog names no service and needs + * no grant; so does an empty target. + */ +export async function requirePublish(verified: VerifiedBiscuit, targetService: string): Promise { + if (targetService === "" || targetService === `system://${BASELINE_DATALOG.catalog_target}`) { + return; + } + const m = /^([a-z0-9]+):\/\/(.+)$/.exec(targetService); + if (m === null) { + throw new BiscuitVerificationError(`service ${JSON.stringify(targetService)} must be ://`); + } + const wasm = await loadBiscuit(); + const token = wasm.Biscuit.fromBytes(verified.token, wasm.PublicKey.fromBytes(verified.verifyingKey, wasm.SignatureAlgorithm.Ed25519)); + const builder = new wasm.AuthorizerBuilder(); + builder.addFact(wasm.Fact.fromString(`${BASELINE_DATALOG.fact_publish}(${JSON.stringify(m[1])}, ${JSON.stringify(m[2])})`)); + for (const policy of BASELINE_DATALOG.publish_policies) { + builder.addPolicy(wasm.Policy.fromString(policy)); + } + try { + withinLimits(() => builder.buildAuthenticated(token).authorizeWithLimits(AUTHORIZER_LIMITS)); + } catch { + throw new BiscuitVerificationError(`peer ${verified.peerId} is not granted serving ${targetService}`); + } +} + diff --git a/sdk/js/src/credential.ts b/sdk/js/src/credential.ts index f0e435a4..d435e566 100644 --- a/sdk/js/src/credential.ts +++ b/sdk/js/src/credential.ts @@ -21,6 +21,7 @@ import { AuthResponseSchema, MemberCredentialSchema, TaskAuthorizationRuleSchema, + type AuthFrame, type AuthResponse, type OIDCSession, } from "./gen/agentmesh_pb.ts"; @@ -120,6 +121,11 @@ export function encodeAuthFrame(biscuit: Uint8Array, targetService = ""): Uint8A } /** The peer's answer to an AuthResponse, carrying its own biscuit on success. */ +/** Reads back an AuthFrame this member built (encodeAuthFrame). */ +export function decodeAuthFrame(bytes: Uint8Array): AuthFrame { + return fromBinary(AuthFrameSchema, bytes); +} + export function decodeAuthResponse(bytes: Uint8Array): AuthResponse { return fromBinary(AuthResponseSchema, bytes); } diff --git a/sdk/js/src/gen/agentmesh_pb.ts b/sdk/js/src/gen/agentmesh_pb.ts index f5624249..24eddf51 100644 --- a/sdk/js/src/gen/agentmesh_pb.ts +++ b/sdk/js/src/gen/agentmesh_pb.ts @@ -26,7 +26,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file agentmesh.proto. */ export const file_agentmesh: GenFile = /*@__PURE__*/ - fileDesc("Cg9hZ2VudG1lc2gucHJvdG8SDGFnZW50bWVzaC52MSI0CglBdXRoRnJhbWUSDwoHYmlzY3VpdBgBIAEoDBIWCg50YXJnZXRfc2VydmljZRgCIAEoCSI/CgxBdXRoUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCBINCgVlcnJvchgCIAEoCRIPCgdiaXNjdWl0GAMgASgMIrEBCgxSb3V0ZXJHb0F3YXkSMQoGcmVhc29uGAEgASgOMiEuYWdlbnRtZXNoLnYxLlJvdXRlckdvQXdheS5SZWFzb24SLgoLcmV0cnlfYWZ0ZXIYAiABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24iPgoGUmVhc29uEhYKElJFQVNPTl9VTlNQRUNJRklFRBAAEgwKCERSQUlOSU5HEAESDgoKT1ZFUkxPQURFRBACItwBCglNZXNoRXZlbnQSKgoEdHlwZRgBIAEoDjIcLmFnZW50bWVzaC52MS5NZXNoRXZlbnQuVHlwZRIPCgdwZWVyX2lkGAIgASgJEi4KCmV2ZW50X3RpbWUYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhYKDm5ld19wdWJsaWNfa2V5GAQgASgMEhEKCXNpZ25hdHVyZRgFIAEoDCI3CgRUeXBlEgoKBkJBTk5FRBAAEhAKDEtFWV9ST1RBVElPThABEhEKDVBPTElDWV9VUERBVEUQAiL5AQoNRW5yb2xsUmVxdWVzdBILCgNqd3QYASABKAkSDwoHcGVlcl9pZBgCIAEoCRISCgpwdWJsaWNfa2V5GAMgASgMEhYKDnJlcXVlc3RlZF9yb2xlGAQgASgJEjcKBmxhYmVscxgFIAMoCzInLmFnZW50bWVzaC52MS5FbnJvbGxSZXF1ZXN0LkxhYmVsc0VudHJ5EhkKEWNoYWxsZW5nZV91bml4X21zGAYgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYByABKAwaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKrAQoORW5yb2xsUmVzcG9uc2USFQoNYmlzY3VpdF90b2tlbhgBIAEoDBIVCg1lcnJvcl9tZXNzYWdlGAIgASgJEiAKGGNvbnRyb2xfcGxhbmVfcHVibGljX2tleRgDIAEoDBIYChByb3V0ZXJfYWRkcmVzc2VzGAQgAygJEi8KC2V4cGlyZV90aW1lGAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCKXAgoWQm9vdHN0cmFwRW5yb2xsUmVxdWVzdBIXCg9ib290c3RyYXBfdG9rZW4YASABKAkSDwoHcGVlcl9pZBgCIAEoCRISCgpwdWJsaWNfa2V5GAMgASgMEhYKDnJlcXVlc3RlZF9yb2xlGAQgASgJEkAKBmxhYmVscxgFIAMoCzIwLmFnZW50bWVzaC52MS5Cb290c3RyYXBFbnJvbGxSZXF1ZXN0LkxhYmVsc0VudHJ5EhkKEWNoYWxsZW5nZV91bml4X21zGAYgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYByABKAwaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKDAgoXQm9vdHN0cmFwRW5yb2xsUmVzcG9uc2USLgoGc3RhdHVzGAEgASgOMh4uYWdlbnRtZXNoLnYxLkVucm9sbG1lbnRTdGF0dXMSFQoNYmlzY3VpdF90b2tlbhgCIAEoDBIdChVwb2xsX2ludGVydmFsX3NlY29uZHMYAyABKAUSFQoNZXJyb3JfbWVzc2FnZRgEIAEoCRIgChhjb250cm9sX3BsYW5lX3B1YmxpY19rZXkYBSABKAwSGAoQcm91dGVyX2FkZHJlc3NlcxgGIAMoCRIvCgtleHBpcmVfdGltZRgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiWQoLU2VydmljZUluZm8SJwoEdHlwZRgBIAEoDjIZLmFnZW50bWVzaC52MS5TZXJ2aWNlVHlwZRIMCgRuYW1lGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJIoEBCg5Db21tYW5kQmFja2VuZBIPCgdjb21tYW5kGAEgAygJEjIKA2VudhgCIAMoCzIlLmFnZW50bWVzaC52MS5Db21tYW5kQmFja2VuZC5FbnZFbnRyeRoqCghFbnZFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIpYBChZSZWdpc3RlclNlcnZpY2VSZXF1ZXN0EioKB3NlcnZpY2UYASABKAsyGS5hZ2VudG1lc2gudjEuU2VydmljZUluZm8SFAoKdGFyZ2V0X3VybBgCIAEoCUgAEi8KB2NvbW1hbmQYAyABKAsyHC5hZ2VudG1lc2gudjEuQ29tbWFuZEJhY2tlbmRIAEIJCgdiYWNrZW5kImkKEkRpc2NvdmVyZWRQcm92aWRlchIPCgdwZWVyX2lkGAEgASgJEhcKD2xvY2FsX3Byb3h5X3VybBgCIAEoCRIQCghzcnZfbmFtZRgDIAEoCRIXCg9zcnZfZGVzY3JpcHRpb24YBCABKAki6AEKGENvbnRyb2xQbGFuZUluZm9SZXNwb25zZRITCgtvaWRjX2lzc3VlchgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSEAoIYXVkaWVuY2UYAyABKAkSGAoQcm91dGVyX2FkZHJlc3NlcxgEIAMoCRIXCg9iYW5uZWRfcGVlcl9pZHMYBSADKAkSKQoHcm91dGVycxgGIAMoCzIYLmFnZW50bWVzaC52MS5Sb3V0ZXJJbmZvEjQKCWRpcmVjdG9yeRgHIAEoCzIhLmFnZW50bWVzaC52MS5EaXJlY3RvcnlQYXJhbWV0ZXJzIpABChNEaXJlY3RvcnlQYXJhbWV0ZXJzEhMKC3JlcGxpY2F0aW9uGAEgASgFEjUKEnJlcHJvdmlkZV9pbnRlcnZhbBgCIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbhItCgpyZWNvcmRfdHRsGAMgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uIvsBChBEaXJlY3RvcnlSZXF1ZXN0EjEKB3Byb3ZpZGUYASABKAsyHi5hZ2VudG1lc2gudjEuRGlyZWN0b3J5UHJvdmlkZUgAEj4KDmZpbmRfcHJvdmlkZXJzGAIgASgLMiQuYWdlbnRtZXNoLnYxLkRpcmVjdG9yeUZpbmRQcm92aWRlcnNIABI0CglmaW5kX3BlZXIYAyABKAsyHy5hZ2VudG1lc2gudjEuRGlyZWN0b3J5RmluZFBlZXJIABIzCgh3aXRoZHJhdxgEIAEoCzIfLmFnZW50bWVzaC52MS5EaXJlY3RvcnlXaXRoZHJhd0gAQgkKB3JlcXVlc3QiUQoQRGlyZWN0b3J5UHJvdmlkZRILCgNrZXkYASABKAwSDQoFYWRkcnMYAiADKAkSEAoIcHJvdmlkZXIYAyABKAkSDwoHc2VydmljZRgEIAEoCSIyChFEaXJlY3RvcnlXaXRoZHJhdxILCgNrZXkYASABKAwSEAoIcHJvdmlkZXIYAiABKAkiNAoWRGlyZWN0b3J5RmluZFByb3ZpZGVycxILCgNrZXkYASABKAwSDQoFbGltaXQYAiABKA0iJAoRRGlyZWN0b3J5RmluZFBlZXISDwoHcGVlcl9pZBgBIAEoCSJ3ChFEaXJlY3RvcnlSZXNwb25zZRIrCglwcm92aWRlcnMYASADKAsyGC5hZ2VudG1lc2gudjEuUGVlclJlY29yZBImCgRwZWVyGAIgASgLMhguYWdlbnRtZXNoLnYxLlBlZXJSZWNvcmQSDQoFZXJyb3IYAyABKAkiLAoKUGVlclJlY29yZBIPCgdwZWVyX2lkGAEgASgJEg0KBWFkZHJzGAIgAygJIsQBCgpSb3V0ZXJJbmZvEg8KB3BlZXJfaWQYASABKAkSEQoJYWRkcmVzc2VzGAIgAygJEjQKBmxhYmVscxgDIAMoCzIkLmFnZW50bWVzaC52MS5Sb3V0ZXJJbmZvLkxhYmVsc0VudHJ5EhMKC2Nvbm5lY3Rpb25zGAQgASgFEhgKEGNvbm5lY3Rpb25fbGltaXQYBSABKAUaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKDAgoSUm91dGVyTGVhc2VSZXF1ZXN0Eg8KB3BlZXJfaWQYASABKAkSEQoJYWRkcmVzc2VzGAIgAygJEg8KB2Jpc2N1aXQYAyABKAwSFwoPY29ubmVjdGVkX3BlZXJzGAQgAygJEhAKCGRodF9zaXplGAUgASgFEhkKEWNoYWxsZW5nZV91bml4X21zGAYgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYByABKAwSJgoDdHRsGAggASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uEhMKC2Nvbm5lY3Rpb25zGAkgASgFEhgKEGNvbm5lY3Rpb25fbGltaXQYCiABKAUiZgoTUm91dGVyTGVhc2VSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIEg0KBWVycm9yGAIgASgJEi8KC2V4cGlyZV90aW1lGAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCKkAQoKUG9saWN5Um9sZRIMCgRuYW1lGAEgASgJEhcKD2FsbG93ZWRfdGFyZ2V0cxgCIAMoCRIYChBhbGxvd2VkX3NlcnZpY2VzGAMgAygJEhYKDmN1c3RvbV9kYXRhbG9nGAQgAygJEhYKDmFsbG93ZWRfbGFiZWxzGAUgAygJEiUKBGh0dHAYBiADKAsyFy5hZ2VudG1lc2gudjEuSFRUUEdyYW50IjwKCUhUVFBHcmFudBIPCgdzZXJ2aWNlGAEgASgJEg8KB21ldGhvZHMYAiADKAkSDQoFcGF0aHMYAyADKAkioQIKEUVncmVzc0Rlc3RpbmF0aW9uEgwKBG5hbWUYASABKAkSEgoKdGFyZ2V0X3VybBgCIAEoCRISCgpjcmVkZW50aWFsGAMgASgJEhEKCXNlcnZlZF9ieRgEIAMoCRIuCgZicm9rZXIYBSABKAsyHi5hZ2VudG1lc2gudjEuQ3JlZGVudGlhbEJyb2tlchIsCgppbnNwZWN0aW9uGAYgASgLMhguYWdlbnRtZXNoLnYxLkluc3BlY3Rpb24SJgoEbW9kZRgHIAEoDjIYLmFnZW50bWVzaC52MS5FZ3Jlc3NNb2RlEg0KBXBvcnRzGAggAygNEhUKDXByZXNlcnZlX2hvc3QYCSABKAgSFwoPZm9yd2FyZF9jb250ZXh0GAogASgIIjkKCkluc3BlY3Rpb24SKwoKaW5zcGVjdG9ycxgBIAMoCzIXLmFnZW50bWVzaC52MS5JbnNwZWN0b3IibwoJSW5zcGVjdG9yEi8KC21vZGVsX2FybW9yGAEgASgLMhguYWdlbnRtZXNoLnYxLk1vZGVsQXJtb3JIABIpCghleHRfcHJvYxgCIAEoCzIVLmFnZW50bWVzaC52MS5FeHRQcm9jSABCBgoEa2luZCKRAQoKTW9kZWxBcm1vchIQCgh0ZW1wbGF0ZRgBIAEoCRIyCghyZXNwb25zZRgCIAEoDjIgLmFnZW50bWVzaC52MS5SZXNwb25zZUluc3BlY3Rpb24SEQoJZmFpbF9vcGVuGAMgASgIEioKB3RpbWVvdXQYBCABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24iiAIKB0V4dFByb2MSDgoGdGFyZ2V0GAEgASgJEgoKAmNhGAIgASgJEhoKEmNsaWVudF9jZXJ0aWZpY2F0ZRgDIAEoCRI8Cg9wcm9jZXNzaW5nX21vZGUYBCABKAsyIy5hZ2VudG1lc2gudjEuRXh0UHJvY1Byb2Nlc3NpbmdNb2RlEhsKE2FsbG93X21vZGVfb3ZlcnJpZGUYBSABKAgSMgoPbWVzc2FnZV90aW1lb3V0GAYgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uEhoKEmZhaWx1cmVfbW9kZV9hbGxvdxgHIAEoCBIaChJtYXhfYnVmZmVyZWRfYnl0ZXMYCCABKA0i/wQKFUV4dFByb2NQcm9jZXNzaW5nTW9kZRJLChNyZXF1ZXN0X2hlYWRlcl9tb2RlGAEgASgOMi4uYWdlbnRtZXNoLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZS5IZWFkZXJNb2RlEkwKFHJlc3BvbnNlX2hlYWRlcl9tb2RlGAIgASgOMi4uYWdlbnRtZXNoLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZS5IZWFkZXJNb2RlEkcKEXJlcXVlc3RfYm9keV9tb2RlGAMgASgOMiwuYWdlbnRtZXNoLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZS5Cb2R5TW9kZRJIChJyZXNwb25zZV9ib2R5X21vZGUYBCABKA4yLC5hZ2VudG1lc2gudjEuRXh0UHJvY1Byb2Nlc3NpbmdNb2RlLkJvZHlNb2RlEkwKFHJlcXVlc3RfdHJhaWxlcl9tb2RlGAUgASgOMi4uYWdlbnRtZXNoLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZS5IZWFkZXJNb2RlEk0KFXJlc3BvbnNlX3RyYWlsZXJfbW9kZRgGIAEoDjIuLmFnZW50bWVzaC52MS5FeHRQcm9jUHJvY2Vzc2luZ01vZGUuSGVhZGVyTW9kZSI5CgpIZWFkZXJNb2RlEhcKE0hFQURFUl9NT0RFX0RFRkFVTFQQABIICgRTRU5EEAESCAoEU0tJUBACImAKCEJvZHlNb2RlEggKBE5PTkUQABIMCghTVFJFQU1FRBABEgwKCEJVRkZFUkVEEAISFAoQQlVGRkVSRURfUEFSVElBTBADEhgKFEZVTExfRFVQTEVYX1NUUkVBTUVEEAQi4QEKEENyZWRlbnRpYWxCcm9rZXISFwoNc3RhdGljX3NlY3JldBgBIAEoCUgAEjcKD29pZGNfZmVkZXJhdGlvbhgCIAEoCzIcLmFnZW50bWVzaC52MS5PSURDRmVkZXJhdGlvbkgAEjYKD2F3c19hc3N1bWVfcm9sZRgDIAEoCzIbLmFnZW50bWVzaC52MS5BV1NBc3N1bWVSb2xlSAASOwoRcGxhdGZvcm1faWRlbnRpdHkYBCABKAsyHi5hZ2VudG1lc2gudjEuUGxhdGZvcm1JZGVudGl0eUgAQgYKBGtpbmQiXwoOT0lEQ0ZlZGVyYXRpb24SFgoOdG9rZW5fZW5kcG9pbnQYASABKAkSEAoIYXVkaWVuY2UYAiABKAkSEwoLaW1wZXJzb25hdGUYAyABKAkSDgoGc2NvcGVzGAQgAygJIjkKDUFXU0Fzc3VtZVJvbGUSEAoIcm9sZV9hcm4YASABKAkSFgoOc2Vzc2lvbl9wb2xpY3kYAiABKAkiIgoQUGxhdGZvcm1JZGVudGl0eRIOCgZzY29wZXMYASADKAkiLgoNUG9saWN5QmluZGluZxIMCgRyb2xlGAEgASgJEg8KB21lbWJlcnMYAiADKAkilwEKDFBvbGljeUNvbmZpZxInCgVyb2xlcxgBIAMoCzIYLmFnZW50bWVzaC52MS5Qb2xpY3lSb2xlEi0KCGJpbmRpbmdzGAIgAygLMhsuYWdlbnRtZXNoLnYxLlBvbGljeUJpbmRpbmcSLwoGZWdyZXNzGAMgAygLMh8uYWdlbnRtZXNoLnYxLkVncmVzc0Rlc3RpbmF0aW9uIhgKFlBvbGljeUNvbmZpZ0dldFJlcXVlc3QiMAoXUG9saWN5Q29uZmlnR2V0UmVzcG9uc2USFQoNZGF0YWxvZ19ydWxlcxgBIAMoCSI8ChpQb2xpY3lDb25maWdVcGRhdGVSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIEg0KBWVycm9yGAIgASgJIhoKGEVncmVzc0Fzc2lnbm1lbnRzUmVxdWVzdCJMChlFZ3Jlc3NBc3NpZ25tZW50c1Jlc3BvbnNlEi8KBmVncmVzcxgBIAMoCzIfLmFnZW50bWVzaC52MS5FZ3Jlc3NEZXN0aW5hdGlvbiJmCgxLZXlzUmVzcG9uc2USEwoLcHVibGljX2tleXMYASADKAwSLQoJc2lnbl90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBISCgpzaWduYXR1cmVzGAMgAygMImsKE1Rva2VuUmVmcmVzaFJlcXVlc3QSGwoTY2hhbGxlbmdlX3NpZ25hdHVyZRgBIAEoDBIZChFjaGFsbGVuZ2VfdW5peF9tcxgCIAEoAxIPCgdwZWVyX2lkGAMgASgJEgsKA2p3dBgEIAEoCSJ1ChRUb2tlblJlZnJlc2hSZXNwb25zZRIVCg1iaXNjdWl0X3Rva2VuGAEgASgMEi8KC2V4cGlyZV90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIVCg1lcnJvcl9tZXNzYWdlGAMgASgJIkAKEU5vZGVDYXRhbG9nUmVwb3J0EisKCHNlcnZpY2VzGAEgAygLMhkuYWdlbnRtZXNoLnYxLlNlcnZpY2VJbmZvIiUKElRva2VuUmV2b2tlUmVxdWVzdBIPCgdwZWVyX2lkGAEgASgJIjUKE1Rva2VuUmV2b2tlUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCBINCgVlcnJvchgCIAEoCSKWAQobQm9vdHN0cmFwVG9rZW5DcmVhdGVSZXF1ZXN0EgwKBHJvbGUYASABKAkSEAoIb3duZXJfaWQYAiABKAkSEQoJdHRsX2hvdXJzGAMgASgFEhIKCm1heF91c2FnZXMYBCABKAUSEwoLZGVzY3JpcHRpb24YBSABKAkSGwoTYXV0b25vbW91c19yZWNvdmVyeRgGIAEoCCKKAQocQm9vdHN0cmFwVG9rZW5DcmVhdGVSZXNwb25zZRIKCgJpZBgBIAEoCRINCgV0b2tlbhgCIAEoCRIMCgRyb2xlGAMgASgJEhAKCG93bmVyX2lkGAQgASgJEi8KC2V4cGlyZV90aW1lGAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCKrAgoOQm9vdHN0cmFwVG9rZW4SCgoCaWQYASABKAkSDAoEcm9sZRgCIAEoCRIQCghvd25lcl9pZBgDIAEoCRISCgptYXhfdXNhZ2VzGAQgASgFEhQKDHVzYWdlc19jb3VudBgFIAEoBRITCgtkZXNjcmlwdGlvbhgGIAEoCRIvCgtjcmVhdGVfdGltZRgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLwoLZXhwaXJlX3RpbWUYCCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC3Jldm9rZV90aW1lGAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIbChNhdXRvbm9tb3VzX3JlY292ZXJ5GAogASgIIkoKGkJvb3RzdHJhcFRva2VuTGlzdFJlc3BvbnNlEiwKBnRva2VucxgBIAMoCzIcLmFnZW50bWVzaC52MS5Cb290c3RyYXBUb2tlbiLWAgoRRW5yb2xsbWVudFJlcXVlc3QSCgoCaWQYASABKAkSDwoHcGVlcl9pZBgCIAEoCRIQCgh0b2tlbl9pZBgDIAEoCRIuCgZzdGF0dXMYBCABKA4yHi5hZ2VudG1lc2gudjEuRW5yb2xsbWVudFN0YXR1cxI7CgZsYWJlbHMYBSADKAsyKy5hZ2VudG1lc2gudjEuRW5yb2xsbWVudFJlcXVlc3QuTGFiZWxzRW50cnkSLwoLY3JlYXRlX3RpbWUYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjAKDHJlc29sdmVfdGltZRgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASEwoLcmVzb2x2ZWRfYnkYCCABKAkaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJSCh1FbnJvbGxtZW50UmVxdWVzdExpc3RSZXNwb25zZRIxCghyZXF1ZXN0cxgBIAMoCzIfLmFnZW50bWVzaC52MS5FbnJvbGxtZW50UmVxdWVzdCJwCgRVc2VyEgoKAmlkGAEgASgJEg4KBmlzc3VlchgCIAEoCRINCgVlbWFpbBgDIAEoCRIMCgRyb2xlGAQgASgJEi8KC2NyZWF0ZV90aW1lGAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCLjAgoMRW5yb2xsZWROb2RlEg8KB3BlZXJfaWQYASABKAkSDAoEcm9sZRgCIAEoCRIXCg9lbnJvbGxtZW50X3R5cGUYAyABKAkSEwoLY2xhaW1zX2pzb24YBCABKAkSEAoIb3duZXJfaWQYBSABKAkSNgoGbGFiZWxzGAYgAygLMiYuYWdlbnRtZXNoLnYxLkVucm9sbGVkTm9kZS5MYWJlbHNFbnRyeRIvCgtlbnJvbGxfdGltZRgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLwoLZXhwaXJlX3RpbWUYCCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEg4KBmJhbm5lZBgJIAEoCBIbChNhdXRvbm9tb3VzX3JlY292ZXJ5GAogASgIGi0KC0xhYmVsc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEixAEKC1JvdXRlckxlYXNlEg8KB3BlZXJfaWQYASABKAkSEQoJYWRkcmVzc2VzGAIgAygJEjUKEWxhc3RfcmVuZXdhbF90aW1lGAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIvCgtleHBpcmVfdGltZRgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASFwoPY29ubmVjdGVkX3BlZXJzGAUgAygJEhAKCGRodF9zaXplGAYgASgFImwKDE5vZGVTZXJ2aWNlcxIrCghzZXJ2aWNlcxgBIAMoCzIZLmFnZW50bWVzaC52MS5TZXJ2aWNlSW5mbxIvCgtyZXBvcnRfdGltZRgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAi2wMKE0FkbWluU3RhdHVzUmVzcG9uc2USIQoFdXNlcnMYASADKAsyEi5hZ2VudG1lc2gudjEuVXNlchIxCg5hY3RpdmVfcm91dGVycxgCIAMoCzIZLmFnZW50bWVzaC52MS5Sb3V0ZXJMZWFzZRIyCg5lbnJvbGxlZF9ub2RlcxgDIAMoCzIaLmFnZW50bWVzaC52MS5FbnJvbGxlZE5vZGUSPAoTZW5yb2xsbWVudF9yZXF1ZXN0cxgEIAMoCzIfLmFnZW50bWVzaC52MS5FbnJvbGxtZW50UmVxdWVzdBI2ChBib290c3RyYXBfdG9rZW5zGAUgAygLMhwuYWdlbnRtZXNoLnYxLkJvb3RzdHJhcFRva2VuEioKBnBvbGljeRgGIAEoCzIaLmFnZW50bWVzaC52MS5Qb2xpY3lDb25maWcSSAoMbm9kZV9jYXRhbG9nGAcgAygLMjIuYWdlbnRtZXNoLnYxLkFkbWluU3RhdHVzUmVzcG9uc2UuTm9kZUNhdGFsb2dFbnRyeRpOChBOb2RlQ2F0YWxvZ0VudHJ5EgsKA2tleRgBIAEoCRIpCgV2YWx1ZRgCIAEoCzIaLmFnZW50bWVzaC52MS5Ob2RlU2VydmljZXM6AjgBIoECChJVc2VyU3RhdHVzUmVzcG9uc2USIAoEdXNlchgBIAEoCzISLmFnZW50bWVzaC52MS5Vc2VyEjIKDmVucm9sbGVkX25vZGVzGAIgAygLMhouYWdlbnRtZXNoLnYxLkVucm9sbGVkTm9kZRI2ChBib290c3RyYXBfdG9rZW5zGAMgAygLMhwuYWdlbnRtZXNoLnYxLkJvb3RzdHJhcFRva2VuEjEKDmFjdGl2ZV9yb3V0ZXJzGAQgAygLMhkuYWdlbnRtZXNoLnYxLlJvdXRlckxlYXNlEioKBnBvbGljeRgFIAEoCzIaLmFnZW50bWVzaC52MS5Qb2xpY3lDb25maWci5AEKGElkZW50aXR5RXZpZGVuY2VSZXNwb25zZRIPCgdwZWVyX2lkGAEgASgJEg8KB2Jpc2N1aXQYAiABKAwSNwoTYmlzY3VpdF9leHBpcmVfdGltZRgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRY29udHJvbF9wbGFuZV91cmwYBCABKAkSIgoadHJ1c3RlZF9jb250cm9sX3BsYW5lX2tleXMYBSADKAwSLgoKY2hlY2tfdGltZRgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAixgIKFFBlZXJFdmlkZW5jZVJlc3BvbnNlEg8KB3BlZXJfaWQYASABKAkSDwoHYmlzY3VpdBgCIAEoDBIVCg12ZXJpZnlpbmdfa2V5GAMgASgMEg0KBXJvbGVzGAQgAygJEj4KBmxhYmVscxgFIAMoCzIuLmFnZW50bWVzaC52MS5QZWVyRXZpZGVuY2VSZXNwb25zZS5MYWJlbHNFbnRyeRIvCgtleHBpcmVfdGltZRgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASFgoOcmV2b2NhdGlvbl9pZHMYByADKAkSLgoKY2hlY2tfdGltZRgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKMAgoQTWVtYmVyQ3JlZGVudGlhbBIZChFjb250cm9sX3BsYW5lX3VybBgBIAEoCRIPCgdiaXNjdWl0GAIgASgMEi8KC2V4cGlyZV90aW1lGAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBI1Cgx0cnVzdGVkX2tleXMYBCADKAsyHy5hZ2VudG1lc2gudjEuVHJ1c3RlZFNpZ25pbmdLZXkSGQoRaXNzdWVkX3VuZGVyX2tleXMYBSADKAwSGAoQcm91dGVyX2FkZHJlc3NlcxgGIAMoCRIvCgxvaWRjX3Nlc3Npb24YByABKAsyGS5hZ2VudG1lc2gudjEuT0lEQ1Nlc3Npb24iWQoRVHJ1c3RlZFNpZ25pbmdLZXkSEgoKcHVibGljX2tleRgBIAEoDBIwCgxyZWNlaXZlX3RpbWUYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIlkKC09JRENTZXNzaW9uEg4KBmlzc3VlchgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSEAoIYXVkaWVuY2UYAyABKAkSFQoNcmVmcmVzaF90b2tlbhgEIAEoCSKTAQoVVGFza0F1dGhvcml6YXRpb25SdWxlEgwKBG5hbWUYASABKAkSFAoMZGlzcGxheV9uYW1lGAIgASgJEiUKBXJ1bGVzGAMgAygLMhYuYWdlbnRtZXNoLnYxLlRhc2tSdWxlEi8KC2V4cGlyZV90aW1lGAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCKEAQoIVGFza1J1bGUSEwoLZGVzY3JpcHRpb24YASABKAkSGAoQYWxsb3dlZF9zZXJ2aWNlcxgCIAMoCRIuCglvcGVyYXRpb24YAyABKAsyGy5hZ2VudG1lc2gudjEuVGFza09wZXJhdGlvbhIZChFhbGxvd2VkX3Jlc291cmNlcxgEIAMoCSJzCg1UYXNrT3BlcmF0aW9uEhUKDWFsbG93ZWRfdG9vbHMYASADKAkSFwoPYWxsb3dlZF9tZXRob2RzGAIgAygJEhUKDWFsbG93ZWRfcGF0aHMYAyADKAkSGwoTYWxsb3dlZF9wZXJtaXNzaW9ucxgEIAMoCSKrAQoUVG9rZW5FeGNoYW5nZVJlcXVlc3QSFQoNc3ViamVjdF90b2tlbhgBIAEoCRI2Cgl0YXNrX3J1bGUYAiABKAsyIy5hZ2VudG1lc2gudjEuVGFza0F1dGhvcml6YXRpb25SdWxlEgwKBHNlYWwYAyABKAgSGQoRY2hhbGxlbmdlX3VuaXhfbXMYBCABKAMSGwoTY2hhbGxlbmdlX3NpZ25hdHVyZRgFIAEoDCJ/ChVUb2tlbkV4Y2hhbmdlUmVzcG9uc2USFQoNYmlzY3VpdF90b2tlbhgBIAEoDBIvCgtleHBpcmVfdGltZRgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASDQoFcm9sZXMYAyADKAkSDwoHc3ViamVjdBgEIAEoCSKBAQoPU1RTVG9rZW5SZXF1ZXN0Eg8KB2Jpc2N1aXQYASABKAwSEwoLZGVzdGluYXRpb24YAiABKAkSEAoIYXVkaWVuY2UYAyABKAkSGQoRY2hhbGxlbmdlX3VuaXhfbXMYBCABKAMSGwoTY2hhbGxlbmdlX3NpZ25hdHVyZRgFIAEoDCKDAQoQU1RTVG9rZW5SZXNwb25zZRILCgNqd3QYASABKAkSLwoLZXhwaXJlX3RpbWUYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEg8KB3N1YmplY3QYAyABKAkSDQoFcm9sZXMYBCADKAkSEQoJdGFza19uYW1lGAUgASgJIkYKE1Jldm9jYXRpb25zUmVzcG9uc2USFgoOcmV2b2NhdGlvbl9pZHMYASADKAkSFwoPYmFubmVkX3BlZXJfaWRzGAIgAygJKpQBChBFbnJvbGxtZW50U3RhdHVzEiEKHUVOUk9MTE1FTlRfU1RBVFVTX1VOU1BFQ0lGSUVEEAASHQoZRU5ST0xMTUVOVF9TVEFUVVNfUEVORElORxABEh4KGkVOUk9MTE1FTlRfU1RBVFVTX0FQUFJPVkVEEAISHgoaRU5ST0xMTUVOVF9TVEFUVVNfUkVKRUNURUQQAyqMAQoLU2VydmljZVR5cGUSHAoYU0VSVklDRV9UWVBFX1VOU1BFQ0lGSUVEEAASFAoQU0VSVklDRV9UWVBFX01DUBABEhoKFlNFUlZJQ0VfVFlQRV9JTkZFUkVOQ0UQAhIUChBTRVJWSUNFX1RZUEVfQTJBEAMSFwoTU0VSVklDRV9UWVBFX0VHUkVTUxAEKjcKCkVncmVzc01vZGUSFAoQRUdSRVNTX01PREVfSFRUUBAAEhMKD0VHUkVTU19NT0RFX1RDUBABKlwKElJlc3BvbnNlSW5zcGVjdGlvbhIgChxSRVNQT05TRV9JTlNQRUNUSU9OX0JVRkZFUkVEEAASJAogUkVTUE9OU0VfSU5TUEVDVElPTl9SRVFVRVNUX09OTFkQAUIhWh9naXRodWIuY29tL2dvb2dsZS9hZ2VudG1lc2gvYXBpYgZwcm90bzM", [file_google_protobuf_duration, file_google_protobuf_timestamp]); + fileDesc("Cg9hZ2VudG1lc2gucHJvdG8SDGFnZW50bWVzaC52MSI0CglBdXRoRnJhbWUSDwoHYmlzY3VpdBgBIAEoDBIWCg50YXJnZXRfc2VydmljZRgCIAEoCSI/CgxBdXRoUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCBINCgVlcnJvchgCIAEoCRIPCgdiaXNjdWl0GAMgASgMIrEBCgxSb3V0ZXJHb0F3YXkSMQoGcmVhc29uGAEgASgOMiEuYWdlbnRtZXNoLnYxLlJvdXRlckdvQXdheS5SZWFzb24SLgoLcmV0cnlfYWZ0ZXIYAiABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24iPgoGUmVhc29uEhYKElJFQVNPTl9VTlNQRUNJRklFRBAAEgwKCERSQUlOSU5HEAESDgoKT1ZFUkxPQURFRBACItwBCglNZXNoRXZlbnQSKgoEdHlwZRgBIAEoDjIcLmFnZW50bWVzaC52MS5NZXNoRXZlbnQuVHlwZRIPCgdwZWVyX2lkGAIgASgJEi4KCmV2ZW50X3RpbWUYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhYKDm5ld19wdWJsaWNfa2V5GAQgASgMEhEKCXNpZ25hdHVyZRgFIAEoDCI3CgRUeXBlEgoKBkJBTk5FRBAAEhAKDEtFWV9ST1RBVElPThABEhEKDVBPTElDWV9VUERBVEUQAiL5AQoNRW5yb2xsUmVxdWVzdBILCgNqd3QYASABKAkSDwoHcGVlcl9pZBgCIAEoCRISCgpwdWJsaWNfa2V5GAMgASgMEhYKDnJlcXVlc3RlZF9yb2xlGAQgASgJEjcKBmxhYmVscxgFIAMoCzInLmFnZW50bWVzaC52MS5FbnJvbGxSZXF1ZXN0LkxhYmVsc0VudHJ5EhkKEWNoYWxsZW5nZV91bml4X21zGAYgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYByABKAwaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKrAQoORW5yb2xsUmVzcG9uc2USFQoNYmlzY3VpdF90b2tlbhgBIAEoDBIVCg1lcnJvcl9tZXNzYWdlGAIgASgJEiAKGGNvbnRyb2xfcGxhbmVfcHVibGljX2tleRgDIAEoDBIYChByb3V0ZXJfYWRkcmVzc2VzGAQgAygJEi8KC2V4cGlyZV90aW1lGAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCKXAgoWQm9vdHN0cmFwRW5yb2xsUmVxdWVzdBIXCg9ib290c3RyYXBfdG9rZW4YASABKAkSDwoHcGVlcl9pZBgCIAEoCRISCgpwdWJsaWNfa2V5GAMgASgMEhYKDnJlcXVlc3RlZF9yb2xlGAQgASgJEkAKBmxhYmVscxgFIAMoCzIwLmFnZW50bWVzaC52MS5Cb290c3RyYXBFbnJvbGxSZXF1ZXN0LkxhYmVsc0VudHJ5EhkKEWNoYWxsZW5nZV91bml4X21zGAYgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYByABKAwaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKDAgoXQm9vdHN0cmFwRW5yb2xsUmVzcG9uc2USLgoGc3RhdHVzGAEgASgOMh4uYWdlbnRtZXNoLnYxLkVucm9sbG1lbnRTdGF0dXMSFQoNYmlzY3VpdF90b2tlbhgCIAEoDBIdChVwb2xsX2ludGVydmFsX3NlY29uZHMYAyABKAUSFQoNZXJyb3JfbWVzc2FnZRgEIAEoCRIgChhjb250cm9sX3BsYW5lX3B1YmxpY19rZXkYBSABKAwSGAoQcm91dGVyX2FkZHJlc3NlcxgGIAMoCRIvCgtleHBpcmVfdGltZRgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiWQoLU2VydmljZUluZm8SJwoEdHlwZRgBIAEoDjIZLmFnZW50bWVzaC52MS5TZXJ2aWNlVHlwZRIMCgRuYW1lGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJIoEBCg5Db21tYW5kQmFja2VuZBIPCgdjb21tYW5kGAEgAygJEjIKA2VudhgCIAMoCzIlLmFnZW50bWVzaC52MS5Db21tYW5kQmFja2VuZC5FbnZFbnRyeRoqCghFbnZFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIpYBChZSZWdpc3RlclNlcnZpY2VSZXF1ZXN0EioKB3NlcnZpY2UYASABKAsyGS5hZ2VudG1lc2gudjEuU2VydmljZUluZm8SFAoKdGFyZ2V0X3VybBgCIAEoCUgAEi8KB2NvbW1hbmQYAyABKAsyHC5hZ2VudG1lc2gudjEuQ29tbWFuZEJhY2tlbmRIAEIJCgdiYWNrZW5kImkKEkRpc2NvdmVyZWRQcm92aWRlchIPCgdwZWVyX2lkGAEgASgJEhcKD2xvY2FsX3Byb3h5X3VybBgCIAEoCRIQCghzcnZfbmFtZRgDIAEoCRIXCg9zcnZfZGVzY3JpcHRpb24YBCABKAki6AEKGENvbnRyb2xQbGFuZUluZm9SZXNwb25zZRITCgtvaWRjX2lzc3VlchgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSEAoIYXVkaWVuY2UYAyABKAkSGAoQcm91dGVyX2FkZHJlc3NlcxgEIAMoCRIXCg9iYW5uZWRfcGVlcl9pZHMYBSADKAkSKQoHcm91dGVycxgGIAMoCzIYLmFnZW50bWVzaC52MS5Sb3V0ZXJJbmZvEjQKCWRpcmVjdG9yeRgHIAEoCzIhLmFnZW50bWVzaC52MS5EaXJlY3RvcnlQYXJhbWV0ZXJzIpABChNEaXJlY3RvcnlQYXJhbWV0ZXJzEhMKC3JlcGxpY2F0aW9uGAEgASgFEjUKEnJlcHJvdmlkZV9pbnRlcnZhbBgCIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbhItCgpyZWNvcmRfdHRsGAMgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uIvsBChBEaXJlY3RvcnlSZXF1ZXN0EjEKB3Byb3ZpZGUYASABKAsyHi5hZ2VudG1lc2gudjEuRGlyZWN0b3J5UHJvdmlkZUgAEj4KDmZpbmRfcHJvdmlkZXJzGAIgASgLMiQuYWdlbnRtZXNoLnYxLkRpcmVjdG9yeUZpbmRQcm92aWRlcnNIABI0CglmaW5kX3BlZXIYAyABKAsyHy5hZ2VudG1lc2gudjEuRGlyZWN0b3J5RmluZFBlZXJIABIzCgh3aXRoZHJhdxgEIAEoCzIfLmFnZW50bWVzaC52MS5EaXJlY3RvcnlXaXRoZHJhd0gAQgkKB3JlcXVlc3QiUQoQRGlyZWN0b3J5UHJvdmlkZRILCgNrZXkYASABKAwSDQoFYWRkcnMYAiADKAkSEAoIcHJvdmlkZXIYAyABKAkSDwoHc2VydmljZRgEIAEoCSIyChFEaXJlY3RvcnlXaXRoZHJhdxILCgNrZXkYASABKAwSEAoIcHJvdmlkZXIYAiABKAkiNAoWRGlyZWN0b3J5RmluZFByb3ZpZGVycxILCgNrZXkYASABKAwSDQoFbGltaXQYAiABKA0iJAoRRGlyZWN0b3J5RmluZFBlZXISDwoHcGVlcl9pZBgBIAEoCSJ3ChFEaXJlY3RvcnlSZXNwb25zZRIrCglwcm92aWRlcnMYASADKAsyGC5hZ2VudG1lc2gudjEuUGVlclJlY29yZBImCgRwZWVyGAIgASgLMhguYWdlbnRtZXNoLnYxLlBlZXJSZWNvcmQSDQoFZXJyb3IYAyABKAkiLAoKUGVlclJlY29yZBIPCgdwZWVyX2lkGAEgASgJEg0KBWFkZHJzGAIgAygJIsQBCgpSb3V0ZXJJbmZvEg8KB3BlZXJfaWQYASABKAkSEQoJYWRkcmVzc2VzGAIgAygJEjQKBmxhYmVscxgDIAMoCzIkLmFnZW50bWVzaC52MS5Sb3V0ZXJJbmZvLkxhYmVsc0VudHJ5EhMKC2Nvbm5lY3Rpb25zGAQgASgFEhgKEGNvbm5lY3Rpb25fbGltaXQYBSABKAUaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKDAgoSUm91dGVyTGVhc2VSZXF1ZXN0Eg8KB3BlZXJfaWQYASABKAkSEQoJYWRkcmVzc2VzGAIgAygJEg8KB2Jpc2N1aXQYAyABKAwSFwoPY29ubmVjdGVkX3BlZXJzGAQgAygJEhAKCGRodF9zaXplGAUgASgFEhkKEWNoYWxsZW5nZV91bml4X21zGAYgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYByABKAwSJgoDdHRsGAggASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uEhMKC2Nvbm5lY3Rpb25zGAkgASgFEhgKEGNvbm5lY3Rpb25fbGltaXQYCiABKAUiZgoTUm91dGVyTGVhc2VSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIEg0KBWVycm9yGAIgASgJEi8KC2V4cGlyZV90aW1lGAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCK9AQoKUG9saWN5Um9sZRIMCgRuYW1lGAEgASgJEhcKD2FsbG93ZWRfdGFyZ2V0cxgCIAMoCRIYChBhbGxvd2VkX3NlcnZpY2VzGAMgAygJEhYKDmN1c3RvbV9kYXRhbG9nGAQgAygJEhYKDmFsbG93ZWRfbGFiZWxzGAUgAygJEiUKBGh0dHAYBiADKAsyFy5hZ2VudG1lc2gudjEuSFRUUEdyYW50EhcKD2FsbG93ZWRfcHVibGlzaBgHIAMoCSI8CglIVFRQR3JhbnQSDwoHc2VydmljZRgBIAEoCRIPCgdtZXRob2RzGAIgAygJEg0KBXBhdGhzGAMgAygJIqECChFFZ3Jlc3NEZXN0aW5hdGlvbhIMCgRuYW1lGAEgASgJEhIKCnRhcmdldF91cmwYAiABKAkSEgoKY3JlZGVudGlhbBgDIAEoCRIRCglzZXJ2ZWRfYnkYBCADKAkSLgoGYnJva2VyGAUgASgLMh4uYWdlbnRtZXNoLnYxLkNyZWRlbnRpYWxCcm9rZXISLAoKaW5zcGVjdGlvbhgGIAEoCzIYLmFnZW50bWVzaC52MS5JbnNwZWN0aW9uEiYKBG1vZGUYByABKA4yGC5hZ2VudG1lc2gudjEuRWdyZXNzTW9kZRINCgVwb3J0cxgIIAMoDRIVCg1wcmVzZXJ2ZV9ob3N0GAkgASgIEhcKD2ZvcndhcmRfY29udGV4dBgKIAEoCCI5CgpJbnNwZWN0aW9uEisKCmluc3BlY3RvcnMYASADKAsyFy5hZ2VudG1lc2gudjEuSW5zcGVjdG9yIm8KCUluc3BlY3RvchIvCgttb2RlbF9hcm1vchgBIAEoCzIYLmFnZW50bWVzaC52MS5Nb2RlbEFybW9ySAASKQoIZXh0X3Byb2MYAiABKAsyFS5hZ2VudG1lc2gudjEuRXh0UHJvY0gAQgYKBGtpbmQikQEKCk1vZGVsQXJtb3ISEAoIdGVtcGxhdGUYASABKAkSMgoIcmVzcG9uc2UYAiABKA4yIC5hZ2VudG1lc2gudjEuUmVzcG9uc2VJbnNwZWN0aW9uEhEKCWZhaWxfb3BlbhgDIAEoCBIqCgd0aW1lb3V0GAQgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uIogCCgdFeHRQcm9jEg4KBnRhcmdldBgBIAEoCRIKCgJjYRgCIAEoCRIaChJjbGllbnRfY2VydGlmaWNhdGUYAyABKAkSPAoPcHJvY2Vzc2luZ19tb2RlGAQgASgLMiMuYWdlbnRtZXNoLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZRIbChNhbGxvd19tb2RlX292ZXJyaWRlGAUgASgIEjIKD21lc3NhZ2VfdGltZW91dBgGIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbhIaChJmYWlsdXJlX21vZGVfYWxsb3cYByABKAgSGgoSbWF4X2J1ZmZlcmVkX2J5dGVzGAggASgNIv8EChVFeHRQcm9jUHJvY2Vzc2luZ01vZGUSSwoTcmVxdWVzdF9oZWFkZXJfbW9kZRgBIAEoDjIuLmFnZW50bWVzaC52MS5FeHRQcm9jUHJvY2Vzc2luZ01vZGUuSGVhZGVyTW9kZRJMChRyZXNwb25zZV9oZWFkZXJfbW9kZRgCIAEoDjIuLmFnZW50bWVzaC52MS5FeHRQcm9jUHJvY2Vzc2luZ01vZGUuSGVhZGVyTW9kZRJHChFyZXF1ZXN0X2JvZHlfbW9kZRgDIAEoDjIsLmFnZW50bWVzaC52MS5FeHRQcm9jUHJvY2Vzc2luZ01vZGUuQm9keU1vZGUSSAoScmVzcG9uc2VfYm9keV9tb2RlGAQgASgOMiwuYWdlbnRtZXNoLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZS5Cb2R5TW9kZRJMChRyZXF1ZXN0X3RyYWlsZXJfbW9kZRgFIAEoDjIuLmFnZW50bWVzaC52MS5FeHRQcm9jUHJvY2Vzc2luZ01vZGUuSGVhZGVyTW9kZRJNChVyZXNwb25zZV90cmFpbGVyX21vZGUYBiABKA4yLi5hZ2VudG1lc2gudjEuRXh0UHJvY1Byb2Nlc3NpbmdNb2RlLkhlYWRlck1vZGUiOQoKSGVhZGVyTW9kZRIXChNIRUFERVJfTU9ERV9ERUZBVUxUEAASCAoEU0VORBABEggKBFNLSVAQAiJgCghCb2R5TW9kZRIICgROT05FEAASDAoIU1RSRUFNRUQQARIMCghCVUZGRVJFRBACEhQKEEJVRkZFUkVEX1BBUlRJQUwQAxIYChRGVUxMX0RVUExFWF9TVFJFQU1FRBAEIuEBChBDcmVkZW50aWFsQnJva2VyEhcKDXN0YXRpY19zZWNyZXQYASABKAlIABI3Cg9vaWRjX2ZlZGVyYXRpb24YAiABKAsyHC5hZ2VudG1lc2gudjEuT0lEQ0ZlZGVyYXRpb25IABI2Cg9hd3NfYXNzdW1lX3JvbGUYAyABKAsyGy5hZ2VudG1lc2gudjEuQVdTQXNzdW1lUm9sZUgAEjsKEXBsYXRmb3JtX2lkZW50aXR5GAQgASgLMh4uYWdlbnRtZXNoLnYxLlBsYXRmb3JtSWRlbnRpdHlIAEIGCgRraW5kIl8KDk9JRENGZWRlcmF0aW9uEhYKDnRva2VuX2VuZHBvaW50GAEgASgJEhAKCGF1ZGllbmNlGAIgASgJEhMKC2ltcGVyc29uYXRlGAMgASgJEg4KBnNjb3BlcxgEIAMoCSI5Cg1BV1NBc3N1bWVSb2xlEhAKCHJvbGVfYXJuGAEgASgJEhYKDnNlc3Npb25fcG9saWN5GAIgASgJIiIKEFBsYXRmb3JtSWRlbnRpdHkSDgoGc2NvcGVzGAEgAygJIi4KDVBvbGljeUJpbmRpbmcSDAoEcm9sZRgBIAEoCRIPCgdtZW1iZXJzGAIgAygJIpcBCgxQb2xpY3lDb25maWcSJwoFcm9sZXMYASADKAsyGC5hZ2VudG1lc2gudjEuUG9saWN5Um9sZRItCghiaW5kaW5ncxgCIAMoCzIbLmFnZW50bWVzaC52MS5Qb2xpY3lCaW5kaW5nEi8KBmVncmVzcxgDIAMoCzIfLmFnZW50bWVzaC52MS5FZ3Jlc3NEZXN0aW5hdGlvbiIYChZQb2xpY3lDb25maWdHZXRSZXF1ZXN0IjAKF1BvbGljeUNvbmZpZ0dldFJlc3BvbnNlEhUKDWRhdGFsb2dfcnVsZXMYASADKAkiPAoaUG9saWN5Q29uZmlnVXBkYXRlUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCBINCgVlcnJvchgCIAEoCSIaChhFZ3Jlc3NBc3NpZ25tZW50c1JlcXVlc3QiTAoZRWdyZXNzQXNzaWdubWVudHNSZXNwb25zZRIvCgZlZ3Jlc3MYASADKAsyHy5hZ2VudG1lc2gudjEuRWdyZXNzRGVzdGluYXRpb24iZgoMS2V5c1Jlc3BvbnNlEhMKC3B1YmxpY19rZXlzGAEgAygMEi0KCXNpZ25fdGltZRgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASEgoKc2lnbmF0dXJlcxgDIAMoDCJrChNUb2tlblJlZnJlc2hSZXF1ZXN0EhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYASABKAwSGQoRY2hhbGxlbmdlX3VuaXhfbXMYAiABKAMSDwoHcGVlcl9pZBgDIAEoCRILCgNqd3QYBCABKAkidQoUVG9rZW5SZWZyZXNoUmVzcG9uc2USFQoNYmlzY3VpdF90b2tlbhgBIAEoDBIvCgtleHBpcmVfdGltZRgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASFQoNZXJyb3JfbWVzc2FnZRgDIAEoCSJAChFOb2RlQ2F0YWxvZ1JlcG9ydBIrCghzZXJ2aWNlcxgBIAMoCzIZLmFnZW50bWVzaC52MS5TZXJ2aWNlSW5mbyIlChJUb2tlblJldm9rZVJlcXVlc3QSDwoHcGVlcl9pZBgBIAEoCSI1ChNUb2tlblJldm9rZVJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgSDQoFZXJyb3IYAiABKAkilgEKG0Jvb3RzdHJhcFRva2VuQ3JlYXRlUmVxdWVzdBIMCgRyb2xlGAEgASgJEhAKCG93bmVyX2lkGAIgASgJEhEKCXR0bF9ob3VycxgDIAEoBRISCgptYXhfdXNhZ2VzGAQgASgFEhMKC2Rlc2NyaXB0aW9uGAUgASgJEhsKE2F1dG9ub21vdXNfcmVjb3ZlcnkYBiABKAgiigEKHEJvb3RzdHJhcFRva2VuQ3JlYXRlUmVzcG9uc2USCgoCaWQYASABKAkSDQoFdG9rZW4YAiABKAkSDAoEcm9sZRgDIAEoCRIQCghvd25lcl9pZBgEIAEoCRIvCgtleHBpcmVfdGltZRgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiqwIKDkJvb3RzdHJhcFRva2VuEgoKAmlkGAEgASgJEgwKBHJvbGUYAiABKAkSEAoIb3duZXJfaWQYAyABKAkSEgoKbWF4X3VzYWdlcxgEIAEoBRIUCgx1c2FnZXNfY291bnQYBSABKAUSEwoLZGVzY3JpcHRpb24YBiABKAkSLwoLY3JlYXRlX3RpbWUYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2V4cGlyZV90aW1lGAggASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIvCgtyZXZva2VfdGltZRgJIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGwoTYXV0b25vbW91c19yZWNvdmVyeRgKIAEoCCJKChpCb290c3RyYXBUb2tlbkxpc3RSZXNwb25zZRIsCgZ0b2tlbnMYASADKAsyHC5hZ2VudG1lc2gudjEuQm9vdHN0cmFwVG9rZW4i1gIKEUVucm9sbG1lbnRSZXF1ZXN0EgoKAmlkGAEgASgJEg8KB3BlZXJfaWQYAiABKAkSEAoIdG9rZW5faWQYAyABKAkSLgoGc3RhdHVzGAQgASgOMh4uYWdlbnRtZXNoLnYxLkVucm9sbG1lbnRTdGF0dXMSOwoGbGFiZWxzGAUgAygLMisuYWdlbnRtZXNoLnYxLkVucm9sbG1lbnRSZXF1ZXN0LkxhYmVsc0VudHJ5Ei8KC2NyZWF0ZV90aW1lGAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIwCgxyZXNvbHZlX3RpbWUYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhMKC3Jlc29sdmVkX2J5GAggASgJGi0KC0xhYmVsc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiUgodRW5yb2xsbWVudFJlcXVlc3RMaXN0UmVzcG9uc2USMQoIcmVxdWVzdHMYASADKAsyHy5hZ2VudG1lc2gudjEuRW5yb2xsbWVudFJlcXVlc3QicAoEVXNlchIKCgJpZBgBIAEoCRIOCgZpc3N1ZXIYAiABKAkSDQoFZW1haWwYAyABKAkSDAoEcm9sZRgEIAEoCRIvCgtjcmVhdGVfdGltZRgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAi4wIKDEVucm9sbGVkTm9kZRIPCgdwZWVyX2lkGAEgASgJEgwKBHJvbGUYAiABKAkSFwoPZW5yb2xsbWVudF90eXBlGAMgASgJEhMKC2NsYWltc19qc29uGAQgASgJEhAKCG93bmVyX2lkGAUgASgJEjYKBmxhYmVscxgGIAMoCzImLmFnZW50bWVzaC52MS5FbnJvbGxlZE5vZGUuTGFiZWxzRW50cnkSLwoLZW5yb2xsX3RpbWUYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC2V4cGlyZV90aW1lGAggASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIOCgZiYW5uZWQYCSABKAgSGwoTYXV0b25vbW91c19yZWNvdmVyeRgKIAEoCBotCgtMYWJlbHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIsQBCgtSb3V0ZXJMZWFzZRIPCgdwZWVyX2lkGAEgASgJEhEKCWFkZHJlc3NlcxgCIAMoCRI1ChFsYXN0X3JlbmV3YWxfdGltZRgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLwoLZXhwaXJlX3RpbWUYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhcKD2Nvbm5lY3RlZF9wZWVycxgFIAMoCRIQCghkaHRfc2l6ZRgGIAEoBSJsCgxOb2RlU2VydmljZXMSKwoIc2VydmljZXMYASADKAsyGS5hZ2VudG1lc2gudjEuU2VydmljZUluZm8SLwoLcmVwb3J0X3RpbWUYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wItsDChNBZG1pblN0YXR1c1Jlc3BvbnNlEiEKBXVzZXJzGAEgAygLMhIuYWdlbnRtZXNoLnYxLlVzZXISMQoOYWN0aXZlX3JvdXRlcnMYAiADKAsyGS5hZ2VudG1lc2gudjEuUm91dGVyTGVhc2USMgoOZW5yb2xsZWRfbm9kZXMYAyADKAsyGi5hZ2VudG1lc2gudjEuRW5yb2xsZWROb2RlEjwKE2Vucm9sbG1lbnRfcmVxdWVzdHMYBCADKAsyHy5hZ2VudG1lc2gudjEuRW5yb2xsbWVudFJlcXVlc3QSNgoQYm9vdHN0cmFwX3Rva2VucxgFIAMoCzIcLmFnZW50bWVzaC52MS5Cb290c3RyYXBUb2tlbhIqCgZwb2xpY3kYBiABKAsyGi5hZ2VudG1lc2gudjEuUG9saWN5Q29uZmlnEkgKDG5vZGVfY2F0YWxvZxgHIAMoCzIyLmFnZW50bWVzaC52MS5BZG1pblN0YXR1c1Jlc3BvbnNlLk5vZGVDYXRhbG9nRW50cnkaTgoQTm9kZUNhdGFsb2dFbnRyeRILCgNrZXkYASABKAkSKQoFdmFsdWUYAiABKAsyGi5hZ2VudG1lc2gudjEuTm9kZVNlcnZpY2VzOgI4ASKBAgoSVXNlclN0YXR1c1Jlc3BvbnNlEiAKBHVzZXIYASABKAsyEi5hZ2VudG1lc2gudjEuVXNlchIyCg5lbnJvbGxlZF9ub2RlcxgCIAMoCzIaLmFnZW50bWVzaC52MS5FbnJvbGxlZE5vZGUSNgoQYm9vdHN0cmFwX3Rva2VucxgDIAMoCzIcLmFnZW50bWVzaC52MS5Cb290c3RyYXBUb2tlbhIxCg5hY3RpdmVfcm91dGVycxgEIAMoCzIZLmFnZW50bWVzaC52MS5Sb3V0ZXJMZWFzZRIqCgZwb2xpY3kYBSABKAsyGi5hZ2VudG1lc2gudjEuUG9saWN5Q29uZmlnIuQBChhJZGVudGl0eUV2aWRlbmNlUmVzcG9uc2USDwoHcGVlcl9pZBgBIAEoCRIPCgdiaXNjdWl0GAIgASgMEjcKE2Jpc2N1aXRfZXhwaXJlX3RpbWUYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhkKEWNvbnRyb2xfcGxhbmVfdXJsGAQgASgJEiIKGnRydXN0ZWRfY29udHJvbF9wbGFuZV9rZXlzGAUgAygMEi4KCmNoZWNrX3RpbWUYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIsYCChRQZWVyRXZpZGVuY2VSZXNwb25zZRIPCgdwZWVyX2lkGAEgASgJEg8KB2Jpc2N1aXQYAiABKAwSFQoNdmVyaWZ5aW5nX2tleRgDIAEoDBINCgVyb2xlcxgEIAMoCRI+CgZsYWJlbHMYBSADKAsyLi5hZ2VudG1lc2gudjEuUGVlckV2aWRlbmNlUmVzcG9uc2UuTGFiZWxzRW50cnkSLwoLZXhwaXJlX3RpbWUYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhYKDnJldm9jYXRpb25faWRzGAcgAygJEi4KCmNoZWNrX3RpbWUYCCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wGi0KC0xhYmVsc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEijAIKEE1lbWJlckNyZWRlbnRpYWwSGQoRY29udHJvbF9wbGFuZV91cmwYASABKAkSDwoHYmlzY3VpdBgCIAEoDBIvCgtleHBpcmVfdGltZRgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASNQoMdHJ1c3RlZF9rZXlzGAQgAygLMh8uYWdlbnRtZXNoLnYxLlRydXN0ZWRTaWduaW5nS2V5EhkKEWlzc3VlZF91bmRlcl9rZXlzGAUgAygMEhgKEHJvdXRlcl9hZGRyZXNzZXMYBiADKAkSLwoMb2lkY19zZXNzaW9uGAcgASgLMhkuYWdlbnRtZXNoLnYxLk9JRENTZXNzaW9uIlkKEVRydXN0ZWRTaWduaW5nS2V5EhIKCnB1YmxpY19rZXkYASABKAwSMAoMcmVjZWl2ZV90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJZCgtPSURDU2Vzc2lvbhIOCgZpc3N1ZXIYASABKAkSEQoJY2xpZW50X2lkGAIgASgJEhAKCGF1ZGllbmNlGAMgASgJEhUKDXJlZnJlc2hfdG9rZW4YBCABKAkikwEKFVRhc2tBdXRob3JpemF0aW9uUnVsZRIMCgRuYW1lGAEgASgJEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRIlCgVydWxlcxgDIAMoCzIWLmFnZW50bWVzaC52MS5UYXNrUnVsZRIvCgtleHBpcmVfdGltZRgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAihAEKCFRhc2tSdWxlEhMKC2Rlc2NyaXB0aW9uGAEgASgJEhgKEGFsbG93ZWRfc2VydmljZXMYAiADKAkSLgoJb3BlcmF0aW9uGAMgASgLMhsuYWdlbnRtZXNoLnYxLlRhc2tPcGVyYXRpb24SGQoRYWxsb3dlZF9yZXNvdXJjZXMYBCADKAkicwoNVGFza09wZXJhdGlvbhIVCg1hbGxvd2VkX3Rvb2xzGAEgAygJEhcKD2FsbG93ZWRfbWV0aG9kcxgCIAMoCRIVCg1hbGxvd2VkX3BhdGhzGAMgAygJEhsKE2FsbG93ZWRfcGVybWlzc2lvbnMYBCADKAkiqwEKFFRva2VuRXhjaGFuZ2VSZXF1ZXN0EhUKDXN1YmplY3RfdG9rZW4YASABKAkSNgoJdGFza19ydWxlGAIgASgLMiMuYWdlbnRtZXNoLnYxLlRhc2tBdXRob3JpemF0aW9uUnVsZRIMCgRzZWFsGAMgASgIEhkKEWNoYWxsZW5nZV91bml4X21zGAQgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYBSABKAwifwoVVG9rZW5FeGNoYW5nZVJlc3BvbnNlEhUKDWJpc2N1aXRfdG9rZW4YASABKAwSLwoLZXhwaXJlX3RpbWUYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEg0KBXJvbGVzGAMgAygJEg8KB3N1YmplY3QYBCABKAkigQEKD1NUU1Rva2VuUmVxdWVzdBIPCgdiaXNjdWl0GAEgASgMEhMKC2Rlc3RpbmF0aW9uGAIgASgJEhAKCGF1ZGllbmNlGAMgASgJEhkKEWNoYWxsZW5nZV91bml4X21zGAQgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYBSABKAwigwEKEFNUU1Rva2VuUmVzcG9uc2USCwoDand0GAEgASgJEi8KC2V4cGlyZV90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIPCgdzdWJqZWN0GAMgASgJEg0KBXJvbGVzGAQgAygJEhEKCXRhc2tfbmFtZRgFIAEoCSJGChNSZXZvY2F0aW9uc1Jlc3BvbnNlEhYKDnJldm9jYXRpb25faWRzGAEgAygJEhcKD2Jhbm5lZF9wZWVyX2lkcxgCIAMoCSqUAQoQRW5yb2xsbWVudFN0YXR1cxIhCh1FTlJPTExNRU5UX1NUQVRVU19VTlNQRUNJRklFRBAAEh0KGUVOUk9MTE1FTlRfU1RBVFVTX1BFTkRJTkcQARIeChpFTlJPTExNRU5UX1NUQVRVU19BUFBST1ZFRBACEh4KGkVOUk9MTE1FTlRfU1RBVFVTX1JFSkVDVEVEEAMqjAEKC1NlcnZpY2VUeXBlEhwKGFNFUlZJQ0VfVFlQRV9VTlNQRUNJRklFRBAAEhQKEFNFUlZJQ0VfVFlQRV9NQ1AQARIaChZTRVJWSUNFX1RZUEVfSU5GRVJFTkNFEAISFAoQU0VSVklDRV9UWVBFX0EyQRADEhcKE1NFUlZJQ0VfVFlQRV9FR1JFU1MQBCo3CgpFZ3Jlc3NNb2RlEhQKEEVHUkVTU19NT0RFX0hUVFAQABITCg9FR1JFU1NfTU9ERV9UQ1AQASpcChJSZXNwb25zZUluc3BlY3Rpb24SIAocUkVTUE9OU0VfSU5TUEVDVElPTl9CVUZGRVJFRBAAEiQKIFJFU1BPTlNFX0lOU1BFQ1RJT05fUkVRVUVTVF9PTkxZEAFCIVofZ2l0aHViLmNvbS9nb29nbGUvYWdlbnRtZXNoL2FwaWIGcHJvdG8z", [file_google_protobuf_duration, file_google_protobuf_timestamp]); /** * @generated from message agentmesh.v1.AuthFrame @@ -1066,6 +1066,19 @@ export type PolicyRole = Message<"agentmesh.v1.PolicyRole"> & { * @generated from field: repeated agentmesh.v1.HTTPGrant http = 6; */ http: HTTPGrant[]; + + /** + * Services a member with this role may serve, in the syntax of + * allowed_services ("mcp://calc", "inference://*", "*"). A role serves + * nothing it is not granted here: a router keeps a member's record only + * for a service its credential grants, and a caller sends a request to + * a provider it found by name only when the provider's credential does. + * An egress destination needs no entry: served_by grants it to the + * nodes it selects. + * + * @generated from field: repeated string allowed_publish = 7; + */ + allowedPublish: string[]; }; /** diff --git a/sdk/js/src/gen/datalog.ts b/sdk/js/src/gen/datalog.ts index 133a64fe..f9fbb0c9 100644 --- a/sdk/js/src/gen/datalog.ts +++ b/sdk/js/src/gen/datalog.ts @@ -11,6 +11,14 @@ export const BASELINE_DATALOG = { "allow if service($type, $name), granted_service_all($type)", "allow if service($type, $name), granted_service_all_types(true)" ], + "publish_policies": [ + "allow if publish($type, $name), granted_publish_exact($type, $name)", + "allow if publish($type, $name), granted_publish_set($type, $set), $set.contains($name)", + "allow if publish($type, $name), granted_publish_prefix($type, $prefix), $name.starts_with($prefix)", + "allow if publish($type, $name), granted_publish_suffix($type, $suffix), $name.ends_with($suffix)", + "allow if publish($type, $name), granted_publish_all($type)", + "allow if publish($type, $name), granted_publish_all_types(true)" + ], "rules": [ "allow_network_target($fact, $val) <- target_fact($fact, $val), granted_target_exact($fact, $val)", "allow_network_target($fact, $val) <- target_fact($fact, $val), granted_target_set($fact, $set), $set.contains($val)", @@ -52,6 +60,8 @@ export const BASELINE_DATALOG = { "fact_time": "time", "fact_role": "role", "fact_target_fact": "target_fact", + "fact_publish": "publish", + "catalog_target": "mesh.catalog", "fact_tar_block": "tar_block", "tar_block_source_pattern": "^tar_block\\(\"([A-Za-z0-9_-]+)\"\\);?\\s*$", "http_method_syntax": "^[A-Z][A-Z0-9-]{0,31}$", diff --git a/sdk/js/src/mcp.test.ts b/sdk/js/src/mcp.test.ts index 4dd9afc7..1bc821b7 100644 --- a/sdk/js/src/mcp.test.ts +++ b/sdk/js/src/mcp.test.ts @@ -49,7 +49,10 @@ let caller: Libp2p; let callerBiscuit: Uint8Array; const servedTargets: string[] = []; -function mint(peerId: string, role: string, labels: Record = {}): Uint8Array { +// mint is a credential as the control plane issues one to a member whose +// role grants publishing the given services (allowed_publish syntax, as +// granted_publish_* facts); a provider's needs the grant for what it serves. +function mint(peerId: string, role: string, labels: Record = {}, publish: string[] = ["mcp://calc"]): Uint8Array { const b = wasm.Biscuit.builder(); b.addFact(wasm.Fact.fromString(`node(${JSON.stringify(peerId)})`)); b.addFact(wasm.Fact.fromString("expiration(2035-01-01T00:00:00Z)")); @@ -57,6 +60,16 @@ function mint(peerId: string, role: string, labels: Record = {}) for (const [k, v] of Object.entries(labels)) { b.addFact(wasm.Fact.fromString(`label(${JSON.stringify(k)}, ${JSON.stringify(v)})`)); } + for (const svc of publish) { + const m = /^([a-z0-9]+):\/\/(.+)$/.exec(svc); + if (m === null) { + b.addFact(wasm.Fact.fromString("granted_publish_all_types(true)")); + } else if (m[2] === "*") { + b.addFact(wasm.Fact.fromString(`granted_publish_all(${JSON.stringify(m[1])})`)); + } else { + b.addFact(wasm.Fact.fromString(`granted_publish_exact(${JSON.stringify(m[1])}, ${JSON.stringify(m[2])})`)); + } + } return b.build(cpKeyPair.getPrivateKey()).toBytes(); } @@ -164,12 +177,12 @@ test("required labels are checked on the provider's credential", async () => { const ok = await openMCPSession(conn, frame("mcp://calc"), [cpKey], { requiredLabels: { region: "eu" } }); await ok.close(); await assert.rejects(openMCPSession(conn, frame("mcp://calc"), [cpKey], { requiredLabels: { region: "us" } }), LabelsNotSatisfiedError); - assert.throws(() => requireLabels({ peerId: "p", expiration: new Date(), verifyingKey: cpKey, roles: [], labels: {}, taskRules: [] }, { team: "x" }), /team=x/); + assert.throws(() => requireLabels({ peerId: "p", expiration: new Date(), verifyingKey: cpKey, roles: [], labels: {}, taskRules: [], token: new Uint8Array() }, { team: "x" }), /team=x/); }); test("a requirement of several labels is met only by every one of them, as agentmesh-node's checkPeerLabels", () => { // The cases of internal/node/labels_gate_test.go, run through the SDK's predicate. - const attesting = (labels: Record) => ({ peerId: "p", expiration: new Date(), verifyingKey: cpKey, roles: [], labels, taskRules: [] }); + const attesting = (labels: Record) => ({ peerId: "p", expiration: new Date(), verifyingKey: cpKey, roles: [], labels, taskRules: [], token: new Uint8Array() }); // exact match requireLabels(attesting({ region: "us-east-1" }), { region: "us-east-1" }); // every pair of two attested @@ -190,7 +203,7 @@ test("a requirement of several labels is met only by every one of them, as agent }); test("the egress floor is met only by every one of its pairs, as agentmesh-node's api.LabelCheck", () => { - const attesting = (labels: Record) => ({ peerId: "p", expiration: new Date(), verifyingKey: cpKey, roles: [], labels, taskRules: [] }); + const attesting = (labels: Record) => ({ peerId: "p", expiration: new Date(), verifyingKey: cpKey, roles: [], labels, taskRules: [], token: new Uint8Array() }); requireEgressLabels(attesting({ region: "eu", team: "platform" }), { region: "eu" }); requireEgressLabels(attesting({ region: "eu", team: "platform" }), { region: "eu", team: "platform" }); // one pair short is a refusal that names the whole floor @@ -243,3 +256,29 @@ test("only a node is a provider, as agentmesh-node's checkPeerLabels requires", providerBiscuit = nodeBiscuit; } }); + +test("only a node granted the service is its provider, as agentmesh-node's checkProvider requires", async () => { + // The directory names whoever announced calc; the provider's credential + // says who may serve it. A grant by type or for everything serves too; + // the provider's own catalog needs none. + const nodeBiscuit = providerBiscuit; + try { + for (const [publish, ok] of [ + [["mcp://weather"], false], + [[], false], + [["mcp://*"], true], + [["*"], true], + ] as Array<[string[], boolean]>) { + providerBiscuit = mint(provider.peerId.toString(), ROLE_NODE, { region: "eu" }, publish); + const conn = await caller.dial(provider.getMultiaddrs()[0] as Parameters[0]); + if (ok) { + await (await openMCPSession(conn, frame("mcp://calc"), [cpKey])).close(); + } else { + await assert.rejects(openMCPSession(conn, frame("mcp://calc"), [cpKey]), /is not granted serving mcp:\/\/calc/); + await (await openMCPSession(conn, frame(""), [cpKey])).close(); + } + } + } finally { + providerBiscuit = nodeBiscuit; + } +}); diff --git a/sdk/js/src/mcp.ts b/sdk/js/src/mcp.ts index 57e8fcf0..6f597d0c 100644 --- a/sdk/js/src/mcp.ts +++ b/sdk/js/src/mcp.ts @@ -22,9 +22,9 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { JSONRPCMessageSchema, type JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"; import { AUTH_HANDSHAKE_TIMEOUT_MS, AuthRejectedError, MAX_AUTH_FRAME_BYTES, MCP_PROTOCOL } from "./auth.ts"; -import { BiscuitVerificationError, requireRole, verifyPeerBiscuit, type VerifiedBiscuit } from "./biscuit.ts"; +import { BiscuitVerificationError, requirePublish, requireRole, verifyPeerBiscuit, type VerifiedBiscuit } from "./biscuit.ts"; import { ROLE_NODE } from "./controlplane.ts"; -import { decodeAuthResponse } from "./credential.ts"; +import { decodeAuthFrame, decodeAuthResponse } from "./credential.ts"; /** go-msgio's default message cap, which agentmesh-node's StreamTransport uses. */ export const MAX_MCP_MESSAGE_BYTES = 8 * 1024 * 1024; @@ -173,6 +173,8 @@ export async function openMCPSession( provider = await verifyPeerBiscuit(resp.biscuit, conn.remotePeer.toString(), trustedKeys); // Only nodes host services; a router's or an admin's credential is a member, not a provider. requireRole(provider, ROLE_NODE); + // And only a node the policy lets serve the named service is one. + await requirePublish(provider, decodeAuthFrame(frame).targetService); requireLabels(provider, options.requiredLabels); requireEgressLabels(provider, egressRequireLabels); } catch (err) { diff --git a/sdk/js/src/session.test.ts b/sdk/js/src/session.test.ts index 9bea398d..db5442ed 100644 --- a/sdk/js/src/session.test.ts +++ b/sdk/js/src/session.test.ts @@ -70,6 +70,10 @@ function mint(peerId: string, role: string, expiration = "2035-01-01T00:00:00Z", b.addFact(wasm.Fact.fromString(`client_peer_id(${JSON.stringify(peerId)})`)); b.addFact(wasm.Fact.fromString(`expiration(${expiration})`)); b.addFact(wasm.Fact.fromString(`role(${JSON.stringify(role)})`)); + if (role === ROLE_NODE) { + // The node role of these meshes serves everything. + b.addFact(wasm.Fact.fromString("granted_publish_all_types(true)")); + } for (const [k, v] of Object.entries(labels)) { b.addFact(wasm.Fact.fromString(`label(${JSON.stringify(k)}, ${JSON.stringify(v)})`)); } diff --git a/sdk/js/src/session.ts b/sdk/js/src/session.ts index 598efbef..17179095 100644 --- a/sdk/js/src/session.ts +++ b/sdk/js/src/session.ts @@ -28,7 +28,7 @@ import { goAwayReasonName, goAwayStreamHandler, } from "./auth.ts"; -import { ROLE_ROUTER, attenuateBiscuit, requireRole, sealBiscuit, type VerifiedBiscuit } from "./biscuit.ts"; +import { ROLE_ROUTER, attenuateBiscuit, requirePublish, requireRole, sealBiscuit, type VerifiedBiscuit } from "./biscuit.ts"; import { ROLE_NODE } from "./controlplane.ts"; import { encodeAuthFrame } from "./credential.ts"; import { canonicalPeerId } from "./identity.ts"; @@ -1111,26 +1111,28 @@ export class MeshSession { * way agentmesh-node's egress proxy does for /mesh////. */ async request(peer: Peer, targetService: string, path: string, options: HTTPRequestOptions = {}): Promise { - const conn = await this.#egressConnection(peer, options.signal); + const conn = await this.#egressConnection(peer, targetService, options.signal); return httpRequestOverStream(conn, this.biscuit, targetService, path, options); } /** * The connection an HTTP call goes out on, its peer verified as an enrolled - * node holding the floor before anything is sent (agentmesh-node's VerifyPeerLabels). + * node holding the floor and granted the service before anything is sent + * (agentmesh-node's VerifyProvider). */ - async #egressConnection(peer: Peer, signal?: AbortSignal): Promise { + async #egressConnection(peer: Peer, targetService: string, signal?: AbortSignal): Promise { const conn = await this.connect(peer, signal); - const peerId = conn.remotePeer.toString(); - const until = this.#egressVerdicts.get(peerId); + const verdict = `${conn.remotePeer.toString()}|${targetService}`; + const until = this.#egressVerdicts.get(verdict); if (until !== undefined && until.getTime() > Date.now()) { return conn; } const provider = await authenticateWithPeer(conn, this.mesh.authFrame(), this.mesh.credential.controlPlaneKeys); // Only nodes host services; a router's or an admin's credential is a member, not a provider. requireRole(provider, ROLE_NODE); + await requirePublish(provider, targetService); requireEgressLabels(provider, this.#egressRequireLabels); - this.#egressVerdicts.set(peerId, new Date(Date.now() + EGRESS_VERDICT_TTL_MS)); + this.#egressVerdicts.set(verdict, new Date(Date.now() + EGRESS_VERDICT_TTL_MS)); return conn; } @@ -1143,8 +1145,9 @@ export class MeshSession { fetch(): typeof fetch { return async (input, init) => { const request = new Request(input, init); - const { peerId } = splitMeshURL(new URL(request.url)); - const conn = await this.#egressConnection(peerId, request.signal); + const { peerId, target } = splitMeshURL(new URL(request.url)); + const [svcType = "", svcName = ""] = target.slice(1).split("/"); + const conn = await this.#egressConnection(peerId, `${svcType}://${svcName}`, request.signal); const streamOptions: { signal?: AbortSignal } = {}; if (init?.signal !== undefined && init.signal !== null) { streamOptions.signal = init.signal; diff --git a/sdk/python/src/agent_mesh/_gen/datalog.json b/sdk/python/src/agent_mesh/_gen/datalog.json index 48cb3929..7060bdae 100644 --- a/sdk/python/src/agent_mesh/_gen/datalog.json +++ b/sdk/python/src/agent_mesh/_gen/datalog.json @@ -8,6 +8,14 @@ "allow if service($type, $name), granted_service_all($type)", "allow if service($type, $name), granted_service_all_types(true)" ], + "publish_policies": [ + "allow if publish($type, $name), granted_publish_exact($type, $name)", + "allow if publish($type, $name), granted_publish_set($type, $set), $set.contains($name)", + "allow if publish($type, $name), granted_publish_prefix($type, $prefix), $name.starts_with($prefix)", + "allow if publish($type, $name), granted_publish_suffix($type, $suffix), $name.ends_with($suffix)", + "allow if publish($type, $name), granted_publish_all($type)", + "allow if publish($type, $name), granted_publish_all_types(true)" + ], "rules": [ "allow_network_target($fact, $val) <- target_fact($fact, $val), granted_target_exact($fact, $val)", "allow_network_target($fact, $val) <- target_fact($fact, $val), granted_target_set($fact, $set), $set.contains($val)", @@ -49,6 +57,8 @@ "fact_time": "time", "fact_role": "role", "fact_target_fact": "target_fact", + "fact_publish": "publish", + "catalog_target": "mesh.catalog", "fact_tar_block": "tar_block", "tar_block_source_pattern": "^tar_block\\(\"([A-Za-z0-9_-]+)\"\\);?\\s*$", "http_method_syntax": "^[A-Z][A-Z0-9-]{0,31}$", diff --git a/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.py b/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.py index 2d9874bb..be4059d3 100644 --- a/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.py +++ b/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.py @@ -15,7 +15,7 @@ from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0f\x61gentmesh.proto\x12\x0c\x61gentmesh.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"4\n\tAuthFrame\x12\x0f\n\x07\x62iscuit\x18\x01 \x01(\x0c\x12\x16\n\x0etarget_service\x18\x02 \x01(\t\"?\n\x0c\x41uthResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x03 \x01(\x0c\"\xb1\x01\n\x0cRouterGoAway\x12\x31\n\x06reason\x18\x01 \x01(\x0e\x32!.agentmesh.v1.RouterGoAway.Reason\x12.\n\x0bretry_after\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\">\n\x06Reason\x12\x16\n\x12REASON_UNSPECIFIED\x10\x00\x12\x0c\n\x08\x44RAINING\x10\x01\x12\x0e\n\nOVERLOADED\x10\x02\"\xdc\x01\n\tMeshEvent\x12*\n\x04type\x18\x01 \x01(\x0e\x32\x1c.agentmesh.v1.MeshEvent.Type\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12.\n\nevent_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0enew_public_key\x18\x04 \x01(\x0c\x12\x11\n\tsignature\x18\x05 \x01(\x0c\"7\n\x04Type\x12\n\n\x06\x42\x41NNED\x10\x00\x12\x10\n\x0cKEY_ROTATION\x10\x01\x12\x11\n\rPOLICY_UPDATE\x10\x02\"\xf9\x01\n\rEnrollRequest\x12\x0b\n\x03jwt\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x12\n\npublic_key\x18\x03 \x01(\x0c\x12\x16\n\x0erequested_role\x18\x04 \x01(\t\x12\x37\n\x06labels\x18\x05 \x03(\x0b\x32\'.agentmesh.v1.EnrollRequest.LabelsEntry\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xab\x01\n\x0e\x45nrollResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12 \n\x18\x63ontrol_plane_public_key\x18\x03 \x01(\x0c\x12\x18\n\x10router_addresses\x18\x04 \x03(\t\x12/\n\x0b\x65xpire_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x97\x02\n\x16\x42ootstrapEnrollRequest\x12\x17\n\x0f\x62ootstrap_token\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x12\n\npublic_key\x18\x03 \x01(\x0c\x12\x16\n\x0erequested_role\x18\x04 \x01(\t\x12@\n\x06labels\x18\x05 \x03(\x0b\x32\x30.agentmesh.v1.BootstrapEnrollRequest.LabelsEntry\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x83\x02\n\x17\x42ootstrapEnrollResponse\x12.\n\x06status\x18\x01 \x01(\x0e\x32\x1e.agentmesh.v1.EnrollmentStatus\x12\x15\n\rbiscuit_token\x18\x02 \x01(\x0c\x12\x1d\n\x15poll_interval_seconds\x18\x03 \x01(\x05\x12\x15\n\rerror_message\x18\x04 \x01(\t\x12 \n\x18\x63ontrol_plane_public_key\x18\x05 \x01(\x0c\x12\x18\n\x10router_addresses\x18\x06 \x03(\t\x12/\n\x0b\x65xpire_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"Y\n\x0bServiceInfo\x12\'\n\x04type\x18\x01 \x01(\x0e\x32\x19.agentmesh.v1.ServiceType\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\"\x81\x01\n\x0e\x43ommandBackend\x12\x0f\n\x07\x63ommand\x18\x01 \x03(\t\x12\x32\n\x03\x65nv\x18\x02 \x03(\x0b\x32%.agentmesh.v1.CommandBackend.EnvEntry\x1a*\n\x08\x45nvEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x96\x01\n\x16RegisterServiceRequest\x12*\n\x07service\x18\x01 \x01(\x0b\x32\x19.agentmesh.v1.ServiceInfo\x12\x14\n\ntarget_url\x18\x02 \x01(\tH\x00\x12/\n\x07\x63ommand\x18\x03 \x01(\x0b\x32\x1c.agentmesh.v1.CommandBackendH\x00\x42\t\n\x07\x62\x61\x63kend\"i\n\x12\x44iscoveredProvider\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x17\n\x0flocal_proxy_url\x18\x02 \x01(\t\x12\x10\n\x08srv_name\x18\x03 \x01(\t\x12\x17\n\x0fsrv_description\x18\x04 \x01(\t\"\xe8\x01\n\x18\x43ontrolPlaneInfoResponse\x12\x13\n\x0boidc_issuer\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x18\n\x10router_addresses\x18\x04 \x03(\t\x12\x17\n\x0f\x62\x61nned_peer_ids\x18\x05 \x03(\t\x12)\n\x07routers\x18\x06 \x03(\x0b\x32\x18.agentmesh.v1.RouterInfo\x12\x34\n\tdirectory\x18\x07 \x01(\x0b\x32!.agentmesh.v1.DirectoryParameters\"\x90\x01\n\x13\x44irectoryParameters\x12\x13\n\x0breplication\x18\x01 \x01(\x05\x12\x35\n\x12reprovide_interval\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\x12-\n\nrecord_ttl\x18\x03 \x01(\x0b\x32\x19.google.protobuf.Duration\"\xfb\x01\n\x10\x44irectoryRequest\x12\x31\n\x07provide\x18\x01 \x01(\x0b\x32\x1e.agentmesh.v1.DirectoryProvideH\x00\x12>\n\x0e\x66ind_providers\x18\x02 \x01(\x0b\x32$.agentmesh.v1.DirectoryFindProvidersH\x00\x12\x34\n\tfind_peer\x18\x03 \x01(\x0b\x32\x1f.agentmesh.v1.DirectoryFindPeerH\x00\x12\x33\n\x08withdraw\x18\x04 \x01(\x0b\x32\x1f.agentmesh.v1.DirectoryWithdrawH\x00\x42\t\n\x07request\"Q\n\x10\x44irectoryProvide\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\r\n\x05\x61\x64\x64rs\x18\x02 \x03(\t\x12\x10\n\x08provider\x18\x03 \x01(\t\x12\x0f\n\x07service\x18\x04 \x01(\t\"2\n\x11\x44irectoryWithdraw\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\x10\n\x08provider\x18\x02 \x01(\t\"4\n\x16\x44irectoryFindProviders\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\r\n\x05limit\x18\x02 \x01(\r\"$\n\x11\x44irectoryFindPeer\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\"w\n\x11\x44irectoryResponse\x12+\n\tproviders\x18\x01 \x03(\x0b\x32\x18.agentmesh.v1.PeerRecord\x12&\n\x04peer\x18\x02 \x01(\x0b\x32\x18.agentmesh.v1.PeerRecord\x12\r\n\x05\x65rror\x18\x03 \x01(\t\",\n\nPeerRecord\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\r\n\x05\x61\x64\x64rs\x18\x02 \x03(\t\"\xc4\x01\n\nRouterInfo\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x11\n\taddresses\x18\x02 \x03(\t\x12\x34\n\x06labels\x18\x03 \x03(\x0b\x32$.agentmesh.v1.RouterInfo.LabelsEntry\x12\x13\n\x0b\x63onnections\x18\x04 \x01(\x05\x12\x18\n\x10\x63onnection_limit\x18\x05 \x01(\x05\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x83\x02\n\x12RouterLeaseRequest\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x11\n\taddresses\x18\x02 \x03(\t\x12\x0f\n\x07\x62iscuit\x18\x03 \x01(\x0c\x12\x17\n\x0f\x63onnected_peers\x18\x04 \x03(\t\x12\x10\n\x08\x64ht_size\x18\x05 \x01(\x05\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\x12&\n\x03ttl\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x13\n\x0b\x63onnections\x18\t \x01(\x05\x12\x18\n\x10\x63onnection_limit\x18\n \x01(\x05\"f\n\x13RouterLeaseResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xa4\x01\n\nPolicyRole\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x17\n\x0f\x61llowed_targets\x18\x02 \x03(\t\x12\x18\n\x10\x61llowed_services\x18\x03 \x03(\t\x12\x16\n\x0e\x63ustom_datalog\x18\x04 \x03(\t\x12\x16\n\x0e\x61llowed_labels\x18\x05 \x03(\t\x12%\n\x04http\x18\x06 \x03(\x0b\x32\x17.agentmesh.v1.HTTPGrant\"<\n\tHTTPGrant\x12\x0f\n\x07service\x18\x01 \x01(\t\x12\x0f\n\x07methods\x18\x02 \x03(\t\x12\r\n\x05paths\x18\x03 \x03(\t\"\xa1\x02\n\x11\x45gressDestination\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x12\n\ntarget_url\x18\x02 \x01(\t\x12\x12\n\ncredential\x18\x03 \x01(\t\x12\x11\n\tserved_by\x18\x04 \x03(\t\x12.\n\x06\x62roker\x18\x05 \x01(\x0b\x32\x1e.agentmesh.v1.CredentialBroker\x12,\n\ninspection\x18\x06 \x01(\x0b\x32\x18.agentmesh.v1.Inspection\x12&\n\x04mode\x18\x07 \x01(\x0e\x32\x18.agentmesh.v1.EgressMode\x12\r\n\x05ports\x18\x08 \x03(\r\x12\x15\n\rpreserve_host\x18\t \x01(\x08\x12\x17\n\x0f\x66orward_context\x18\n \x01(\x08\"9\n\nInspection\x12+\n\ninspectors\x18\x01 \x03(\x0b\x32\x17.agentmesh.v1.Inspector\"o\n\tInspector\x12/\n\x0bmodel_armor\x18\x01 \x01(\x0b\x32\x18.agentmesh.v1.ModelArmorH\x00\x12)\n\x08\x65xt_proc\x18\x02 \x01(\x0b\x32\x15.agentmesh.v1.ExtProcH\x00\x42\x06\n\x04kind\"\x91\x01\n\nModelArmor\x12\x10\n\x08template\x18\x01 \x01(\t\x12\x32\n\x08response\x18\x02 \x01(\x0e\x32 .agentmesh.v1.ResponseInspection\x12\x11\n\tfail_open\x18\x03 \x01(\x08\x12*\n\x07timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\"\x88\x02\n\x07\x45xtProc\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\n\n\x02\x63\x61\x18\x02 \x01(\t\x12\x1a\n\x12\x63lient_certificate\x18\x03 \x01(\t\x12<\n\x0fprocessing_mode\x18\x04 \x01(\x0b\x32#.agentmesh.v1.ExtProcProcessingMode\x12\x1b\n\x13\x61llow_mode_override\x18\x05 \x01(\x08\x12\x32\n\x0fmessage_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x1a\n\x12\x66\x61ilure_mode_allow\x18\x07 \x01(\x08\x12\x1a\n\x12max_buffered_bytes\x18\x08 \x01(\r\"\xff\x04\n\x15\x45xtProcProcessingMode\x12K\n\x13request_header_mode\x18\x01 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\x12L\n\x14response_header_mode\x18\x02 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\x12G\n\x11request_body_mode\x18\x03 \x01(\x0e\x32,.agentmesh.v1.ExtProcProcessingMode.BodyMode\x12H\n\x12response_body_mode\x18\x04 \x01(\x0e\x32,.agentmesh.v1.ExtProcProcessingMode.BodyMode\x12L\n\x14request_trailer_mode\x18\x05 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\x12M\n\x15response_trailer_mode\x18\x06 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\"9\n\nHeaderMode\x12\x17\n\x13HEADER_MODE_DEFAULT\x10\x00\x12\x08\n\x04SEND\x10\x01\x12\x08\n\x04SKIP\x10\x02\"`\n\x08\x42odyMode\x12\x08\n\x04NONE\x10\x00\x12\x0c\n\x08STREAMED\x10\x01\x12\x0c\n\x08\x42UFFERED\x10\x02\x12\x14\n\x10\x42UFFERED_PARTIAL\x10\x03\x12\x18\n\x14\x46ULL_DUPLEX_STREAMED\x10\x04\"\xe1\x01\n\x10\x43redentialBroker\x12\x17\n\rstatic_secret\x18\x01 \x01(\tH\x00\x12\x37\n\x0foidc_federation\x18\x02 \x01(\x0b\x32\x1c.agentmesh.v1.OIDCFederationH\x00\x12\x36\n\x0f\x61ws_assume_role\x18\x03 \x01(\x0b\x32\x1b.agentmesh.v1.AWSAssumeRoleH\x00\x12;\n\x11platform_identity\x18\x04 \x01(\x0b\x32\x1e.agentmesh.v1.PlatformIdentityH\x00\x42\x06\n\x04kind\"_\n\x0eOIDCFederation\x12\x16\n\x0etoken_endpoint\x18\x01 \x01(\t\x12\x10\n\x08\x61udience\x18\x02 \x01(\t\x12\x13\n\x0bimpersonate\x18\x03 \x01(\t\x12\x0e\n\x06scopes\x18\x04 \x03(\t\"9\n\rAWSAssumeRole\x12\x10\n\x08role_arn\x18\x01 \x01(\t\x12\x16\n\x0esession_policy\x18\x02 \x01(\t\"\"\n\x10PlatformIdentity\x12\x0e\n\x06scopes\x18\x01 \x03(\t\".\n\rPolicyBinding\x12\x0c\n\x04role\x18\x01 \x01(\t\x12\x0f\n\x07members\x18\x02 \x03(\t\"\x97\x01\n\x0cPolicyConfig\x12\'\n\x05roles\x18\x01 \x03(\x0b\x32\x18.agentmesh.v1.PolicyRole\x12-\n\x08\x62indings\x18\x02 \x03(\x0b\x32\x1b.agentmesh.v1.PolicyBinding\x12/\n\x06\x65gress\x18\x03 \x03(\x0b\x32\x1f.agentmesh.v1.EgressDestination\"\x18\n\x16PolicyConfigGetRequest\"0\n\x17PolicyConfigGetResponse\x12\x15\n\rdatalog_rules\x18\x01 \x03(\t\"<\n\x1aPolicyConfigUpdateResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x1a\n\x18\x45gressAssignmentsRequest\"L\n\x19\x45gressAssignmentsResponse\x12/\n\x06\x65gress\x18\x01 \x03(\x0b\x32\x1f.agentmesh.v1.EgressDestination\"f\n\x0cKeysResponse\x12\x13\n\x0bpublic_keys\x18\x01 \x03(\x0c\x12-\n\tsign_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x12\n\nsignatures\x18\x03 \x03(\x0c\"k\n\x13TokenRefreshRequest\x12\x1b\n\x13\x63hallenge_signature\x18\x01 \x01(\x0c\x12\x19\n\x11\x63hallenge_unix_ms\x18\x02 \x01(\x03\x12\x0f\n\x07peer_id\x18\x03 \x01(\t\x12\x0b\n\x03jwt\x18\x04 \x01(\t\"u\n\x14TokenRefreshResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x15\n\rerror_message\x18\x03 \x01(\t\"@\n\x11NodeCatalogReport\x12+\n\x08services\x18\x01 \x03(\x0b\x32\x19.agentmesh.v1.ServiceInfo\"%\n\x12TokenRevokeRequest\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\"5\n\x13TokenRevokeResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x96\x01\n\x1b\x42ootstrapTokenCreateRequest\x12\x0c\n\x04role\x18\x01 \x01(\t\x12\x10\n\x08owner_id\x18\x02 \x01(\t\x12\x11\n\tttl_hours\x18\x03 \x01(\x05\x12\x12\n\nmax_usages\x18\x04 \x01(\x05\x12\x13\n\x0b\x64\x65scription\x18\x05 \x01(\t\x12\x1b\n\x13\x61utonomous_recovery\x18\x06 \x01(\x08\"\x8a\x01\n\x1c\x42ootstrapTokenCreateResponse\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\t\x12\x0c\n\x04role\x18\x03 \x01(\t\x12\x10\n\x08owner_id\x18\x04 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xab\x02\n\x0e\x42ootstrapToken\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04role\x18\x02 \x01(\t\x12\x10\n\x08owner_id\x18\x03 \x01(\t\x12\x12\n\nmax_usages\x18\x04 \x01(\x05\x12\x14\n\x0cusages_count\x18\x05 \x01(\x05\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12/\n\x0b\x63reate_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0brevoke_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x1b\n\x13\x61utonomous_recovery\x18\n \x01(\x08\"J\n\x1a\x42ootstrapTokenListResponse\x12,\n\x06tokens\x18\x01 \x03(\x0b\x32\x1c.agentmesh.v1.BootstrapToken\"\xd6\x02\n\x11\x45nrollmentRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x10\n\x08token_id\x18\x03 \x01(\t\x12.\n\x06status\x18\x04 \x01(\x0e\x32\x1e.agentmesh.v1.EnrollmentStatus\x12;\n\x06labels\x18\x05 \x03(\x0b\x32+.agentmesh.v1.EnrollmentRequest.LabelsEntry\x12/\n\x0b\x63reate_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0cresolve_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x13\n\x0bresolved_by\x18\x08 \x01(\t\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"R\n\x1d\x45nrollmentRequestListResponse\x12\x31\n\x08requests\x18\x01 \x03(\x0b\x32\x1f.agentmesh.v1.EnrollmentRequest\"p\n\x04User\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0e\n\x06issuer\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12\x0c\n\x04role\x18\x04 \x01(\t\x12/\n\x0b\x63reate_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xe3\x02\n\x0c\x45nrolledNode\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0c\n\x04role\x18\x02 \x01(\t\x12\x17\n\x0f\x65nrollment_type\x18\x03 \x01(\t\x12\x13\n\x0b\x63laims_json\x18\x04 \x01(\t\x12\x10\n\x08owner_id\x18\x05 \x01(\t\x12\x36\n\x06labels\x18\x06 \x03(\x0b\x32&.agentmesh.v1.EnrolledNode.LabelsEntry\x12/\n\x0b\x65nroll_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06\x62\x61nned\x18\t \x01(\x08\x12\x1b\n\x13\x61utonomous_recovery\x18\n \x01(\x08\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc4\x01\n\x0bRouterLease\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x11\n\taddresses\x18\x02 \x03(\t\x12\x35\n\x11last_renewal_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x17\n\x0f\x63onnected_peers\x18\x05 \x03(\t\x12\x10\n\x08\x64ht_size\x18\x06 \x01(\x05\"l\n\x0cNodeServices\x12+\n\x08services\x18\x01 \x03(\x0b\x32\x19.agentmesh.v1.ServiceInfo\x12/\n\x0breport_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xdb\x03\n\x13\x41\x64minStatusResponse\x12!\n\x05users\x18\x01 \x03(\x0b\x32\x12.agentmesh.v1.User\x12\x31\n\x0e\x61\x63tive_routers\x18\x02 \x03(\x0b\x32\x19.agentmesh.v1.RouterLease\x12\x32\n\x0e\x65nrolled_nodes\x18\x03 \x03(\x0b\x32\x1a.agentmesh.v1.EnrolledNode\x12<\n\x13\x65nrollment_requests\x18\x04 \x03(\x0b\x32\x1f.agentmesh.v1.EnrollmentRequest\x12\x36\n\x10\x62ootstrap_tokens\x18\x05 \x03(\x0b\x32\x1c.agentmesh.v1.BootstrapToken\x12*\n\x06policy\x18\x06 \x01(\x0b\x32\x1a.agentmesh.v1.PolicyConfig\x12H\n\x0cnode_catalog\x18\x07 \x03(\x0b\x32\x32.agentmesh.v1.AdminStatusResponse.NodeCatalogEntry\x1aN\n\x10NodeCatalogEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.agentmesh.v1.NodeServices:\x02\x38\x01\"\x81\x02\n\x12UserStatusResponse\x12 \n\x04user\x18\x01 \x01(\x0b\x32\x12.agentmesh.v1.User\x12\x32\n\x0e\x65nrolled_nodes\x18\x02 \x03(\x0b\x32\x1a.agentmesh.v1.EnrolledNode\x12\x36\n\x10\x62ootstrap_tokens\x18\x03 \x03(\x0b\x32\x1c.agentmesh.v1.BootstrapToken\x12\x31\n\x0e\x61\x63tive_routers\x18\x04 \x03(\x0b\x32\x19.agentmesh.v1.RouterLease\x12*\n\x06policy\x18\x05 \x01(\x0b\x32\x1a.agentmesh.v1.PolicyConfig\"\xe4\x01\n\x18IdentityEvidenceResponse\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12\x37\n\x13\x62iscuit_expire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x19\n\x11\x63ontrol_plane_url\x18\x04 \x01(\t\x12\"\n\x1atrusted_control_plane_keys\x18\x05 \x03(\x0c\x12.\n\ncheck_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xc6\x02\n\x14PeerEvidenceResponse\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12\x15\n\rverifying_key\x18\x03 \x01(\x0c\x12\r\n\x05roles\x18\x04 \x03(\t\x12>\n\x06labels\x18\x05 \x03(\x0b\x32..agentmesh.v1.PeerEvidenceResponse.LabelsEntry\x12/\n\x0b\x65xpire_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0erevocation_ids\x18\x07 \x03(\t\x12.\n\ncheck_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8c\x02\n\x10MemberCredential\x12\x19\n\x11\x63ontrol_plane_url\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x35\n\x0ctrusted_keys\x18\x04 \x03(\x0b\x32\x1f.agentmesh.v1.TrustedSigningKey\x12\x19\n\x11issued_under_keys\x18\x05 \x03(\x0c\x12\x18\n\x10router_addresses\x18\x06 \x03(\t\x12/\n\x0coidc_session\x18\x07 \x01(\x0b\x32\x19.agentmesh.v1.OIDCSession\"Y\n\x11TrustedSigningKey\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\x12\x30\n\x0creceive_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"Y\n\x0bOIDCSession\x12\x0e\n\x06issuer\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x15\n\rrefresh_token\x18\x04 \x01(\t\"\x93\x01\n\x15TaskAuthorizationRule\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12%\n\x05rules\x18\x03 \x03(\x0b\x32\x16.agentmesh.v1.TaskRule\x12/\n\x0b\x65xpire_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x84\x01\n\x08TaskRule\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x18\n\x10\x61llowed_services\x18\x02 \x03(\t\x12.\n\toperation\x18\x03 \x01(\x0b\x32\x1b.agentmesh.v1.TaskOperation\x12\x19\n\x11\x61llowed_resources\x18\x04 \x03(\t\"s\n\rTaskOperation\x12\x15\n\rallowed_tools\x18\x01 \x03(\t\x12\x17\n\x0f\x61llowed_methods\x18\x02 \x03(\t\x12\x15\n\rallowed_paths\x18\x03 \x03(\t\x12\x1b\n\x13\x61llowed_permissions\x18\x04 \x03(\t\"\xab\x01\n\x14TokenExchangeRequest\x12\x15\n\rsubject_token\x18\x01 \x01(\t\x12\x36\n\ttask_rule\x18\x02 \x01(\x0b\x32#.agentmesh.v1.TaskAuthorizationRule\x12\x0c\n\x04seal\x18\x03 \x01(\x08\x12\x19\n\x11\x63hallenge_unix_ms\x18\x04 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x05 \x01(\x0c\"\x7f\n\x15TokenExchangeResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\r\n\x05roles\x18\x03 \x03(\t\x12\x0f\n\x07subject\x18\x04 \x01(\t\"\x81\x01\n\x0fSTSTokenRequest\x12\x0f\n\x07\x62iscuit\x18\x01 \x01(\x0c\x12\x13\n\x0b\x64\x65stination\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x19\n\x11\x63hallenge_unix_ms\x18\x04 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x05 \x01(\x0c\"\x83\x01\n\x10STSTokenResponse\x12\x0b\n\x03jwt\x18\x01 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0f\n\x07subject\x18\x03 \x01(\t\x12\r\n\x05roles\x18\x04 \x03(\t\x12\x11\n\ttask_name\x18\x05 \x01(\t\"F\n\x13RevocationsResponse\x12\x16\n\x0erevocation_ids\x18\x01 \x03(\t\x12\x17\n\x0f\x62\x61nned_peer_ids\x18\x02 \x03(\t*\x94\x01\n\x10\x45nrollmentStatus\x12!\n\x1d\x45NROLLMENT_STATUS_UNSPECIFIED\x10\x00\x12\x1d\n\x19\x45NROLLMENT_STATUS_PENDING\x10\x01\x12\x1e\n\x1a\x45NROLLMENT_STATUS_APPROVED\x10\x02\x12\x1e\n\x1a\x45NROLLMENT_STATUS_REJECTED\x10\x03*\x8c\x01\n\x0bServiceType\x12\x1c\n\x18SERVICE_TYPE_UNSPECIFIED\x10\x00\x12\x14\n\x10SERVICE_TYPE_MCP\x10\x01\x12\x1a\n\x16SERVICE_TYPE_INFERENCE\x10\x02\x12\x14\n\x10SERVICE_TYPE_A2A\x10\x03\x12\x17\n\x13SERVICE_TYPE_EGRESS\x10\x04*7\n\nEgressMode\x12\x14\n\x10\x45GRESS_MODE_HTTP\x10\x00\x12\x13\n\x0f\x45GRESS_MODE_TCP\x10\x01*\\\n\x12ResponseInspection\x12 \n\x1cRESPONSE_INSPECTION_BUFFERED\x10\x00\x12$\n RESPONSE_INSPECTION_REQUEST_ONLY\x10\x01\x42!Z\x1fgithub.com/google/agentmesh/apib\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0f\x61gentmesh.proto\x12\x0c\x61gentmesh.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"4\n\tAuthFrame\x12\x0f\n\x07\x62iscuit\x18\x01 \x01(\x0c\x12\x16\n\x0etarget_service\x18\x02 \x01(\t\"?\n\x0c\x41uthResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x03 \x01(\x0c\"\xb1\x01\n\x0cRouterGoAway\x12\x31\n\x06reason\x18\x01 \x01(\x0e\x32!.agentmesh.v1.RouterGoAway.Reason\x12.\n\x0bretry_after\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\">\n\x06Reason\x12\x16\n\x12REASON_UNSPECIFIED\x10\x00\x12\x0c\n\x08\x44RAINING\x10\x01\x12\x0e\n\nOVERLOADED\x10\x02\"\xdc\x01\n\tMeshEvent\x12*\n\x04type\x18\x01 \x01(\x0e\x32\x1c.agentmesh.v1.MeshEvent.Type\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12.\n\nevent_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0enew_public_key\x18\x04 \x01(\x0c\x12\x11\n\tsignature\x18\x05 \x01(\x0c\"7\n\x04Type\x12\n\n\x06\x42\x41NNED\x10\x00\x12\x10\n\x0cKEY_ROTATION\x10\x01\x12\x11\n\rPOLICY_UPDATE\x10\x02\"\xf9\x01\n\rEnrollRequest\x12\x0b\n\x03jwt\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x12\n\npublic_key\x18\x03 \x01(\x0c\x12\x16\n\x0erequested_role\x18\x04 \x01(\t\x12\x37\n\x06labels\x18\x05 \x03(\x0b\x32\'.agentmesh.v1.EnrollRequest.LabelsEntry\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xab\x01\n\x0e\x45nrollResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12 \n\x18\x63ontrol_plane_public_key\x18\x03 \x01(\x0c\x12\x18\n\x10router_addresses\x18\x04 \x03(\t\x12/\n\x0b\x65xpire_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x97\x02\n\x16\x42ootstrapEnrollRequest\x12\x17\n\x0f\x62ootstrap_token\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x12\n\npublic_key\x18\x03 \x01(\x0c\x12\x16\n\x0erequested_role\x18\x04 \x01(\t\x12@\n\x06labels\x18\x05 \x03(\x0b\x32\x30.agentmesh.v1.BootstrapEnrollRequest.LabelsEntry\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x83\x02\n\x17\x42ootstrapEnrollResponse\x12.\n\x06status\x18\x01 \x01(\x0e\x32\x1e.agentmesh.v1.EnrollmentStatus\x12\x15\n\rbiscuit_token\x18\x02 \x01(\x0c\x12\x1d\n\x15poll_interval_seconds\x18\x03 \x01(\x05\x12\x15\n\rerror_message\x18\x04 \x01(\t\x12 \n\x18\x63ontrol_plane_public_key\x18\x05 \x01(\x0c\x12\x18\n\x10router_addresses\x18\x06 \x03(\t\x12/\n\x0b\x65xpire_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"Y\n\x0bServiceInfo\x12\'\n\x04type\x18\x01 \x01(\x0e\x32\x19.agentmesh.v1.ServiceType\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\"\x81\x01\n\x0e\x43ommandBackend\x12\x0f\n\x07\x63ommand\x18\x01 \x03(\t\x12\x32\n\x03\x65nv\x18\x02 \x03(\x0b\x32%.agentmesh.v1.CommandBackend.EnvEntry\x1a*\n\x08\x45nvEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x96\x01\n\x16RegisterServiceRequest\x12*\n\x07service\x18\x01 \x01(\x0b\x32\x19.agentmesh.v1.ServiceInfo\x12\x14\n\ntarget_url\x18\x02 \x01(\tH\x00\x12/\n\x07\x63ommand\x18\x03 \x01(\x0b\x32\x1c.agentmesh.v1.CommandBackendH\x00\x42\t\n\x07\x62\x61\x63kend\"i\n\x12\x44iscoveredProvider\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x17\n\x0flocal_proxy_url\x18\x02 \x01(\t\x12\x10\n\x08srv_name\x18\x03 \x01(\t\x12\x17\n\x0fsrv_description\x18\x04 \x01(\t\"\xe8\x01\n\x18\x43ontrolPlaneInfoResponse\x12\x13\n\x0boidc_issuer\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x18\n\x10router_addresses\x18\x04 \x03(\t\x12\x17\n\x0f\x62\x61nned_peer_ids\x18\x05 \x03(\t\x12)\n\x07routers\x18\x06 \x03(\x0b\x32\x18.agentmesh.v1.RouterInfo\x12\x34\n\tdirectory\x18\x07 \x01(\x0b\x32!.agentmesh.v1.DirectoryParameters\"\x90\x01\n\x13\x44irectoryParameters\x12\x13\n\x0breplication\x18\x01 \x01(\x05\x12\x35\n\x12reprovide_interval\x18\x02 \x01(\x0b\x32\x19.google.protobuf.Duration\x12-\n\nrecord_ttl\x18\x03 \x01(\x0b\x32\x19.google.protobuf.Duration\"\xfb\x01\n\x10\x44irectoryRequest\x12\x31\n\x07provide\x18\x01 \x01(\x0b\x32\x1e.agentmesh.v1.DirectoryProvideH\x00\x12>\n\x0e\x66ind_providers\x18\x02 \x01(\x0b\x32$.agentmesh.v1.DirectoryFindProvidersH\x00\x12\x34\n\tfind_peer\x18\x03 \x01(\x0b\x32\x1f.agentmesh.v1.DirectoryFindPeerH\x00\x12\x33\n\x08withdraw\x18\x04 \x01(\x0b\x32\x1f.agentmesh.v1.DirectoryWithdrawH\x00\x42\t\n\x07request\"Q\n\x10\x44irectoryProvide\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\r\n\x05\x61\x64\x64rs\x18\x02 \x03(\t\x12\x10\n\x08provider\x18\x03 \x01(\t\x12\x0f\n\x07service\x18\x04 \x01(\t\"2\n\x11\x44irectoryWithdraw\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\x10\n\x08provider\x18\x02 \x01(\t\"4\n\x16\x44irectoryFindProviders\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\r\n\x05limit\x18\x02 \x01(\r\"$\n\x11\x44irectoryFindPeer\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\"w\n\x11\x44irectoryResponse\x12+\n\tproviders\x18\x01 \x03(\x0b\x32\x18.agentmesh.v1.PeerRecord\x12&\n\x04peer\x18\x02 \x01(\x0b\x32\x18.agentmesh.v1.PeerRecord\x12\r\n\x05\x65rror\x18\x03 \x01(\t\",\n\nPeerRecord\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\r\n\x05\x61\x64\x64rs\x18\x02 \x03(\t\"\xc4\x01\n\nRouterInfo\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x11\n\taddresses\x18\x02 \x03(\t\x12\x34\n\x06labels\x18\x03 \x03(\x0b\x32$.agentmesh.v1.RouterInfo.LabelsEntry\x12\x13\n\x0b\x63onnections\x18\x04 \x01(\x05\x12\x18\n\x10\x63onnection_limit\x18\x05 \x01(\x05\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x83\x02\n\x12RouterLeaseRequest\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x11\n\taddresses\x18\x02 \x03(\t\x12\x0f\n\x07\x62iscuit\x18\x03 \x01(\x0c\x12\x17\n\x0f\x63onnected_peers\x18\x04 \x03(\t\x12\x10\n\x08\x64ht_size\x18\x05 \x01(\x05\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\x12&\n\x03ttl\x18\x08 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x13\n\x0b\x63onnections\x18\t \x01(\x05\x12\x18\n\x10\x63onnection_limit\x18\n \x01(\x05\"f\n\x13RouterLeaseResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xbd\x01\n\nPolicyRole\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x17\n\x0f\x61llowed_targets\x18\x02 \x03(\t\x12\x18\n\x10\x61llowed_services\x18\x03 \x03(\t\x12\x16\n\x0e\x63ustom_datalog\x18\x04 \x03(\t\x12\x16\n\x0e\x61llowed_labels\x18\x05 \x03(\t\x12%\n\x04http\x18\x06 \x03(\x0b\x32\x17.agentmesh.v1.HTTPGrant\x12\x17\n\x0f\x61llowed_publish\x18\x07 \x03(\t\"<\n\tHTTPGrant\x12\x0f\n\x07service\x18\x01 \x01(\t\x12\x0f\n\x07methods\x18\x02 \x03(\t\x12\r\n\x05paths\x18\x03 \x03(\t\"\xa1\x02\n\x11\x45gressDestination\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x12\n\ntarget_url\x18\x02 \x01(\t\x12\x12\n\ncredential\x18\x03 \x01(\t\x12\x11\n\tserved_by\x18\x04 \x03(\t\x12.\n\x06\x62roker\x18\x05 \x01(\x0b\x32\x1e.agentmesh.v1.CredentialBroker\x12,\n\ninspection\x18\x06 \x01(\x0b\x32\x18.agentmesh.v1.Inspection\x12&\n\x04mode\x18\x07 \x01(\x0e\x32\x18.agentmesh.v1.EgressMode\x12\r\n\x05ports\x18\x08 \x03(\r\x12\x15\n\rpreserve_host\x18\t \x01(\x08\x12\x17\n\x0f\x66orward_context\x18\n \x01(\x08\"9\n\nInspection\x12+\n\ninspectors\x18\x01 \x03(\x0b\x32\x17.agentmesh.v1.Inspector\"o\n\tInspector\x12/\n\x0bmodel_armor\x18\x01 \x01(\x0b\x32\x18.agentmesh.v1.ModelArmorH\x00\x12)\n\x08\x65xt_proc\x18\x02 \x01(\x0b\x32\x15.agentmesh.v1.ExtProcH\x00\x42\x06\n\x04kind\"\x91\x01\n\nModelArmor\x12\x10\n\x08template\x18\x01 \x01(\t\x12\x32\n\x08response\x18\x02 \x01(\x0e\x32 .agentmesh.v1.ResponseInspection\x12\x11\n\tfail_open\x18\x03 \x01(\x08\x12*\n\x07timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\"\x88\x02\n\x07\x45xtProc\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\n\n\x02\x63\x61\x18\x02 \x01(\t\x12\x1a\n\x12\x63lient_certificate\x18\x03 \x01(\t\x12<\n\x0fprocessing_mode\x18\x04 \x01(\x0b\x32#.agentmesh.v1.ExtProcProcessingMode\x12\x1b\n\x13\x61llow_mode_override\x18\x05 \x01(\x08\x12\x32\n\x0fmessage_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x1a\n\x12\x66\x61ilure_mode_allow\x18\x07 \x01(\x08\x12\x1a\n\x12max_buffered_bytes\x18\x08 \x01(\r\"\xff\x04\n\x15\x45xtProcProcessingMode\x12K\n\x13request_header_mode\x18\x01 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\x12L\n\x14response_header_mode\x18\x02 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\x12G\n\x11request_body_mode\x18\x03 \x01(\x0e\x32,.agentmesh.v1.ExtProcProcessingMode.BodyMode\x12H\n\x12response_body_mode\x18\x04 \x01(\x0e\x32,.agentmesh.v1.ExtProcProcessingMode.BodyMode\x12L\n\x14request_trailer_mode\x18\x05 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\x12M\n\x15response_trailer_mode\x18\x06 \x01(\x0e\x32..agentmesh.v1.ExtProcProcessingMode.HeaderMode\"9\n\nHeaderMode\x12\x17\n\x13HEADER_MODE_DEFAULT\x10\x00\x12\x08\n\x04SEND\x10\x01\x12\x08\n\x04SKIP\x10\x02\"`\n\x08\x42odyMode\x12\x08\n\x04NONE\x10\x00\x12\x0c\n\x08STREAMED\x10\x01\x12\x0c\n\x08\x42UFFERED\x10\x02\x12\x14\n\x10\x42UFFERED_PARTIAL\x10\x03\x12\x18\n\x14\x46ULL_DUPLEX_STREAMED\x10\x04\"\xe1\x01\n\x10\x43redentialBroker\x12\x17\n\rstatic_secret\x18\x01 \x01(\tH\x00\x12\x37\n\x0foidc_federation\x18\x02 \x01(\x0b\x32\x1c.agentmesh.v1.OIDCFederationH\x00\x12\x36\n\x0f\x61ws_assume_role\x18\x03 \x01(\x0b\x32\x1b.agentmesh.v1.AWSAssumeRoleH\x00\x12;\n\x11platform_identity\x18\x04 \x01(\x0b\x32\x1e.agentmesh.v1.PlatformIdentityH\x00\x42\x06\n\x04kind\"_\n\x0eOIDCFederation\x12\x16\n\x0etoken_endpoint\x18\x01 \x01(\t\x12\x10\n\x08\x61udience\x18\x02 \x01(\t\x12\x13\n\x0bimpersonate\x18\x03 \x01(\t\x12\x0e\n\x06scopes\x18\x04 \x03(\t\"9\n\rAWSAssumeRole\x12\x10\n\x08role_arn\x18\x01 \x01(\t\x12\x16\n\x0esession_policy\x18\x02 \x01(\t\"\"\n\x10PlatformIdentity\x12\x0e\n\x06scopes\x18\x01 \x03(\t\".\n\rPolicyBinding\x12\x0c\n\x04role\x18\x01 \x01(\t\x12\x0f\n\x07members\x18\x02 \x03(\t\"\x97\x01\n\x0cPolicyConfig\x12\'\n\x05roles\x18\x01 \x03(\x0b\x32\x18.agentmesh.v1.PolicyRole\x12-\n\x08\x62indings\x18\x02 \x03(\x0b\x32\x1b.agentmesh.v1.PolicyBinding\x12/\n\x06\x65gress\x18\x03 \x03(\x0b\x32\x1f.agentmesh.v1.EgressDestination\"\x18\n\x16PolicyConfigGetRequest\"0\n\x17PolicyConfigGetResponse\x12\x15\n\rdatalog_rules\x18\x01 \x03(\t\"<\n\x1aPolicyConfigUpdateResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x1a\n\x18\x45gressAssignmentsRequest\"L\n\x19\x45gressAssignmentsResponse\x12/\n\x06\x65gress\x18\x01 \x03(\x0b\x32\x1f.agentmesh.v1.EgressDestination\"f\n\x0cKeysResponse\x12\x13\n\x0bpublic_keys\x18\x01 \x03(\x0c\x12-\n\tsign_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x12\n\nsignatures\x18\x03 \x03(\x0c\"k\n\x13TokenRefreshRequest\x12\x1b\n\x13\x63hallenge_signature\x18\x01 \x01(\x0c\x12\x19\n\x11\x63hallenge_unix_ms\x18\x02 \x01(\x03\x12\x0f\n\x07peer_id\x18\x03 \x01(\t\x12\x0b\n\x03jwt\x18\x04 \x01(\t\"u\n\x14TokenRefreshResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x15\n\rerror_message\x18\x03 \x01(\t\"@\n\x11NodeCatalogReport\x12+\n\x08services\x18\x01 \x03(\x0b\x32\x19.agentmesh.v1.ServiceInfo\"%\n\x12TokenRevokeRequest\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\"5\n\x13TokenRevokeResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x96\x01\n\x1b\x42ootstrapTokenCreateRequest\x12\x0c\n\x04role\x18\x01 \x01(\t\x12\x10\n\x08owner_id\x18\x02 \x01(\t\x12\x11\n\tttl_hours\x18\x03 \x01(\x05\x12\x12\n\nmax_usages\x18\x04 \x01(\x05\x12\x13\n\x0b\x64\x65scription\x18\x05 \x01(\t\x12\x1b\n\x13\x61utonomous_recovery\x18\x06 \x01(\x08\"\x8a\x01\n\x1c\x42ootstrapTokenCreateResponse\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\t\x12\x0c\n\x04role\x18\x03 \x01(\t\x12\x10\n\x08owner_id\x18\x04 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xab\x02\n\x0e\x42ootstrapToken\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04role\x18\x02 \x01(\t\x12\x10\n\x08owner_id\x18\x03 \x01(\t\x12\x12\n\nmax_usages\x18\x04 \x01(\x05\x12\x14\n\x0cusages_count\x18\x05 \x01(\x05\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12/\n\x0b\x63reate_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0brevoke_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x1b\n\x13\x61utonomous_recovery\x18\n \x01(\x08\"J\n\x1a\x42ootstrapTokenListResponse\x12,\n\x06tokens\x18\x01 \x03(\x0b\x32\x1c.agentmesh.v1.BootstrapToken\"\xd6\x02\n\x11\x45nrollmentRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x10\n\x08token_id\x18\x03 \x01(\t\x12.\n\x06status\x18\x04 \x01(\x0e\x32\x1e.agentmesh.v1.EnrollmentStatus\x12;\n\x06labels\x18\x05 \x03(\x0b\x32+.agentmesh.v1.EnrollmentRequest.LabelsEntry\x12/\n\x0b\x63reate_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0cresolve_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x13\n\x0bresolved_by\x18\x08 \x01(\t\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"R\n\x1d\x45nrollmentRequestListResponse\x12\x31\n\x08requests\x18\x01 \x03(\x0b\x32\x1f.agentmesh.v1.EnrollmentRequest\"p\n\x04User\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0e\n\x06issuer\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12\x0c\n\x04role\x18\x04 \x01(\t\x12/\n\x0b\x63reate_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xe3\x02\n\x0c\x45nrolledNode\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0c\n\x04role\x18\x02 \x01(\t\x12\x17\n\x0f\x65nrollment_type\x18\x03 \x01(\t\x12\x13\n\x0b\x63laims_json\x18\x04 \x01(\t\x12\x10\n\x08owner_id\x18\x05 \x01(\t\x12\x36\n\x06labels\x18\x06 \x03(\x0b\x32&.agentmesh.v1.EnrolledNode.LabelsEntry\x12/\n\x0b\x65nroll_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06\x62\x61nned\x18\t \x01(\x08\x12\x1b\n\x13\x61utonomous_recovery\x18\n \x01(\x08\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc4\x01\n\x0bRouterLease\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x11\n\taddresses\x18\x02 \x03(\t\x12\x35\n\x11last_renewal_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x17\n\x0f\x63onnected_peers\x18\x05 \x03(\t\x12\x10\n\x08\x64ht_size\x18\x06 \x01(\x05\"l\n\x0cNodeServices\x12+\n\x08services\x18\x01 \x03(\x0b\x32\x19.agentmesh.v1.ServiceInfo\x12/\n\x0breport_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xdb\x03\n\x13\x41\x64minStatusResponse\x12!\n\x05users\x18\x01 \x03(\x0b\x32\x12.agentmesh.v1.User\x12\x31\n\x0e\x61\x63tive_routers\x18\x02 \x03(\x0b\x32\x19.agentmesh.v1.RouterLease\x12\x32\n\x0e\x65nrolled_nodes\x18\x03 \x03(\x0b\x32\x1a.agentmesh.v1.EnrolledNode\x12<\n\x13\x65nrollment_requests\x18\x04 \x03(\x0b\x32\x1f.agentmesh.v1.EnrollmentRequest\x12\x36\n\x10\x62ootstrap_tokens\x18\x05 \x03(\x0b\x32\x1c.agentmesh.v1.BootstrapToken\x12*\n\x06policy\x18\x06 \x01(\x0b\x32\x1a.agentmesh.v1.PolicyConfig\x12H\n\x0cnode_catalog\x18\x07 \x03(\x0b\x32\x32.agentmesh.v1.AdminStatusResponse.NodeCatalogEntry\x1aN\n\x10NodeCatalogEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.agentmesh.v1.NodeServices:\x02\x38\x01\"\x81\x02\n\x12UserStatusResponse\x12 \n\x04user\x18\x01 \x01(\x0b\x32\x12.agentmesh.v1.User\x12\x32\n\x0e\x65nrolled_nodes\x18\x02 \x03(\x0b\x32\x1a.agentmesh.v1.EnrolledNode\x12\x36\n\x10\x62ootstrap_tokens\x18\x03 \x03(\x0b\x32\x1c.agentmesh.v1.BootstrapToken\x12\x31\n\x0e\x61\x63tive_routers\x18\x04 \x03(\x0b\x32\x19.agentmesh.v1.RouterLease\x12*\n\x06policy\x18\x05 \x01(\x0b\x32\x1a.agentmesh.v1.PolicyConfig\"\xe4\x01\n\x18IdentityEvidenceResponse\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12\x37\n\x13\x62iscuit_expire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x19\n\x11\x63ontrol_plane_url\x18\x04 \x01(\t\x12\"\n\x1atrusted_control_plane_keys\x18\x05 \x03(\x0c\x12.\n\ncheck_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xc6\x02\n\x14PeerEvidenceResponse\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12\x15\n\rverifying_key\x18\x03 \x01(\x0c\x12\r\n\x05roles\x18\x04 \x03(\t\x12>\n\x06labels\x18\x05 \x03(\x0b\x32..agentmesh.v1.PeerEvidenceResponse.LabelsEntry\x12/\n\x0b\x65xpire_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0erevocation_ids\x18\x07 \x03(\t\x12.\n\ncheck_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8c\x02\n\x10MemberCredential\x12\x19\n\x11\x63ontrol_plane_url\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x35\n\x0ctrusted_keys\x18\x04 \x03(\x0b\x32\x1f.agentmesh.v1.TrustedSigningKey\x12\x19\n\x11issued_under_keys\x18\x05 \x03(\x0c\x12\x18\n\x10router_addresses\x18\x06 \x03(\t\x12/\n\x0coidc_session\x18\x07 \x01(\x0b\x32\x19.agentmesh.v1.OIDCSession\"Y\n\x11TrustedSigningKey\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\x12\x30\n\x0creceive_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"Y\n\x0bOIDCSession\x12\x0e\n\x06issuer\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x15\n\rrefresh_token\x18\x04 \x01(\t\"\x93\x01\n\x15TaskAuthorizationRule\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12%\n\x05rules\x18\x03 \x03(\x0b\x32\x16.agentmesh.v1.TaskRule\x12/\n\x0b\x65xpire_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x84\x01\n\x08TaskRule\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x18\n\x10\x61llowed_services\x18\x02 \x03(\t\x12.\n\toperation\x18\x03 \x01(\x0b\x32\x1b.agentmesh.v1.TaskOperation\x12\x19\n\x11\x61llowed_resources\x18\x04 \x03(\t\"s\n\rTaskOperation\x12\x15\n\rallowed_tools\x18\x01 \x03(\t\x12\x17\n\x0f\x61llowed_methods\x18\x02 \x03(\t\x12\x15\n\rallowed_paths\x18\x03 \x03(\t\x12\x1b\n\x13\x61llowed_permissions\x18\x04 \x03(\t\"\xab\x01\n\x14TokenExchangeRequest\x12\x15\n\rsubject_token\x18\x01 \x01(\t\x12\x36\n\ttask_rule\x18\x02 \x01(\x0b\x32#.agentmesh.v1.TaskAuthorizationRule\x12\x0c\n\x04seal\x18\x03 \x01(\x08\x12\x19\n\x11\x63hallenge_unix_ms\x18\x04 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x05 \x01(\x0c\"\x7f\n\x15TokenExchangeResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\r\n\x05roles\x18\x03 \x03(\t\x12\x0f\n\x07subject\x18\x04 \x01(\t\"\x81\x01\n\x0fSTSTokenRequest\x12\x0f\n\x07\x62iscuit\x18\x01 \x01(\x0c\x12\x13\n\x0b\x64\x65stination\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x19\n\x11\x63hallenge_unix_ms\x18\x04 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x05 \x01(\x0c\"\x83\x01\n\x10STSTokenResponse\x12\x0b\n\x03jwt\x18\x01 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0f\n\x07subject\x18\x03 \x01(\t\x12\r\n\x05roles\x18\x04 \x03(\t\x12\x11\n\ttask_name\x18\x05 \x01(\t\"F\n\x13RevocationsResponse\x12\x16\n\x0erevocation_ids\x18\x01 \x03(\t\x12\x17\n\x0f\x62\x61nned_peer_ids\x18\x02 \x03(\t*\x94\x01\n\x10\x45nrollmentStatus\x12!\n\x1d\x45NROLLMENT_STATUS_UNSPECIFIED\x10\x00\x12\x1d\n\x19\x45NROLLMENT_STATUS_PENDING\x10\x01\x12\x1e\n\x1a\x45NROLLMENT_STATUS_APPROVED\x10\x02\x12\x1e\n\x1a\x45NROLLMENT_STATUS_REJECTED\x10\x03*\x8c\x01\n\x0bServiceType\x12\x1c\n\x18SERVICE_TYPE_UNSPECIFIED\x10\x00\x12\x14\n\x10SERVICE_TYPE_MCP\x10\x01\x12\x1a\n\x16SERVICE_TYPE_INFERENCE\x10\x02\x12\x14\n\x10SERVICE_TYPE_A2A\x10\x03\x12\x17\n\x13SERVICE_TYPE_EGRESS\x10\x04*7\n\nEgressMode\x12\x14\n\x10\x45GRESS_MODE_HTTP\x10\x00\x12\x13\n\x0f\x45GRESS_MODE_TCP\x10\x01*\\\n\x12ResponseInspection\x12 \n\x1cRESPONSE_INSPECTION_BUFFERED\x10\x00\x12$\n RESPONSE_INSPECTION_REQUEST_ONLY\x10\x01\x42!Z\x1fgithub.com/google/agentmesh/apib\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'agentmesh_pb2', globals()) @@ -39,14 +39,14 @@ _ADMINSTATUSRESPONSE_NODECATALOGENTRY._serialized_options = b'8\001' _PEEREVIDENCERESPONSE_LABELSENTRY._options = None _PEEREVIDENCERESPONSE_LABELSENTRY._serialized_options = b'8\001' - _ENROLLMENTSTATUS._serialized_start=11453 - _ENROLLMENTSTATUS._serialized_end=11601 - _SERVICETYPE._serialized_start=11604 - _SERVICETYPE._serialized_end=11744 - _EGRESSMODE._serialized_start=11746 - _EGRESSMODE._serialized_end=11801 - _RESPONSEINSPECTION._serialized_start=11803 - _RESPONSEINSPECTION._serialized_end=11895 + _ENROLLMENTSTATUS._serialized_start=11478 + _ENROLLMENTSTATUS._serialized_end=11626 + _SERVICETYPE._serialized_start=11629 + _SERVICETYPE._serialized_end=11769 + _EGRESSMODE._serialized_start=11771 + _EGRESSMODE._serialized_end=11826 + _RESPONSEINSPECTION._serialized_start=11828 + _RESPONSEINSPECTION._serialized_end=11920 _AUTHFRAME._serialized_start=98 _AUTHFRAME._serialized_end=150 _AUTHRESPONSE._serialized_start=152 @@ -108,115 +108,115 @@ _ROUTERLEASERESPONSE._serialized_start=3564 _ROUTERLEASERESPONSE._serialized_end=3666 _POLICYROLE._serialized_start=3669 - _POLICYROLE._serialized_end=3833 - _HTTPGRANT._serialized_start=3835 - _HTTPGRANT._serialized_end=3895 - _EGRESSDESTINATION._serialized_start=3898 - _EGRESSDESTINATION._serialized_end=4187 - _INSPECTION._serialized_start=4189 - _INSPECTION._serialized_end=4246 - _INSPECTOR._serialized_start=4248 - _INSPECTOR._serialized_end=4359 - _MODELARMOR._serialized_start=4362 - _MODELARMOR._serialized_end=4507 - _EXTPROC._serialized_start=4510 - _EXTPROC._serialized_end=4774 - _EXTPROCPROCESSINGMODE._serialized_start=4777 - _EXTPROCPROCESSINGMODE._serialized_end=5416 - _EXTPROCPROCESSINGMODE_HEADERMODE._serialized_start=5261 - _EXTPROCPROCESSINGMODE_HEADERMODE._serialized_end=5318 - _EXTPROCPROCESSINGMODE_BODYMODE._serialized_start=5320 - _EXTPROCPROCESSINGMODE_BODYMODE._serialized_end=5416 - _CREDENTIALBROKER._serialized_start=5419 - _CREDENTIALBROKER._serialized_end=5644 - _OIDCFEDERATION._serialized_start=5646 - _OIDCFEDERATION._serialized_end=5741 - _AWSASSUMEROLE._serialized_start=5743 - _AWSASSUMEROLE._serialized_end=5800 - _PLATFORMIDENTITY._serialized_start=5802 - _PLATFORMIDENTITY._serialized_end=5836 - _POLICYBINDING._serialized_start=5838 - _POLICYBINDING._serialized_end=5884 - _POLICYCONFIG._serialized_start=5887 - _POLICYCONFIG._serialized_end=6038 - _POLICYCONFIGGETREQUEST._serialized_start=6040 - _POLICYCONFIGGETREQUEST._serialized_end=6064 - _POLICYCONFIGGETRESPONSE._serialized_start=6066 - _POLICYCONFIGGETRESPONSE._serialized_end=6114 - _POLICYCONFIGUPDATERESPONSE._serialized_start=6116 - _POLICYCONFIGUPDATERESPONSE._serialized_end=6176 - _EGRESSASSIGNMENTSREQUEST._serialized_start=6178 - _EGRESSASSIGNMENTSREQUEST._serialized_end=6204 - _EGRESSASSIGNMENTSRESPONSE._serialized_start=6206 - _EGRESSASSIGNMENTSRESPONSE._serialized_end=6282 - _KEYSRESPONSE._serialized_start=6284 - _KEYSRESPONSE._serialized_end=6386 - _TOKENREFRESHREQUEST._serialized_start=6388 - _TOKENREFRESHREQUEST._serialized_end=6495 - _TOKENREFRESHRESPONSE._serialized_start=6497 - _TOKENREFRESHRESPONSE._serialized_end=6614 - _NODECATALOGREPORT._serialized_start=6616 - _NODECATALOGREPORT._serialized_end=6680 - _TOKENREVOKEREQUEST._serialized_start=6682 - _TOKENREVOKEREQUEST._serialized_end=6719 - _TOKENREVOKERESPONSE._serialized_start=6721 - _TOKENREVOKERESPONSE._serialized_end=6774 - _BOOTSTRAPTOKENCREATEREQUEST._serialized_start=6777 - _BOOTSTRAPTOKENCREATEREQUEST._serialized_end=6927 - _BOOTSTRAPTOKENCREATERESPONSE._serialized_start=6930 - _BOOTSTRAPTOKENCREATERESPONSE._serialized_end=7068 - _BOOTSTRAPTOKEN._serialized_start=7071 - _BOOTSTRAPTOKEN._serialized_end=7370 - _BOOTSTRAPTOKENLISTRESPONSE._serialized_start=7372 - _BOOTSTRAPTOKENLISTRESPONSE._serialized_end=7446 - _ENROLLMENTREQUEST._serialized_start=7449 - _ENROLLMENTREQUEST._serialized_end=7791 + _POLICYROLE._serialized_end=3858 + _HTTPGRANT._serialized_start=3860 + _HTTPGRANT._serialized_end=3920 + _EGRESSDESTINATION._serialized_start=3923 + _EGRESSDESTINATION._serialized_end=4212 + _INSPECTION._serialized_start=4214 + _INSPECTION._serialized_end=4271 + _INSPECTOR._serialized_start=4273 + _INSPECTOR._serialized_end=4384 + _MODELARMOR._serialized_start=4387 + _MODELARMOR._serialized_end=4532 + _EXTPROC._serialized_start=4535 + _EXTPROC._serialized_end=4799 + _EXTPROCPROCESSINGMODE._serialized_start=4802 + _EXTPROCPROCESSINGMODE._serialized_end=5441 + _EXTPROCPROCESSINGMODE_HEADERMODE._serialized_start=5286 + _EXTPROCPROCESSINGMODE_HEADERMODE._serialized_end=5343 + _EXTPROCPROCESSINGMODE_BODYMODE._serialized_start=5345 + _EXTPROCPROCESSINGMODE_BODYMODE._serialized_end=5441 + _CREDENTIALBROKER._serialized_start=5444 + _CREDENTIALBROKER._serialized_end=5669 + _OIDCFEDERATION._serialized_start=5671 + _OIDCFEDERATION._serialized_end=5766 + _AWSASSUMEROLE._serialized_start=5768 + _AWSASSUMEROLE._serialized_end=5825 + _PLATFORMIDENTITY._serialized_start=5827 + _PLATFORMIDENTITY._serialized_end=5861 + _POLICYBINDING._serialized_start=5863 + _POLICYBINDING._serialized_end=5909 + _POLICYCONFIG._serialized_start=5912 + _POLICYCONFIG._serialized_end=6063 + _POLICYCONFIGGETREQUEST._serialized_start=6065 + _POLICYCONFIGGETREQUEST._serialized_end=6089 + _POLICYCONFIGGETRESPONSE._serialized_start=6091 + _POLICYCONFIGGETRESPONSE._serialized_end=6139 + _POLICYCONFIGUPDATERESPONSE._serialized_start=6141 + _POLICYCONFIGUPDATERESPONSE._serialized_end=6201 + _EGRESSASSIGNMENTSREQUEST._serialized_start=6203 + _EGRESSASSIGNMENTSREQUEST._serialized_end=6229 + _EGRESSASSIGNMENTSRESPONSE._serialized_start=6231 + _EGRESSASSIGNMENTSRESPONSE._serialized_end=6307 + _KEYSRESPONSE._serialized_start=6309 + _KEYSRESPONSE._serialized_end=6411 + _TOKENREFRESHREQUEST._serialized_start=6413 + _TOKENREFRESHREQUEST._serialized_end=6520 + _TOKENREFRESHRESPONSE._serialized_start=6522 + _TOKENREFRESHRESPONSE._serialized_end=6639 + _NODECATALOGREPORT._serialized_start=6641 + _NODECATALOGREPORT._serialized_end=6705 + _TOKENREVOKEREQUEST._serialized_start=6707 + _TOKENREVOKEREQUEST._serialized_end=6744 + _TOKENREVOKERESPONSE._serialized_start=6746 + _TOKENREVOKERESPONSE._serialized_end=6799 + _BOOTSTRAPTOKENCREATEREQUEST._serialized_start=6802 + _BOOTSTRAPTOKENCREATEREQUEST._serialized_end=6952 + _BOOTSTRAPTOKENCREATERESPONSE._serialized_start=6955 + _BOOTSTRAPTOKENCREATERESPONSE._serialized_end=7093 + _BOOTSTRAPTOKEN._serialized_start=7096 + _BOOTSTRAPTOKEN._serialized_end=7395 + _BOOTSTRAPTOKENLISTRESPONSE._serialized_start=7397 + _BOOTSTRAPTOKENLISTRESPONSE._serialized_end=7471 + _ENROLLMENTREQUEST._serialized_start=7474 + _ENROLLMENTREQUEST._serialized_end=7816 _ENROLLMENTREQUEST_LABELSENTRY._serialized_start=825 _ENROLLMENTREQUEST_LABELSENTRY._serialized_end=870 - _ENROLLMENTREQUESTLISTRESPONSE._serialized_start=7793 - _ENROLLMENTREQUESTLISTRESPONSE._serialized_end=7875 - _USER._serialized_start=7877 - _USER._serialized_end=7989 - _ENROLLEDNODE._serialized_start=7992 - _ENROLLEDNODE._serialized_end=8347 + _ENROLLMENTREQUESTLISTRESPONSE._serialized_start=7818 + _ENROLLMENTREQUESTLISTRESPONSE._serialized_end=7900 + _USER._serialized_start=7902 + _USER._serialized_end=8014 + _ENROLLEDNODE._serialized_start=8017 + _ENROLLEDNODE._serialized_end=8372 _ENROLLEDNODE_LABELSENTRY._serialized_start=825 _ENROLLEDNODE_LABELSENTRY._serialized_end=870 - _ROUTERLEASE._serialized_start=8350 - _ROUTERLEASE._serialized_end=8546 - _NODESERVICES._serialized_start=8548 - _NODESERVICES._serialized_end=8656 - _ADMINSTATUSRESPONSE._serialized_start=8659 - _ADMINSTATUSRESPONSE._serialized_end=9134 - _ADMINSTATUSRESPONSE_NODECATALOGENTRY._serialized_start=9056 - _ADMINSTATUSRESPONSE_NODECATALOGENTRY._serialized_end=9134 - _USERSTATUSRESPONSE._serialized_start=9137 - _USERSTATUSRESPONSE._serialized_end=9394 - _IDENTITYEVIDENCERESPONSE._serialized_start=9397 - _IDENTITYEVIDENCERESPONSE._serialized_end=9625 - _PEEREVIDENCERESPONSE._serialized_start=9628 - _PEEREVIDENCERESPONSE._serialized_end=9954 + _ROUTERLEASE._serialized_start=8375 + _ROUTERLEASE._serialized_end=8571 + _NODESERVICES._serialized_start=8573 + _NODESERVICES._serialized_end=8681 + _ADMINSTATUSRESPONSE._serialized_start=8684 + _ADMINSTATUSRESPONSE._serialized_end=9159 + _ADMINSTATUSRESPONSE_NODECATALOGENTRY._serialized_start=9081 + _ADMINSTATUSRESPONSE_NODECATALOGENTRY._serialized_end=9159 + _USERSTATUSRESPONSE._serialized_start=9162 + _USERSTATUSRESPONSE._serialized_end=9419 + _IDENTITYEVIDENCERESPONSE._serialized_start=9422 + _IDENTITYEVIDENCERESPONSE._serialized_end=9650 + _PEEREVIDENCERESPONSE._serialized_start=9653 + _PEEREVIDENCERESPONSE._serialized_end=9979 _PEEREVIDENCERESPONSE_LABELSENTRY._serialized_start=825 _PEEREVIDENCERESPONSE_LABELSENTRY._serialized_end=870 - _MEMBERCREDENTIAL._serialized_start=9957 - _MEMBERCREDENTIAL._serialized_end=10225 - _TRUSTEDSIGNINGKEY._serialized_start=10227 - _TRUSTEDSIGNINGKEY._serialized_end=10316 - _OIDCSESSION._serialized_start=10318 - _OIDCSESSION._serialized_end=10407 - _TASKAUTHORIZATIONRULE._serialized_start=10410 - _TASKAUTHORIZATIONRULE._serialized_end=10557 - _TASKRULE._serialized_start=10560 - _TASKRULE._serialized_end=10692 - _TASKOPERATION._serialized_start=10694 - _TASKOPERATION._serialized_end=10809 - _TOKENEXCHANGEREQUEST._serialized_start=10812 - _TOKENEXCHANGEREQUEST._serialized_end=10983 - _TOKENEXCHANGERESPONSE._serialized_start=10985 - _TOKENEXCHANGERESPONSE._serialized_end=11112 - _STSTOKENREQUEST._serialized_start=11115 - _STSTOKENREQUEST._serialized_end=11244 - _STSTOKENRESPONSE._serialized_start=11247 - _STSTOKENRESPONSE._serialized_end=11378 - _REVOCATIONSRESPONSE._serialized_start=11380 - _REVOCATIONSRESPONSE._serialized_end=11450 + _MEMBERCREDENTIAL._serialized_start=9982 + _MEMBERCREDENTIAL._serialized_end=10250 + _TRUSTEDSIGNINGKEY._serialized_start=10252 + _TRUSTEDSIGNINGKEY._serialized_end=10341 + _OIDCSESSION._serialized_start=10343 + _OIDCSESSION._serialized_end=10432 + _TASKAUTHORIZATIONRULE._serialized_start=10435 + _TASKAUTHORIZATIONRULE._serialized_end=10582 + _TASKRULE._serialized_start=10585 + _TASKRULE._serialized_end=10717 + _TASKOPERATION._serialized_start=10719 + _TASKOPERATION._serialized_end=10834 + _TOKENEXCHANGEREQUEST._serialized_start=10837 + _TOKENEXCHANGEREQUEST._serialized_end=11008 + _TOKENEXCHANGERESPONSE._serialized_start=11010 + _TOKENEXCHANGERESPONSE._serialized_end=11137 + _STSTOKENREQUEST._serialized_start=11140 + _STSTOKENREQUEST._serialized_end=11269 + _STSTOKENRESPONSE._serialized_start=11272 + _STSTOKENRESPONSE._serialized_end=11403 + _REVOCATIONSRESPONSE._serialized_start=11405 + _REVOCATIONSRESPONSE._serialized_end=11475 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.pyi b/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.pyi index 26bc72f6..8ddc53b0 100644 --- a/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.pyi +++ b/sdk/python/src/agent_mesh/_proto/agentmesh_pb2.pyi @@ -698,20 +698,22 @@ class PolicyConfigUpdateResponse(_message.Message): def __init__(self, success: bool = ..., error: _Optional[str] = ...) -> None: ... class PolicyRole(_message.Message): - __slots__ = ["allowed_labels", "allowed_services", "allowed_targets", "custom_datalog", "http", "name"] + __slots__ = ["allowed_labels", "allowed_publish", "allowed_services", "allowed_targets", "custom_datalog", "http", "name"] ALLOWED_LABELS_FIELD_NUMBER: _ClassVar[int] + ALLOWED_PUBLISH_FIELD_NUMBER: _ClassVar[int] ALLOWED_SERVICES_FIELD_NUMBER: _ClassVar[int] ALLOWED_TARGETS_FIELD_NUMBER: _ClassVar[int] CUSTOM_DATALOG_FIELD_NUMBER: _ClassVar[int] HTTP_FIELD_NUMBER: _ClassVar[int] NAME_FIELD_NUMBER: _ClassVar[int] allowed_labels: _containers.RepeatedScalarFieldContainer[str] + allowed_publish: _containers.RepeatedScalarFieldContainer[str] allowed_services: _containers.RepeatedScalarFieldContainer[str] allowed_targets: _containers.RepeatedScalarFieldContainer[str] custom_datalog: _containers.RepeatedScalarFieldContainer[str] http: _containers.RepeatedCompositeFieldContainer[HTTPGrant] name: str - def __init__(self, name: _Optional[str] = ..., allowed_targets: _Optional[_Iterable[str]] = ..., allowed_services: _Optional[_Iterable[str]] = ..., custom_datalog: _Optional[_Iterable[str]] = ..., allowed_labels: _Optional[_Iterable[str]] = ..., http: _Optional[_Iterable[_Union[HTTPGrant, _Mapping]]] = ...) -> None: ... + def __init__(self, name: _Optional[str] = ..., allowed_targets: _Optional[_Iterable[str]] = ..., allowed_services: _Optional[_Iterable[str]] = ..., custom_datalog: _Optional[_Iterable[str]] = ..., allowed_labels: _Optional[_Iterable[str]] = ..., http: _Optional[_Iterable[_Union[HTTPGrant, _Mapping]]] = ..., allowed_publish: _Optional[_Iterable[str]] = ...) -> None: ... class RegisterServiceRequest(_message.Message): __slots__ = ["command", "service", "target_url"] diff --git a/sdk/python/src/agent_mesh/biscuit.py b/sdk/python/src/agent_mesh/biscuit.py index 043ac456..279a818e 100644 --- a/sdk/python/src/agent_mesh/biscuit.py +++ b/sdk/python/src/agent_mesh/biscuit.py @@ -57,6 +57,9 @@ class VerifiedBiscuit: labels: dict[str, str] = field(default_factory=dict) task_rules: list[agentmesh_pb2.TaskAuthorizationRule] = field(default_factory=list) actor_node: Optional[str] = None + # The token as received, for what is decided over its facts after + # verification (require_publish). + token: bytes = b"" def _limits() -> ba.AuthorizerLimits: @@ -157,6 +160,7 @@ def strings(rule: str) -> list[str]: labels=labels, task_rules=task_rules, actor_node=actor_nodes[0] if actor_nodes else None, + token=bytes(biscuit), ) @@ -296,3 +300,26 @@ def require_role(verified: VerifiedBiscuit, role: str) -> None: if role not in verified.roles: raise BiscuitVerificationError(f"biscuit lacks expected role {role!r}") + +def require_publish(verified: VerifiedBiscuit, target_service: str) -> None: + """Requires that an already verified provider credential grants serving + target_service ("://"), as identity.RequirePublish: the + directory names whoever announced the service, only the credential says + who may serve it. The provider's own catalog names no service and needs + no grant; so does an empty target.""" + if target_service == "" or target_service == f"system://{_DATALOG['catalog_target']}": + return + scheme, sep, name = target_service.partition("://") + if not sep or not scheme or not name: + raise BiscuitVerificationError(f"service {target_service!r} must be ://") + token = ba.Biscuit.from_bytes(verified.token, ba.PublicKey.from_bytes(bytes(verified.verifying_key), ba.Algorithm.Ed25519)) + builder = ba.AuthorizerBuilder() + builder.set_limits(_limits()) + builder.add_fact(ba.Fact(_DATALOG["fact_publish"] + "({t}, {n})", {"t": scheme, "n": name})) + for policy in _DATALOG["publish_policies"]: + builder.add_policy(ba.Policy(policy)) + try: + builder.build(token).authorize() + except Exception as err: # noqa: BLE001 - no policy matched + raise BiscuitVerificationError(f"peer {verified.peer_id} is not granted serving {target_service}") from err + diff --git a/sdk/python/src/agent_mesh/httpx_transport.py b/sdk/python/src/agent_mesh/httpx_transport.py index 7ba9d078..286adf8a 100644 --- a/sdk/python/src/agent_mesh/httpx_transport.py +++ b/sdk/python/src/agent_mesh/httpx_transport.py @@ -40,9 +40,10 @@ _REQUEST_TIMEOUT = 60.0 -def split_mesh_url(url: httpx.URL) -> tuple[str, str]: - """(peer ID, request target) of a mesh URL; the target is the - ///? the peer's ingress takes.""" +def split_mesh_url(url: httpx.URL) -> tuple[str, str, str]: + """(peer ID, service, request target) of a mesh URL: the service is + "://", the target the ///? the + peer's ingress takes.""" raw = url.raw_path.decode("latin-1") path, _, query = raw.partition("?") if not path.startswith(MESH_PATH_PREFIX): @@ -51,7 +52,7 @@ def split_mesh_url(url: httpx.URL) -> tuple[str, str]: parts = rest.split("/") if not peer_id or len(parts) < 2 or not parts[0] or not parts[1]: raise httpx.UnsupportedProtocol(f"a mesh URL names a peer, a service type and a name, got {url}") - return peer_id, "/" + rest + (f"?{query}" if query else "") + return peer_id, f"{parts[0]}://{parts[1]}", "/" + rest + (f"?{query}" if query else "") class _BodyStream(httpx.AsyncByteStream): @@ -76,8 +77,8 @@ def __init__(self, session: "MeshSession", *, timeout: float = _REQUEST_TIMEOUT) self._timeout = timeout async def handle_async_request(self, request: httpx.Request) -> httpx.Response: - peer_text, target = split_mesh_url(request.url) - peer_id = await self._session._egress_peer(peer_text) # noqa: SLF001 - the session's verified egress path, not a caller option + peer_text, service, target = split_mesh_url(request.url) + peer_id = await self._session._egress_peer(peer_text, service) # noqa: SLF001 - the session's verified egress path, not a caller option body = await request.aread() headers = {k.decode("latin-1"): v.decode("latin-1") for k, v in request.headers.raw} biscuit = getattr(self._session, "biscuit", None) or self._session.mesh.credential.biscuit diff --git a/sdk/python/src/agent_mesh/mcp_client.py b/sdk/python/src/agent_mesh/mcp_client.py index b60b486a..3080efc2 100644 --- a/sdk/python/src/agent_mesh/mcp_client.py +++ b/sdk/python/src/agent_mesh/mcp_client.py @@ -40,7 +40,7 @@ AuthRejectedError, read_bounded_varint_prefixed_bytes, ) -from .biscuit import BiscuitVerificationError, VerifiedBiscuit, require_role, verify_peer_biscuit +from .biscuit import BiscuitVerificationError, VerifiedBiscuit, require_publish, require_role, verify_peer_biscuit from .controlplane import ROLE_NODE from .host import open_stream @@ -132,6 +132,8 @@ async def open_mcp_session( provider = verify_peer_biscuit(resp.biscuit, str(peer_id), trusted_keys) # Only nodes host services; a router's or an admin's credential is a member, not a provider. require_role(provider, ROLE_NODE) + # And only a node the policy lets serve the named service is one. + require_publish(provider, pb.AuthFrame.FromString(frame).target_service) except BiscuitVerificationError as err: raise AuthRejectedError(str(peer_id), f"provider credential rejected: {err}") from err require_labels(provider, required_labels) diff --git a/sdk/python/src/agent_mesh/session.py b/sdk/python/src/agent_mesh/session.py index 2959d0aa..9d2a74aa 100644 --- a/sdk/python/src/agent_mesh/session.py +++ b/sdk/python/src/agent_mesh/session.py @@ -45,7 +45,7 @@ go_away_stream_handler, ) from .authorizer import ProviderAuthorizerOptions -from .biscuit import ROLE_ROUTER, VerifiedBiscuit, attenuate_biscuit, require_role, seal_biscuit +from .biscuit import ROLE_ROUTER, VerifiedBiscuit, attenuate_biscuit, require_publish, require_role, seal_biscuit from .controlplane import ROLE_NODE from .credential import encode_auth_frame from .discovery import DiscoveredProvider, find_peer, find_providers, parse_service_target, peer_key, provide, service_key, withdraw @@ -998,22 +998,25 @@ async def request( ) -> HTTPResponse: """Calls an inference or A2A service on a provider over /libp2p-http, the way agentmesh-node's egress proxy does for /mesh////.""" - peer_id = await self._egress_peer(peer) + peer_id = await self._egress_peer(peer, target_service) return await http_request_over_stream( self.host, peer_id, self.biscuit, target_service, path, method=method, headers=headers, body=body ) - async def _egress_peer(self, peer: Peer) -> ID: + async def _egress_peer(self, peer: Peer, target_service: str) -> ID: """The peer an HTTP call goes out to, verified as an enrolled node - holding the floor before anything is sent (agentmesh-node's VerifyPeerLabels).""" + holding the floor and granted the service before anything is sent + (agentmesh-node's VerifyProvider).""" peer_id = await self.connect(peer) - if self._egress_verdicts.get(str(peer_id), 0.0) > time.monotonic(): + verdict = f"{peer_id}|{target_service}" + if self._egress_verdicts.get(verdict, 0.0) > time.monotonic(): return peer_id provider = await authenticate_with_peer(self.host, peer_id, self.mesh.auth_frame(), self.mesh.credential.control_plane_keys) # Only nodes host services; a router's or an admin's credential is a member, not a provider. require_role(provider, ROLE_NODE) + require_publish(provider, target_service) require_egress_labels(provider, self.egress_require_labels) - self._egress_verdicts[str(peer_id)] = time.monotonic() + EGRESS_VERDICT_TTL + self._egress_verdicts[verdict] = time.monotonic() + EGRESS_VERDICT_TTL return peer_id async def accept_a2a(self, target: Union[str, HTTPHandler], *, name: str = DEFAULT_A2A_NAME) -> str: diff --git a/sdk/python/tests/test_libp2p_http.py b/sdk/python/tests/test_libp2p_http.py index 2af740dd..e138c7e5 100644 --- a/sdk/python/tests/test_libp2p_http.py +++ b/sdk/python/tests/test_libp2p_http.py @@ -186,7 +186,7 @@ def __init__(self, host, biscuit: bytes) -> None: self.host = host self.mesh = _Mesh(_Credential(biscuit)) - async def _egress_peer(self, peer) -> ID: + async def _egress_peer(self, peer, target_service) -> ID: return ID.from_base58(str(peer)) @@ -410,8 +410,8 @@ async def with_timeout(): def test_mesh_urls_name_a_peer_and_a_service(): peer = "12D3KooWJ2Yhy3CKwVPDw7AnkxN5HbZbb65xDoRif54hdXXUzh1U" - assert split_mesh_url(httpx.URL(f"http://mesh/mesh/{peer}/a2a/agent")) == (peer, "/a2a/agent") - assert split_mesh_url(httpx.URL(f"http://anything:1/mesh/{peer}/inference/llm/v1/models?x=1")) == (peer, "/inference/llm/v1/models?x=1") + assert split_mesh_url(httpx.URL(f"http://mesh/mesh/{peer}/a2a/agent")) == (peer, "a2a://agent", "/a2a/agent") + assert split_mesh_url(httpx.URL(f"http://anything:1/mesh/{peer}/inference/llm/v1/models?x=1")) == (peer, "inference://llm", "/inference/llm/v1/models?x=1") for bad in ("http://mesh/a2a/agent", f"http://mesh/mesh/{peer}", f"http://mesh/mesh/{peer}/a2a", f"http://mesh/mesh//a2a/agent"): with pytest.raises(httpx.UnsupportedProtocol): split_mesh_url(httpx.URL(bad)) diff --git a/sdk/python/tests/test_mcp.py b/sdk/python/tests/test_mcp.py index 64966a76..9325feae 100644 --- a/sdk/python/tests/test_mcp.py +++ b/sdk/python/tests/test_mcp.py @@ -44,13 +44,27 @@ FIXTURE = json.loads((Path(__file__).resolve().parents[2] / "testdata" / "service_keys.json").read_text()) -def mint(peer_id: str, role: str, labels: dict[str, str] | None = None) -> bytes: +def mint(peer_id: str, role: str, labels: dict[str, str] | None = None, publish: tuple[str, ...] = ("mcp://calc",)) -> bytes: + """A credential as the control plane issues one to a member whose role + grants publishing the given services (allowed_publish syntax, as + granted_publish_* facts); a provider's needs the grant for what it serves.""" code = "node({p}); expiration(2035-01-01T00:00:00Z); role({r});" params = {"p": peer_id, "r": role} for i, (k, v) in enumerate((labels or {}).items()): code += f" label({{k{i}}}, {{v{i}}});" params[f"k{i}"] = k params[f"v{i}"] = v + for i, svc in enumerate(publish): + scheme, sep, name = svc.partition("://") + if not sep: + code += " granted_publish_all_types(true);" + elif name == "*": + code += f" granted_publish_all({{pt{i}}});" + params[f"pt{i}"] = scheme + else: + code += f" granted_publish_exact({{pt{i}}}, {{pn{i}}});" + params[f"pt{i}"] = scheme + params[f"pn{i}"] = name return ba.BiscuitBuilder(code, params).build(CP.private_key).to_bytes() @@ -129,10 +143,10 @@ async def pump_out(): return handle -async def start_provider(nursery, labels=None, role=ROLE_NODE): +async def start_provider(nursery, labels=None, role=ROLE_NODE, publish=("mcp://calc",)): identity = Identity.generate() host = libp2p_host(identity) - biscuit = mint(identity.peer_id, role, labels) + biscuit = mint(identity.peer_id, role, labels, publish) served: list[str] = [] host.set_stream_handler(MCP_PROTOCOL, mcp_stream_handler(biscuit, served)) started = trio.Event() @@ -235,6 +249,23 @@ async def main(): with pytest.raises(AuthRejectedError, match="lacks expected role 'mesh:role:node'"): async with open_mcp_session(caller, router.get_id(), frame(caller_biscuit, "mcp://calc"), [CP_KEY], egress_require_labels={"region": "eu"}): pass + + # Only a node granted the service is its provider, as agentmesh-node's + # checkProvider requires: the directory names whoever announced calc, + # the credential says who may serve it. A grant by type or for + # everything serves too; the provider's own catalog needs none. + for publish, ok in ((("mcp://weather",), False), ((), False), (("mcp://*",), True), (("*",), True)): + other, other_addr, _ = await start_provider(nursery, publish=publish) + await caller.connect(info_from_p2p_addr(other_addr)) + if ok: + async with open_mcp_session(caller, other.get_id(), frame(caller_biscuit, "mcp://calc"), [CP_KEY]): + pass + else: + with pytest.raises(AuthRejectedError, match="is not granted serving mcp://calc"): + async with open_mcp_session(caller, other.get_id(), frame(caller_biscuit, "mcp://calc"), [CP_KEY]): + pass + async with open_mcp_session(caller, other.get_id(), frame(caller_biscuit, ""), [CP_KEY]): + pass nursery.cancel_scope.cancel() async def with_timeout(): diff --git a/sdk/python/tests/test_session.py b/sdk/python/tests/test_session.py index e767d5ca..93f8dd3b 100644 --- a/sdk/python/tests/test_session.py +++ b/sdk/python/tests/test_session.py @@ -84,6 +84,9 @@ def _ts_s(seconds: int) -> Timestamp: def mint(peer_id: str, role: str, expiration: str = "2035-01-01T00:00:00Z", labels: dict[str, str] | None = None, signer: ba.KeyPair = CP) -> bytes: code = "node({p}); client_peer_id({p}); expiration(" + expiration + "); role({r});" + if role == ROLE_NODE: + # The node role of these meshes serves everything. + code += " granted_publish_all_types(true);" params = {"p": peer_id, "r": role} for i, (k, v) in enumerate((labels or {}).items()): code += f" label({{k{i}}}, {{v{i}}});" diff --git a/site/content/docs/concepts/authorization.md b/site/content/docs/concepts/authorization.md index 6f02aa0b..ae8347cf 100644 --- a/site/content/docs/concepts/authorization.md +++ b/site/content/docs/concepts/authorization.md @@ -25,8 +25,9 @@ A node publishes services, each with a type and a name: `mcp://calculator`, `inference://vllm-eu`, `a2a://triage`, `egress://bigquery.googleapis.com`, and the built-in `system://mesh.catalog` that answers discovery queries. -Standing mesh roles grant access to services by name (`allowed_services`) and -can narrow HTTP methods and paths (`http`). On top of standing roles, a +Standing mesh roles grant access to services by name (`allowed_services`), +grant the right to serve them (`allowed_publish`) and can narrow HTTP +methods and paths (`http`). On top of standing roles, a holder can append `TaskAuthorizationRule` blocks (`tar_block`) to scope a token for a single task or sub-agent hop—narrowing which services, MCP tools (`operation.allowed_tools`), HTTP methods and paths @@ -46,6 +47,7 @@ from a file (`--policy-file`). { "name": "developer", "allowed_services": ["mcp://code-reviewer", "mcp://build-runner.*", "inference://*"], + "allowed_publish": ["mcp://build-runner.*"], "allowed_targets": ["group:dev-nodes"], "allowed_labels": ["region=*"], "http": [ @@ -59,6 +61,11 @@ from a file (`--policy-file`). can stand for the whole name, for a leading component (`mcp://*.internal`) or for a trailing one (`mcp://build-runner.*`). A bare `*` means every service. +- `allowed_publish`: the services holders may serve, in the same syntax. + A member announces a service to the directory, and is accepted as its + provider by callers, only when its own credential grants it here. If + absent, holders may publish nothing. Calling and serving are granted + separately: a role that may call `mcp://calculator` does not serve it. - `allowed_targets`: the nodes holders may call, as facts about the destination node's identity: `node:`, `user:`, `email:`, `group:`, `idp_role:`, or `*`. If absent, any diff --git a/site/content/docs/concepts/networking.md b/site/content/docs/concepts/networking.md index 58b921b2..08462aec 100644 --- a/site/content/docs/concepts/networking.md +++ b/site/content/docs/concepts/networking.md @@ -137,10 +137,14 @@ and the node adds the `/v1` prefix. The same backend is therefore reachable through the `/v1` endpoint and, with `/v1/...` appended, through the proxy path. -A caller can restrict which providers are acceptable with -`X-Mesh-Required-Labels: key=value[,key=value]`. The node then verifies the -provider's credential and confirms that every listed label is attested in it -before forwarding. The header is removed before the request leaves the node. +Before any request leaves the node for a provider, the node verifies the +provider's credential and that its role grants serving the service the +request names (`allowed_publish`): the directory says who announced a +name, the credential says who may serve it. A caller can restrict which +providers are acceptable further with +`X-Mesh-Required-Labels: key=value[,key=value]`. The node then confirms +that every listed label is attested in the credential as well before +forwarding. The header is removed before the request leaves the node. An operator can set a floor that every provider must meet with `egress.require_labels` in the node configuration. diff --git a/site/content/docs/contributing/security-architecture.md b/site/content/docs/contributing/security-architecture.md index 244e50d1..ffb965b5 100644 --- a/site/content/docs/contributing/security-architecture.md +++ b/site/content/docs/contributing/security-architecture.md @@ -605,3 +605,14 @@ response directly). a router's routing table admits authenticated peer routers only. A directory lookup and a gossip subscription are mesh data, so this covers reads as much as writes. +8. A member serves only what its credential grants. `PolicyRole.allowed_publish` + is minted as `granted_publish_*` facts, the dual of `allowed_services`, and + `api.PublishPolicies` decide them over a `publish(type, name)` fact. The + router a member announces a directory record to checks the grant against + the credential it holds from the handshake and refuses the record otherwise + (`result="denied"`); every caller, `agentmesh-node` and both SDKs, checks it + on the provider's credential before sending a request for a named service, + so a record is never trusted for who may serve a name. A member's own record + under its peer ID and the catalog name no service. A delegated credential + grants publishing nothing. An egress destination's `served_by` is its + publish grant, minted with the destination. diff --git a/site/content/docs/guides/exposing-services.md b/site/content/docs/guides/exposing-services.md index 60e49c9e..c225d00c 100644 --- a/site/content/docs/guides/exposing-services.md +++ b/site/content/docs/guides/exposing-services.md @@ -147,21 +147,28 @@ must use the A2A 1.0 format. Older formats are refused. The ## Making it reachable -Declaring a service publishes it, but nobody can call it until the mesh -policy grants access. Two conditions must hold: - -1. The caller's identity resolves to a role whose `allowed_services` +Declaring a service is not enough on its own: the mesh policy has to let +this node serve it and let callers call it. Three conditions must hold: + +1. This node's identity resolves to a role whose `allowed_publish` + includes `mcp://calculator`, or a pattern that covers it. Without it the + router refuses the node's directory record and callers refuse the node + as a provider, whatever the node declares in its configuration: the + configuration says what the node offers, the policy says what the mesh + lets it serve. +2. The caller's identity resolves to a role whose `allowed_services` includes `mcp://calculator`, or a pattern that covers it. -2. If that role has `allowed_targets`, one of them matches this node's +3. If that role has `allowed_targets`, one of them matches this node's identity: its peer ID as `node:`, or a claim from the identity it enrolled with. -A policy that grants the service to a role bound to your users: +A policy that lets nodes serve the calculator and grants it to a role bound +to your users: ```json { "roles": [ - { "name": "mesh:role:node", "allowed_labels": ["region=*"] }, + { "name": "mesh:role:node", "allowed_publish": ["mcp://calculator"], "allowed_labels": ["region=*"] }, { "name": "calc-users", "allowed_services": ["mcp://calculator"] } ], "bindings": [ diff --git a/site/content/docs/guides/kubernetes.md b/site/content/docs/guides/kubernetes.md index 15b771ff..acbbbdc9 100644 --- a/site/content/docs/guides/kubernetes.md +++ b/site/content/docs/guides/kubernetes.md @@ -39,7 +39,8 @@ helm upgrade --install agentmesh ./charts/agentmesh \ --set controlPlane.workloadIssuer="$ISSUER" \ --set controlPlane.insecureSkipTlsVerify=true \ --set bootstrap.nodeMembers='{user:system:serviceaccount:agentmesh-nodes:calc-mcp-agentmesh-node}' \ - --set bootstrap.nodeServices='{mcp://calculator,system://mesh.catalog}' + --set bootstrap.nodeServices='{mcp://calculator,system://mesh.catalog}' \ + --set bootstrap.nodePublish='{mcp://calculator}' ``` `controlPlane.workloadIssuer` marks the cluster issuer as a workload identity @@ -51,12 +52,15 @@ for a public provider. On a managed cluster the issuer is often public (on GKE: `https://container.googleapis.com/v1/projects/.../clusters/...`) and does not need the flag. -The three `bootstrap.*` values write the initial policy: +The four `bootstrap.*` values write the initial policy: - `nodeMembers`: the identities that may enroll as `mesh:role:node`. If empty, no node can join until you post a policy. - `nodeServices`: the services that nodes may call. If empty, nodes can call nothing. +- `nodePublish`: the services that nodes may serve: announce to the + directory and be accepted as a provider of. If empty, nodes can publish + nothing. - `nodeLabels`: the label patterns that nodes may attest. If empty, nodes cannot declare labels. diff --git a/site/content/docs/guides/native-sdks.md b/site/content/docs/guides/native-sdks.md index f08f6737..6035e758 100644 --- a/site/content/docs/guides/native-sdks.md +++ b/site/content/docs/guides/native-sdks.md @@ -914,9 +914,12 @@ never forwarded. The connection is encrypted end to end between the two peers; the router forwards ciphertext. Under the open development policy of `agentmesh-one`, every member holds the -`node` role and that role may call any service. On a shared mesh the policy -grants a role the services it may call (`allowed_services: ["a2a://agent"]` -or a pattern) and the members it may reach; see +`node` role and that role may call and serve any service. On a shared mesh +the policy grants a role the services it may call (`allowed_services: +["a2a://agent"]` or a pattern) and the members it may reach, and grants the +agent's own role the service it serves (`allowed_publish: ["a2a://agent"]`): +a caller verifies that grant on the agent's credential before sending a +request, and refuses an agent that lacks it; see [Mesh policy](../../reference/policy/). A refused caller receives `403`. ## Beyond the examples diff --git a/site/content/docs/reference/node-api.md b/site/content/docs/reference/node-api.md index 6a51f3f3..898bfd86 100644 --- a/site/content/docs/reference/node-api.md +++ b/site/content/docs/reference/node-api.md @@ -233,8 +233,11 @@ server-sent events as they are found, and the stream ends with /mesh//// ``` -The node verifies the peer's credential and, if set, the operator's -`egress.require_labels`. It then opens an authenticated stream (presenting the +The node verifies the peer's credential: that it is an enrolled node, that +its role grants serving `://` (`allowed_publish`), and, if set, +the operator's `egress.require_labels`. A peer whose credential does not +grant the service is refused with `403`, whatever the directory said about +it. It then opens an authenticated stream (presenting the caller's Task Biscuit when one was supplied in `X-Mesh-Authentication`) and forwards the request to `///` on the peer, which proxies it to the backend. Headers pass through, including `Authorization`. Paths that diff --git a/site/content/docs/reference/policy.md b/site/content/docs/reference/policy.md index 63e662e0..bdabc4c8 100644 --- a/site/content/docs/reference/policy.md +++ b/site/content/docs/reference/policy.md @@ -18,6 +18,7 @@ back from `GET /admin/policy`, edited in the console, or given to { "name": "mesh:role:node", "allowed_services": ["system://mesh.catalog"], + "allowed_publish": ["mcp://code-reviewer", "mcp://build-runner.*", "inference://*"], "allowed_labels": ["region=*", "team=platform"] }, { @@ -66,6 +67,7 @@ notice. |---|---|---| | `name` | string, required, unique | The role name. `mesh:role:node` and `mesh:role:router` are the roles that the two binaries request at enrollment. Any other name is an ordinary role. | | `allowed_services` | list of service patterns | Services that holders may call. | +| `allowed_publish` | list of service patterns | Services that holders may serve. A router keeps a member's directory record only for a service its credential grants here, and a caller sends a request to a provider it reached by name only when the provider's credential does. If absent, holders may publish nothing. See [Publishing](#publishing). | | `allowed_targets` | list of target patterns | Nodes that holders may call. If absent, any node may be called. | | `allowed_labels` | list of label patterns | Labels that a node holding this role may declare at enrollment. If absent, no labels may be declared. | | `custom_datalog` | list of Datalog statements | Facts are minted into the credentials of holders. Rules are distributed to nodes and applied when a holder is verified. | @@ -95,6 +97,30 @@ The same holds for `target_unrestricted(true)` below. `system://mesh.catalog` is the built-in discovery service that every node runs. A role that should be able to list the tools of a node needs it. +### Publishing + +`allowed_publish` takes the same patterns and compiles them to +`granted_publish_*` facts: `granted_publish_exact("mcp", "calculator")`, +`granted_publish_all("mcp")`, `granted_publish_all_types(true)` and so on. +They say what a member may serve, where `allowed_services` says what it may +call, and the two are independent: a role that may call `mcp://calc` may +not serve it unless granted here. + +Two parties check the grant, on the holder's own credential. The router a +member announces a record to refuses a record for a service the grant does +not cover (`agentmesh_router_directory_requests_total{op="provide",result="denied"}`), +so a member cannot put itself in the directory under a name it may not +serve. A caller that reaches a provider by name, from the directory or by +peer ID, verifies the provider's credential before sending the request and +refuses a provider the grant does not cover, so a record that reached the +directory some other way is still not called. `agentmesh-node` and both +SDKs make the check; a member's own record under its peer ID, and a node's +catalog (`system://mesh.catalog`), name no service and need no grant. + +An egress destination needs no entry: `served_by` selects the nodes that +serve it, and the control plane mints `granted_publish_exact("egress", "")` +into their credentials with the destination. + ### Target patterns `fact:value`, where `fact` is one of the identity facts that a node's diff --git a/site/content/docs/reference/router.md b/site/content/docs/reference/router.md index f5f87834..32eada20 100644 --- a/site/content/docs/reference/router.md +++ b/site/content/docs/reference/router.md @@ -75,7 +75,10 @@ replica count. holds. A member announces each service it publishes and where it is reached; the router keeps the record and places it on the routers closest to the key, as many as the mesh's replication (`/info`). A - record lives for the mesh's record TTL after its last announcement, + record is kept only for a service the member's credential grants serving + (the role's `allowed_publish`), checked against the credential the + handshake presented; a record for anything else is refused. A record + lives for the mesh's record TTL after its last announcement, and an announcement replaces the addresses of the one before; a member that stops a service or leaves withdraws its records, and the router withdraws them from the same routers. A member asks for providers of @@ -134,7 +137,7 @@ The metrics address exposes, next to the Go runtime metrics: | `agentmesh_router_connected_peers`, `agentmesh_router_authenticated_peers`, `agentmesh_router_banned_peers` | Peers in each state. | | `agentmesh_router_peer_routers{state}` | Other routers this one knows, from the control plane's list or a handshake that proved the router role: `listed`, and `authenticated` for those it currently holds a mutual authentication with. The two differ while a federation pass is pending or a peer router is down. | | `agentmesh_router_dht_routing_table_size` | Peer routers in the DHT's routing table: the authenticated peer routers, since nothing else enters it. | -| `agentmesh_router_directory_requests_total{op,result}`, `agentmesh_router_directory_request_seconds{op}` | Directory requests served, by operation (`provide`, `withdraw`, `find_providers`, `find_peer`; `place` is a record or withdrawal another router put here, `lookup` a find another router ran here) and result (`ok`, `empty`, `invalid`, `error`), with the time each took. | +| `agentmesh_router_directory_requests_total{op,result}`, `agentmesh_router_directory_request_seconds{op}` | Directory requests served, by operation (`provide`, `withdraw`, `find_providers`, `find_peer`; `place` is a record or withdrawal another router put here, `lookup` a find another router ran here) and result (`ok`, `empty`, `invalid`, `denied` for a record naming a service the member's credential does not grant serving, `error`), with the time each took. | | `agentmesh_router_directory_records` | Directory records this router holds: its own members' and the copies placed on it. | | `agentmesh_router_directory_records_expired_total` | Records dropped because their provider stopped announcing them for the mesh's record TTL. | | `agentmesh_router_directory_placements_total{result}` | Copies of a member's record, or of its withdrawal, this router placed on the routers closest to the key. | diff --git a/tests/e2e/lib/container_mesh.bash b/tests/e2e/lib/container_mesh.bash index d2a7738d..59505415 100644 --- a/tests/e2e/lib/container_mesh.bash +++ b/tests/e2e/lib/container_mesh.bash @@ -474,6 +474,7 @@ if [[ -z "${MESH_HELPERS_LOADED:-}" ]]; then --set console.enabled=false --set 'router.externalAddrs={/dns4/agentmesh-router/tcp/4501}' --set 'bootstrap.nodeServices={mcp://calculator,mcp://db-agent,mcp://http-tool,mcp://stdio-tool,a2a://echo,system://mesh.catalog}' + --set 'bootstrap.nodePublish={mcp://calculator,mcp://db-agent,mcp://http-tool,mcp://stdio-tool,a2a://echo}' --set 'bootstrap.nodeMembers={user:test-user}' --set 'bootstrap.nodeLabels={region=*}') if ! "${helm_bin}" "${helm_args[@]}"; then diff --git a/tests/integration/biscuit_expiry_test.go b/tests/integration/biscuit_expiry_test.go index a1c4bed8..5a2e38fa 100644 --- a/tests/integration/biscuit_expiry_test.go +++ b/tests/integration/biscuit_expiry_test.go @@ -84,7 +84,7 @@ func TestBiscuitExpiryIsEnforcedOnEveryPath(t *testing.T) { // The grants mirror the shape this test always ran with (the no-policy // mint fallback): unrestricted, because the subject here is expiry. policyFile := filepath.Join(tmpDir, "policies.yaml") - policyYAML := "roles:\n - name: mesh:role:node\n allowed_services: [\"*\"]\n allowed_targets: []\n custom_datalog: [\"target_unrestricted(true);\"]\nbindings:\n - role: mesh:role:node\n members: [\"user:expiry-user\"]\n" + policyYAML := "roles:\n - name: mesh:role:node\n allowed_services: [\"*\"]\n allowed_publish: [\"*\"]\n allowed_targets: []\n custom_datalog: [\"target_unrestricted(true);\"]\nbindings:\n - role: mesh:role:node\n members: [\"user:expiry-user\"]\n" if err := os.WriteFile(policyFile, []byte(policyYAML), 0644); err != nil { t.Fatal(err) } diff --git a/tests/integration/minimal_helpers_test.go b/tests/integration/minimal_helpers_test.go index 5611d772..3f54ddeb 100644 --- a/tests/integration/minimal_helpers_test.go +++ b/tests/integration/minimal_helpers_test.go @@ -337,6 +337,14 @@ func createMockBiscuitToken(t *testing.T, peerID string, priv ed25519.PrivateKey } } + // The mock router stands in for a control plane whose node role grants + // allowed_publish ["*"], as the policy fixtures in this suite do. + if role == api.RoleNode { + if err := builder.AddAuthorityFact(api.MarkerFact(api.FactGrantedPublishAllTypes)); err != nil { + t.Fatalf("failed to add publish grant: %v", err) + } + } + err = builder.AddAuthorityFact(biscuit.Fact{Predicate: biscuit.Predicate{ Name: api.FactExpiration, IDs: []biscuit.Term{biscuit.Date(time.Now().Add(1 * time.Hour))}, @@ -721,7 +729,8 @@ func writePolicyWithRouter(t *testing.T, path string, yamlContent string) { allowed_targets: ["*"] - name: %s allowed_services: [] - allowed_targets: []`, api.RoleRouter, api.RoleNode) + allowed_targets: [] + allowed_publish: ["*"]`, api.RoleRouter, api.RoleNode) routerBinding := fmt.Sprintf(` - role: %s members: ["group:routers"]`, api.RoleRouter) diff --git a/tests/integration/readmission_test.go b/tests/integration/readmission_test.go index 75464b0f..53181521 100644 --- a/tests/integration/readmission_test.go +++ b/tests/integration/readmission_test.go @@ -131,7 +131,7 @@ func startReadmissionMesh(t *testing.T, ctx context.Context) *readmissionMesh { // through group "routers"; what either may reach is not the subject here. roles := []*api.PolicyRole{ {Name: api.RoleRouter, AllowedServices: []string{"*"}, AllowedTargets: []string{"*"}}, - {Name: api.RoleNode, AllowedServices: []string{"*"}, AllowedTargets: []string{"*"}}, + {Name: api.RoleNode, AllowedServices: []string{"*"}, AllowedPublish: []string{"*"}, AllowedTargets: []string{"*"}}, } bindings := []*api.PolicyBinding{ {Role: api.RoleRouter, Members: []string{"group:routers"}}, diff --git a/tests/integration/router_attachment_test.go b/tests/integration/router_attachment_test.go index 4422e7c9..3a438e17 100644 --- a/tests/integration/router_attachment_test.go +++ b/tests/integration/router_attachment_test.go @@ -346,6 +346,7 @@ func startAttachmentMesh(t *testing.T) *attachmentMesh { allowed_labels: ["site=*", "pa=*", "pb=*"] - name: %s allowed_services: ["mcp://*"] + allowed_publish: ["mcp://*"] allowed_targets: ["*"] bindings: - role: %s diff --git a/tests/integration/sdk_mesh_test.go b/tests/integration/sdk_mesh_test.go index 76e789f1..41dfc6bd 100644 --- a/tests/integration/sdk_mesh_test.go +++ b/tests/integration/sdk_mesh_test.go @@ -180,6 +180,7 @@ func startSDKMesh(t *testing.T) *sdkMesh { allowed_labels: ["zone=*"] - name: %s allowed_services: ["*"] + allowed_publish: ["*"] allowed_targets: ["*"] allowed_labels: ["region=*", "team=*"] bindings: diff --git a/tests/integration/sts_cuj_test.go b/tests/integration/sts_cuj_test.go index 01df7c93..cedaaa9f 100644 --- a/tests/integration/sts_cuj_test.go +++ b/tests/integration/sts_cuj_test.go @@ -153,6 +153,7 @@ func TestSTSTaskScopedSecurityCUJ(t *testing.T) { policyYAML := fmt.Sprintf(`roles: - name: pep allowed_services: ["egress://api.github.com", "mcp://github"] + allowed_publish: ["mcp://github"] allowed_targets: ["*"] - name: developer allowed_services: ["egress://api.github.com", "mcp://github"] diff --git a/tests/ui/dev.sh b/tests/ui/dev.sh index e21bf3da..8d559c02 100755 --- a/tests/ui/dev.sh +++ b/tests/ui/dev.sh @@ -52,7 +52,7 @@ api POST /policies -H "Content-Type: application/json" -d '{ "roles": [ {"name": "mesh-admin", "allowed_services": ["*"], "allowed_targets": ["*"]}, {"name": "mesh:role:router", "allowed_services": ["*"], "allowed_targets": ["*"]}, - {"name": "mesh:role:node", "allowed_services": ["mcp://*", "system://mesh.catalog"], "allowed_targets": ["*"]} + {"name": "mesh:role:node", "allowed_services": ["mcp://*", "system://mesh.catalog"], "allowed_publish": ["mcp://*"], "allowed_targets": ["*"]} ], "bindings": [ {"role": "mesh:role:router", "members": ["group:routers"]}, From d4466b133042e3671ec5704f15eb9a720189bca7 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Wed, 14 Oct 2026 16:00:35 +0000 Subject: [PATCH 6/7] policy: a publish grant reaches its verifiers through the policy text A publish grant was minted into the credential and nowhere else, so a change to allowed_publish reached the router and the callers when the member's credential was minted again: up to nineteen hours after the operator saved it, with nothing for them to do but wait or re-enroll. Every other grant already travels a second way, as the Datalog text the control plane renders (granted_*(...) <- role(...)) and every member adds to its authorizer at its next sync or at once on POLICY_UPDATE. Publish grants now travel the same way. BuildPolicyRules renders granted_publish_*(...) <- role("") for each role's allowed_publish, and BuildEgressServingRules renders granted_publish_exact("egress", name) beside granted_service_exact for each served_by selector. Every verifier adds the synced rules to the authorizer that decides the grant (identity.RequirePublish takes them): the node in checkProvider, the JS and Python sessions in requirePublish, which every session now syncs the policy for, callers included, not only A2A endpoints. The router, which read no policy before, pulls /policies with its own credential at start, every PolicySyncInterval and on POLICY_UPDATE, and evaluates it at provide. A node whose policy text changed announces its records again, so a record the directory refused under the previous policy is in it within the sync interval instead of the reprovide interval. GetMeshPolicy reads roles, permissions and bindings in a fixed order, so the rendered text only changes when the policy does; it was built from a map, and two reads of one policy rendered the same rules in a different order. Tests: the rendering (api), RequirePublish over rules (identity), the router and node checks with rules, both SDKs' MCP opens with rules, a control plane validation test for allowed_publish, and an integration test that runs the whole thing: a node serving a service its role does not grant is not in the directory and is refused by peer ID, then the policy grants it and, with nothing restarted, the service is found and called. --- api/egress.go | 22 ++- api/egress_test.go | 2 + api/policy_rules.go | 3 + api/policy_rules_test.go | 3 + internal/controlplane/resolve_roles_test.go | 23 +++ internal/identity/biscuit.go | 12 +- internal/identity/biscuit_test.go | 24 +++- internal/node/labels_gate.go | 5 +- internal/node/labels_gate_test.go | 29 ++++ internal/node/node.go | 9 ++ internal/router/config.go | 4 + internal/router/directory.go | 7 +- internal/router/directory_test.go | 22 +++ internal/router/router.go | 99 ++++++++++++- internal/storage/mesh_policy_test.go | 6 +- internal/storage/sql_store.go | 26 ++-- sdk/js/src/biscuit.ts | 8 +- sdk/js/src/mcp.test.ts | 10 ++ sdk/js/src/mcp.ts | 4 +- sdk/js/src/session.ts | 19 +-- sdk/js/src/sync.test.ts | 4 + sdk/python/src/agent_mesh/biscuit.py | 11 +- sdk/python/src/agent_mesh/mcp_client.py | 6 +- sdk/python/src/agent_mesh/session.py | 24 ++-- sdk/python/tests/test_mcp.py | 12 ++ sdk/python/tests/test_sync.py | 2 + site/content/docs/reference/policy.md | 16 ++- site/content/docs/reference/router.md | 4 +- tests/integration/publish_grant_test.go | 151 ++++++++++++++++++++ tests/integration/router_attachment_test.go | 10 +- 30 files changed, 507 insertions(+), 70 deletions(-) create mode 100644 tests/integration/publish_grant_test.go diff --git a/api/egress.go b/api/egress.go index 95ecbfd7..bac8b2ec 100644 --- a/api/egress.go +++ b/api/egress.go @@ -267,17 +267,23 @@ func WithEgressPublish(policyRoles []*PolicyRole, egress []*EgressDestination, r } // BuildEgressServingRules grants each destination to the nodes that serve it: -// granted_service_exact("egress", name) <- role(r) or <- label(k, v) for every -// served_by entry. The serving node evaluates its own credential when a local -// client asks for the destination, so the grant has to reach it; the rules -// travel with the mesh policy like every other grant. +// granted_service_exact("egress", name) and granted_publish_exact("egress", name) +// <- role(r) or <- label(k, v) for every served_by entry. The serving node +// evaluates its own credential when a local client asks for the destination, +// and the router and the callers evaluate it when the node publishes, so +// both grants have to reach them; the rules travel with the mesh policy like +// every other grant. func BuildEgressServingRules(egress []*EgressDestination) []PolicyRule { var rules []PolicyRule for _, d := range egress { if d == nil || ValidateEgressName(d.GetName()) != nil { continue } - head := biscuit.Predicate{Name: FactGrantedServiceExact, IDs: []biscuit.Term{biscuit.String(ServiceTypeStringEgress), biscuit.String(d.GetName())}} + destination := []biscuit.Term{biscuit.String(ServiceTypeStringEgress), biscuit.String(d.GetName())} + heads := []biscuit.Predicate{ + {Name: FactGrantedServiceExact, IDs: destination}, + {Name: FactGrantedPublishExact, IDs: destination}, + } for _, sel := range d.GetServedBy() { var body biscuit.Predicate if key, value, isLabel := strings.Cut(sel, "="); isLabel { @@ -291,8 +297,10 @@ func BuildEgressServingRules(egress []*EgressDestination) []PolicyRule { } body = biscuit.Predicate{Name: FactRole, IDs: []biscuit.Term{biscuit.String(sel)}} } - r := biscuit.Rule{Head: head, Body: []biscuit.Predicate{body}} - rules = append(rules, PolicyRule{Rule: r, Text: renderRule(r)}) + for _, head := range heads { + r := biscuit.Rule{Head: head, Body: []biscuit.Predicate{body}} + rules = append(rules, PolicyRule{Rule: r, Text: renderRule(r)}) + } } } return rules diff --git a/api/egress_test.go b/api/egress_test.go index de1a98b7..3d4e4ac2 100644 --- a/api/egress_test.go +++ b/api/egress_test.go @@ -142,7 +142,9 @@ func TestBuildEgressServingRules(t *testing.T) { texts := PolicyRuleTexts(rules) want := []string{ `granted_service_exact("egress", "api.github.com") <- role("pep")`, + `granted_publish_exact("egress", "api.github.com") <- role("pep")`, `granted_service_exact("egress", "api.github.com") <- label("site", "eu")`, + `granted_publish_exact("egress", "api.github.com") <- label("site", "eu")`, } if !slices.Equal(texts, want) { t.Errorf("rules = %v, want %v", texts, want) diff --git a/api/policy_rules.go b/api/policy_rules.go index 15a4a08b..44069948 100644 --- a/api/policy_rules.go +++ b/api/policy_rules.go @@ -225,6 +225,9 @@ func BuildPolicyRules(roles []*PolicyRole, bindings []*PolicyBinding) (rules []P add(fact.Predicate, fromRole) } } + for _, fact := range BuildPublishDatalogFacts(role.AllowedPublish) { + add(fact.Predicate, fromRole) + } hasUnrestricted := false var nonWildcardTargets []string diff --git a/api/policy_rules_test.go b/api/policy_rules_test.go index 942235d5..11094e69 100644 --- a/api/policy_rules_test.go +++ b/api/policy_rules_test.go @@ -41,6 +41,7 @@ func TestBuildPolicyRules(t *testing.T) { Name: "test-role", AllowedTargets: []string{"*", "node:" + nodeCID, "node:not-a-peer-id", "custom-fact:custom-val", "legacy-peer"}, AllowedServices: []string{"*:*", "mcp:*", "mcp:*.suffix", "mcp:prefix.*", "mcp:exact"}, + AllowedPublish: []string{"mcp://*", "a2a://echo"}, CustomDatalog: []string{ "custom_rule($x) <- fact($x), $x > 3;", "custom_fact(\"hello\")", @@ -66,6 +67,8 @@ func TestBuildPolicyRules(t *testing.T) { "granted_service_suffix(\"mcp\", \".suffix\") <- role(\"test-role\")": false, "granted_service_prefix(\"mcp\", \"prefix.\") <- role(\"test-role\")": false, "granted_service_set(\"mcp\", [\"exact\"]) <- role(\"test-role\")": false, + "granted_publish_all(\"mcp\") <- role(\"test-role\")": false, + "granted_publish_set(\"a2a\", [\"echo\"]) <- role(\"test-role\")": false, "target_unrestricted(true) <- role(\"test-role\")": false, "target_restricted(true) <- role(\"test-role\")": false, "granted_target_set(\"node\", [\"" + nodeID + "\", \"legacy-peer\"]) <- role(\"test-role\")": false, diff --git a/internal/controlplane/resolve_roles_test.go b/internal/controlplane/resolve_roles_test.go index 298b8540..f4fefe02 100644 --- a/internal/controlplane/resolve_roles_test.go +++ b/internal/controlplane/resolve_roles_test.go @@ -15,6 +15,7 @@ package controlplane import ( + "strings" "testing" jwt "github.com/golang-jwt/jwt/v5" @@ -123,3 +124,25 @@ func TestResolveRolesWildcardsAndValidation(t *testing.T) { } } } + +func TestValidatePolicyConfigAllowedPublish(t *testing.T) { + role := func(publish ...string) *api.PolicyConfig { + return &api.PolicyConfig{ + Roles: []*api.PolicyRole{{Name: "r1", AllowedPublish: publish}}, + Bindings: []*api.PolicyBinding{{Role: "r1", Members: []string{"user:alice"}}}, + } + } + for _, ok := range [][]string{nil, {"*"}, {"mcp://*"}, {"mcp://calc", "a2a://echo"}, {"egress://api.github.com"}} { + if err := ValidatePolicyConfig(role(ok...)); err != nil { + t.Errorf("ValidatePolicyConfig rejected allowed_publish %q: %v", ok, err) + } + } + for _, bad := range []string{"calc", "mcp:calc", "mcp://calc?x=1", "mcp://calc#f", ""} { + err := ValidatePolicyConfig(role(bad)) + if err == nil { + t.Errorf("ValidatePolicyConfig accepted allowed_publish %q", bad) + } else if !strings.Contains(err.Error(), "allowed_publish") { + t.Errorf("rejecting allowed_publish %q, the error does not name the field: %v", bad, err) + } + } +} diff --git a/internal/identity/biscuit.go b/internal/identity/biscuit.go index 9a5de97d..62cf426a 100644 --- a/internal/identity/biscuit.go +++ b/internal/identity/biscuit.go @@ -818,14 +818,20 @@ func RequireRole(b *biscuit.Biscuit, key ed25519.PublicKey, expectedRole string, // RequirePublish reports whether the credential grants serving the service // svcType://svcName: api.PublishPolicies over the token's granted_publish_* -// facts and a publish() fact. b must have passed verification and key must -// be the key that verified it; what is decided here is the grant alone. -func RequirePublish(b *biscuit.Biscuit, key ed25519.PublicKey, svcType, svcName string, timeout time.Duration) error { +// facts, those the mesh policy rules derive from its role() and label() +// facts, and a publish() fact. The rules are what a policy change reaches a +// verifier through before the holder's credential is minted again. b must +// have passed verification and key must be the key that verified it; what +// is decided here is the grant alone. +func RequirePublish(b *biscuit.Biscuit, key ed25519.PublicKey, svcType, svcName string, policyRules []biscuit.Rule, timeout time.Duration) error { authorizer, err := b.Authorizer(key, AuthorizerOptions(timeout)...) if err != nil { return fmt.Errorf("failed to create authorizer: %w", err) } authorizer.AddFact(api.PublishFact(svcType, svcName)) + for _, r := range policyRules { + authorizer.AddRule(r) + } for _, p := range api.PublishPolicies { authorizer.AddPolicy(p) } diff --git a/internal/identity/biscuit_test.go b/internal/identity/biscuit_test.go index 5285eff4..9d0e0d38 100644 --- a/internal/identity/biscuit_test.go +++ b/internal/identity/biscuit_test.go @@ -798,7 +798,7 @@ func TestMintBiscuitToken_PublishFacts(t *testing.T) { {"mcp", "tools", false}, {"egress", "api.github.com", false}, } { - err := RequirePublish(b, pub, tc.svcType, tc.svcName, testTimeout) + err := RequirePublish(b, pub, tc.svcType, tc.svcName, nil, testTimeout) if (err == nil) != tc.granted { t.Errorf("RequirePublish(%s://%s) = %v, want granted %v", tc.svcType, tc.svcName, err, tc.granted) } @@ -809,16 +809,34 @@ func TestMintBiscuitToken_PublishFacts(t *testing.T) { t.Fatal(err) } nb, _ := biscuit.Unmarshal(none) - if err := RequirePublish(nb, pub, "mcp", "calc", testTimeout); err == nil { + if err := RequirePublish(nb, pub, "mcp", "calc", nil, testTimeout); err == nil { t.Error("a credential minted under no policy role grants publishing nothing") } + // The mesh policy rules reach a verifier before the holder's credential + // is minted again: a credential carrying only role("publisher") is + // granted what the current policy gives that role, and only that. + rules, warnings := api.BuildPolicyRules(roles, nil) + if len(warnings) != 0 { + t.Fatalf("BuildPolicyRules warnings: %v", warnings) + } + parsed, err := api.ParseDatalogRules(api.PolicyRuleTexts(rules)) + if err != nil { + t.Fatal(err) + } + if err := RequirePublish(nb, pub, "mcp", "calc", parsed, testTimeout); err != nil { + t.Errorf("the policy rules grant role publisher mcp://calc, RequirePublish = %v", err) + } + if err := RequirePublish(nb, pub, "mcp", "clock", parsed, testTimeout); err == nil { + t.Error("the policy rules do not grant role publisher mcp://clock, RequirePublish = nil") + } + delegated, err := MintDelegatedBiscuitToken(priv, jwt.MapClaims{"sub": "user"}, p, time.Now().Add(time.Hour), []string{"publisher"}, roles, nil, false) if err != nil { t.Fatal(err) } db, _ := biscuit.Unmarshal(delegated) - if err := RequirePublish(db, pub, "mcp", "calc", testTimeout); err == nil { + if err := RequirePublish(db, pub, "mcp", "calc", nil, testTimeout); err == nil { t.Error("a delegated credential grants publishing nothing") } } diff --git a/internal/node/labels_gate.go b/internal/node/labels_gate.go index 80ecee36..6349ecee 100644 --- a/internal/node/labels_gate.go +++ b/internal/node/labels_gate.go @@ -313,7 +313,10 @@ func (n *AgentMeshNode) checkProvider(providerBiscuit []byte, peerID peer.ID, re if svcType == "" || svcName == "" { return fmt.Errorf("service %q must be ://", service) } - if err := identity.RequirePublish(b, key, svcType, svcName, n.BiscuitTimeout); err != nil { + n.MeshPolicyMu.RLock() + meshRules := n.MeshPolicyRules + n.MeshPolicyMu.RUnlock() + if err := identity.RequirePublish(b, key, svcType, svcName, meshRules, n.BiscuitTimeout); err != nil { return fmt.Errorf("%w: %w", ErrProviderNotGranted, err) } } diff --git a/internal/node/labels_gate_test.go b/internal/node/labels_gate_test.go index e3694619..16880099 100644 --- a/internal/node/labels_gate_test.go +++ b/internal/node/labels_gate_test.go @@ -159,6 +159,35 @@ func TestCheckPeerLabels(t *testing.T) { } }) } + + // The synced mesh policy grants too, from the provider's role, so a + // policy change reaches the caller at its next sync and not at the + // provider's next credential. + t.Run("granted by the mesh policy", func(t *testing.T) { + ungranted := mintPublish() + if err := node.checkProvider(ungranted, providerPeer, nil, "mcp://calc"); !errors.Is(err, ErrProviderNotGranted) { + t.Fatalf("checkProvider before the policy = %v, want ErrProviderNotGranted", err) + } + rules, _ := api.BuildPolicyRules([]*api.PolicyRole{{Name: api.RoleNode, AllowedPublish: []string{"mcp://calc"}}}, nil) + parsed, err := api.ParseDatalogRules(api.PolicyRuleTexts(rules)) + if err != nil { + t.Fatal(err) + } + node.MeshPolicyMu.Lock() + node.MeshPolicyRules = parsed + node.MeshPolicyMu.Unlock() + t.Cleanup(func() { + node.MeshPolicyMu.Lock() + node.MeshPolicyRules = nil + node.MeshPolicyMu.Unlock() + }) + if err := node.checkProvider(ungranted, providerPeer, nil, "mcp://calc"); err != nil { + t.Fatalf("checkProvider with the policy granting the node role mcp://calc = %v", err) + } + if err := node.checkProvider(ungranted, providerPeer, nil, "mcp://weather"); !errors.Is(err, ErrProviderNotGranted) { + t.Fatalf("checkProvider(mcp://weather) = %v, want ErrProviderNotGranted: the policy grants mcp://calc only", err) + } + }) }) } diff --git a/internal/node/node.go b/internal/node/node.go index 6a1210a5..34128de9 100644 --- a/internal/node/node.go +++ b/internal/node/node.go @@ -182,6 +182,7 @@ type AgentMeshNode struct { trustedKeys []TrustedKey keysMu sync.RWMutex MeshPolicyRules []biscuit.Rule + meshPolicyTexts []string MeshPolicyMu sync.RWMutex // pendingEgress holds assignments that arrived before Start created the // service registry (SyncControlPlane runs first); Start applies them. @@ -2699,10 +2700,18 @@ func (n *AgentMeshNode) syncMeshPolicy(ctx context.Context) error { } n.MeshPolicyMu.Lock() + changed := n.meshPolicyTexts != nil && !slices.Equal(n.meshPolicyTexts, policyResp.DatalogRules) n.MeshPolicyRules = rules + n.meshPolicyTexts = policyResp.DatalogRules n.MeshPolicyMu.Unlock() logger.Infof("Successfully synchronized mesh policy (%d rules)", len(rules)) + if changed && n.reprovideTrigger != nil { + // The policy decides what this node may publish; a record the + // directory refused under the previous one is announced again now. + logger.Info("Mesh policy changed; announcing this node's records again") + n.triggerReprovide() + } return nil } diff --git a/internal/router/config.go b/internal/router/config.go index ecd67fc9..915c72f3 100644 --- a/internal/router/config.go +++ b/internal/router/config.go @@ -41,6 +41,7 @@ type Options struct { ListenAddrs []string ExternalAddrs []string KeysSyncInterval time.Duration + PolicySyncInterval time.Duration LeaseRenewInterval time.Duration OIDCProvider string // For OIDC enrollment OIDCToken string // OIDC JWT token for enrollment @@ -121,6 +122,9 @@ func (o *Options) Default() { if o.KeysSyncInterval <= 0 { o.KeysSyncInterval = 5 * time.Minute } + if o.PolicySyncInterval <= 0 { + o.PolicySyncInterval = 15 * time.Minute + } if o.LeaseRenewInterval <= 0 { o.LeaseRenewInterval = 30 * time.Second } diff --git a/internal/router/directory.go b/internal/router/directory.go index e6866bf4..a4f01c3f 100644 --- a/internal/router/directory.go +++ b/internal/router/directory.go @@ -244,7 +244,10 @@ func (e *directoryDeniedError) Error() string { return e.err.Error() } func (e *directoryDeniedError) Unwrap() error { return e.err } // requirePublish checks the admitted member's credential grants serving -// the service a record names. A record under the member's own peer key +// the service a record names: its own granted_publish_* facts, or those +// the mesh policy rules derive from its role() and label() facts, so that +// a policy change reaches the directory at the next sync and not at the +// member's next credential. A record under the member's own peer key // names no service and needs no grant: a member may always say where it // is reached. func (r *Router) requirePublish(provider peer.ID, svcType api.ServiceType, svcName string) error { @@ -260,7 +263,7 @@ func (r *Router) requirePublish(provider peer.ID, svcType api.ServiceType, svcNa return &directoryDeniedError{service: typeStr + "://" + svcName, err: errors.New("no credential held for the member")} } cred := v.(memberCredential) - if err := identity.RequirePublish(cred.biscuit, cred.key, typeStr, svcName, r.config.BiscuitTimeout); err != nil { + if err := identity.RequirePublish(cred.biscuit, cred.key, typeStr, svcName, r.meshPolicyRules(), r.config.BiscuitTimeout); err != nil { return &directoryDeniedError{service: typeStr + "://" + svcName, err: err} } return nil diff --git a/internal/router/directory_test.go b/internal/router/directory_test.go index aefc1433..8e3ec8df 100644 --- a/internal/router/directory_test.go +++ b/internal/router/directory_test.go @@ -466,4 +466,26 @@ func TestDirectoryDeniesWhatTheCredentialDoesNotGrant(t *testing.T) { if resp := ask(t, member, a.Host.ID(), provide(calc, "mcp://calculator")); resp.GetError() == "" { t.Fatal("stored with no credential held; want a refusal") } + + // The mesh policy the router synced grants too: a credential carrying + // role("member") and no publish fact is granted what the policy gives + // that role now, so a policy change does not wait for the member's next + // credential. + member = attach(t, a) + if resp := ask(t, member, a.Host.ID(), provide(weather, "mcp://weather")); resp.GetError() == "" { + t.Fatal("stored under a policy granting nothing; want a refusal") + } + rules, _ := api.BuildPolicyRules([]*api.PolicyRole{{Name: "member", AllowedPublish: []string{"mcp://weather"}}}, nil) + texts := api.PolicyRuleTexts(rules) + parsed, err := api.ParseDatalogRules(texts) + if err != nil { + t.Fatal(err) + } + a.policy.Store(&meshPolicy{rules: parsed, texts: texts}) + if resp := ask(t, member, a.Host.ID(), provide(weather, "mcp://weather")); resp.GetError() != "" { + t.Fatalf("refused under a policy granting the role mcp://weather: %s", resp.GetError()) + } + if resp := ask(t, member, a.Host.ID(), provide(calc, "mcp://calculator")); resp.GetError() == "" { + t.Fatal("stored mcp://calculator under a policy granting the role mcp://weather only; want a refusal") + } } diff --git a/internal/router/router.go b/internal/router/router.go index f1a957b5..20c39f18 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -27,6 +27,7 @@ import ( "os" "path/filepath" "runtime/debug" + "slices" "strings" "sync" "sync/atomic" @@ -342,6 +343,10 @@ type Router struct { // credentials is the credential each admitted member presented, as // memberCredential, for what the directory checks against it. credentials sync.Map + // policy is the mesh policy as the control plane renders it + // (PolicyConfigGetResponse.datalog_rules), what a member's credential is + // evaluated with besides its own facts; nil until the first sync. + policy atomic.Pointer[meshPolicy] bannedPeers sync.Map // directory is how the mesh keeps its directory, read from /info at // start (syncDirectory); records is the records this router holds. @@ -374,7 +379,8 @@ type Router struct { // only the last biscuit it issued. refreshMu sync.Mutex - keysSyncTrigger chan struct{} + keysSyncTrigger chan struct{} + policySyncTrigger chan struct{} // Control contexts ctx context.Context @@ -409,11 +415,12 @@ func NewRouter(ctx context.Context, config Options) (*Router, error) { } return &Router{ - config: config, - ctx: ctx, - cancel: cancel, - handshakeLimiter: handshakeLimiter, - keysSyncTrigger: make(chan struct{}, 1), + config: config, + ctx: ctx, + cancel: cancel, + handshakeLimiter: handshakeLimiter, + keysSyncTrigger: make(chan struct{}, 1), + policySyncTrigger: make(chan struct{}, 1), }, nil } @@ -592,9 +599,10 @@ func (r *Router) Start() (retErr error) { }) // 5. Start background routines - r.wg.Add(8) + r.wg.Add(9) go r.runLeaseRenewalLoop() go r.runKeysSyncLoop() + go r.runPolicySyncLoop() go r.runFederationLoop() go r.runBiscuitRenewalLoop() go r.runShedLoop() @@ -1043,6 +1051,9 @@ func (r *Router) listenForControlPlaneEvents(ctx context.Context) { case api.MeshEvent_KEY_ROTATION: logger.Infof("[Router Event] Received KEY_ROTATION event, triggering key sync") r.triggerKeysSync() + case api.MeshEvent_POLICY_UPDATE: + logger.Infof("[Router Event] Received POLICY_UPDATE event, triggering policy sync") + r.triggerPolicySync() } } } @@ -1072,6 +1083,80 @@ func (r *Router) runKeysSyncLoop() { } } +func (r *Router) triggerPolicySync() { + select { + case r.policySyncTrigger <- struct{}{}: + default: + } +} + +// runPolicySyncLoop pulls the mesh policy at start, on each POLICY_UPDATE +// event and every PolicySyncInterval: the event is best effort, the pull is +// what bounds how long a policy change takes to reach this router. +func (r *Router) runPolicySyncLoop() { + defer r.wg.Done() + ticker := time.NewTicker(r.config.PolicySyncInterval) + defer ticker.Stop() + + for { + if err := r.syncPolicy(); err != nil { + logger.Errorf("Failed policy sync: %v", err) + } + select { + case <-ticker.C: + case <-r.policySyncTrigger: + case <-r.ctx.Done(): + return + } + } +} + +// syncPolicy replaces the mesh policy rules with what the control plane +// renders now. The text is the contract: the router adds it to the +// authorizer and derives nothing from roles and bindings itself. +func (r *Router) syncPolicy() error { + r.keysMu.RLock() + biscuit := r.credential.biscuit + r.keysMu.RUnlock() + if len(biscuit) == 0 { + return errors.New("router has no credential yet") + } + resp, err := r.controlPlane(10*time.Second).FetchPolicy(r.ctx, biscuit) + if err != nil { + return err + } + if len(resp.ProtoReflect().GetUnknown()) > 0 { + return fmt.Errorf("control plane %s predates datalog_rules in its policy response; upgrade the control plane", r.config.ControlPlaneURL) + } + rules, err := api.ParseDatalogRules(resp.DatalogRules) + if err != nil { + return fmt.Errorf("mesh policy carries an unparseable rule: %w", err) + } + before := r.policy.Swap(&meshPolicy{rules: rules, texts: resp.DatalogRules}) + if before == nil || !slices.Equal(before.texts, resp.DatalogRules) { + logger.Infof("Synced mesh policy (%d rules)", len(rules)) + } else { + logger.Debugf("Synced mesh policy (%d rules, unchanged)", len(rules)) + } + return nil +} + +// meshPolicy is one synced rendering of the mesh policy: the rules the +// authorizer runs and the text they were parsed from, to tell a change. +type meshPolicy struct { + rules []biscuit.Rule + texts []string +} + +// meshPolicyRules is the mesh policy as last synced; empty before the first +// sync, when only a credential's own facts decide. +func (r *Router) meshPolicyRules() []biscuit.Rule { + if p := r.policy.Load(); p != nil { + return p.rules + } + return nil +} + func (r *Router) runLeaseRenewalLoop() { defer r.wg.Done() ticker := time.NewTicker(r.config.LeaseRenewInterval) diff --git a/internal/storage/mesh_policy_test.go b/internal/storage/mesh_policy_test.go index 0e17aacc..e3c9bea1 100644 --- a/internal/storage/mesh_policy_test.go +++ b/internal/storage/mesh_policy_test.go @@ -46,8 +46,10 @@ func TestMeshPolicyRoundTripsEveryRoleField(t *testing.T) { AllowedServices: []string{"mcp://tool"}, CustomDatalog: []string{`region("emea")`}, AllowedLabels: []string{"region=*"}, - AllowedPublish: []string{"mcp://tool", "inference://*"}, - Http: []*api.HTTPGrant{{Service: "mcp://tool", Methods: []string{"GET"}, Paths: []string{"/v1/*"}}}, + // Sorted as the store reads them back: the order of a list has no + // meaning in the policy, and a stable one is what tells a change. + AllowedPublish: []string{"inference://*", "mcp://tool"}, + Http: []*api.HTTPGrant{{Service: "mcp://tool", Methods: []string{"GET"}, Paths: []string{"/v1/*"}}}, } if err := store.SaveMeshPolicy(ctx, []*api.PolicyRole{want}, nil); err != nil { diff --git a/internal/storage/sql_store.go b/internal/storage/sql_store.go index 71676d81..f8fe901a 100644 --- a/internal/storage/sql_store.go +++ b/internal/storage/sql_store.go @@ -1272,9 +1272,11 @@ func (s *SQLStore) saveMeshPolicyTx(ctx context.Context, tx *sql.Tx, roles []*ap return nil } -// GetMeshPolicy retrieves the entire mesh policy as structured data. +// GetMeshPolicy retrieves the entire mesh policy as structured data, in an +// order that does not change between reads: the policy is rendered to text +// a member compares with the last text it fetched to tell a change. func (s *SQLStore) GetMeshPolicy(ctx context.Context) ([]*api.PolicyRole, []*api.PolicyBinding, error) { - rolesRows, err := s.db.QueryContext(ctx, s.rebind("SELECT name FROM roles")) + rolesRows, err := s.db.QueryContext(ctx, s.rebind("SELECT name FROM roles ORDER BY name")) if err != nil { return nil, nil, err } @@ -1296,7 +1298,7 @@ func (s *SQLStore) GetMeshPolicy(ctx context.Context) ([]*api.PolicyRole, []*api return nil, nil, err } - permsRows, err := s.db.QueryContext(ctx, s.rebind("SELECT role_name, resource_type, resource_value FROM role_permissions")) + permsRows, err := s.db.QueryContext(ctx, s.rebind("SELECT role_name, resource_type, resource_value FROM role_permissions ORDER BY role_name, resource_type, resource_value")) if err != nil { return nil, nil, err } @@ -1332,34 +1334,28 @@ func (s *SQLStore) GetMeshPolicy(ctx context.Context) ([]*api.PolicyRole, []*api return nil, nil, err } - bindingsRows, err := s.db.QueryContext(ctx, s.rebind("SELECT role_name, member FROM role_bindings")) + bindingsRows, err := s.db.QueryContext(ctx, s.rebind("SELECT role_name, member FROM role_bindings ORDER BY role_name, member")) if err != nil { return nil, nil, err } defer func() { _ = bindingsRows.Close() }() - bindingsMap := make(map[string]*api.PolicyBinding) + var bindings []*api.PolicyBinding for bindingsRows.Next() { var roleName, member string if err := bindingsRows.Scan(&roleName, &member); err != nil { return nil, nil, err } - b, ok := bindingsMap[roleName] - if !ok { - b = &api.PolicyBinding{Role: roleName} - bindingsMap[roleName] = b + if n := len(bindings); n > 0 && bindings[n-1].Role == roleName { + bindings[n-1].Members = append(bindings[n-1].Members, member) + continue } - b.Members = append(b.Members, member) + bindings = append(bindings, &api.PolicyBinding{Role: roleName, Members: []string{member}}) } if err := bindingsRows.Err(); err != nil { return nil, nil, err } - var bindings []*api.PolicyBinding - for _, b := range bindingsMap { - bindings = append(bindings, b) - } - return roles, bindings, nil } diff --git a/sdk/js/src/biscuit.ts b/sdk/js/src/biscuit.ts index 793a0a94..fc1234f9 100644 --- a/sdk/js/src/biscuit.ts +++ b/sdk/js/src/biscuit.ts @@ -281,7 +281,7 @@ export function requireRole(verified: VerifiedBiscuit, role: string): void { * who may serve it. The provider's own catalog names no service and needs * no grant; so does an empty target. */ -export async function requirePublish(verified: VerifiedBiscuit, targetService: string): Promise { +export async function requirePublish(verified: VerifiedBiscuit, targetService: string, policyRules: readonly string[] = []): Promise { if (targetService === "" || targetService === `system://${BASELINE_DATALOG.catalog_target}`) { return; } @@ -293,6 +293,12 @@ export async function requirePublish(verified: VerifiedBiscuit, targetService: s const token = wasm.Biscuit.fromBytes(verified.token, wasm.PublicKey.fromBytes(verified.verifyingKey, wasm.SignatureAlgorithm.Ed25519)); const builder = new wasm.AuthorizerBuilder(); builder.addFact(wasm.Fact.fromString(`${BASELINE_DATALOG.fact_publish}(${JSON.stringify(m[1])}, ${JSON.stringify(m[2])})`)); + // The mesh policy derives grants from the credential's role() and label() + // facts, so a policy change reaches this check before the provider's + // credential is minted again. + for (const r of policyRules) { + builder.addRule(wasm.Rule.fromString(r)); + } for (const policy of BASELINE_DATALOG.publish_policies) { builder.addPolicy(wasm.Policy.fromString(policy)); } diff --git a/sdk/js/src/mcp.test.ts b/sdk/js/src/mcp.test.ts index 1bc821b7..4300875a 100644 --- a/sdk/js/src/mcp.test.ts +++ b/sdk/js/src/mcp.test.ts @@ -278,6 +278,16 @@ test("only a node granted the service is its provider, as agentmesh-node's check await (await openMCPSession(conn, frame(""), [cpKey])).close(); } } + + // The mesh policy the session synced grants too, from the provider's + // role: a policy change reaches the caller at its next sync, not at + // the provider's next credential. + providerBiscuit = mint(provider.peerId.toString(), ROLE_NODE, { region: "eu" }, []); + const rules = [`granted_publish_set("mcp", ["calc"]) <- role(${JSON.stringify(ROLE_NODE)})`]; + let conn = await caller.dial(provider.getMultiaddrs()[0] as Parameters[0]); + await (await openMCPSession(conn, frame("mcp://calc"), [cpKey], { policyRules: rules })).close(); + conn = await caller.dial(provider.getMultiaddrs()[0] as Parameters[0]); + await assert.rejects(openMCPSession(conn, frame("mcp://weather"), [cpKey], { policyRules: rules }), /is not granted serving mcp:\/\/weather/); } finally { providerBiscuit = nodeBiscuit; } diff --git a/sdk/js/src/mcp.ts b/sdk/js/src/mcp.ts index 6f597d0c..92cede73 100644 --- a/sdk/js/src/mcp.ts +++ b/sdk/js/src/mcp.ts @@ -91,6 +91,8 @@ export interface MCPSessionOptions { /** Labels the provider's credential must all carry, e.g. { region: "eu", compliance: "gdpr" }. */ requiredLabels?: Record; signal?: AbortSignal; + /** The mesh policy as the session synced it (Session.policyRules); set by the session, not the caller. */ + policyRules?: readonly string[]; } /** An open MCP session with a provider on the mesh. */ @@ -174,7 +176,7 @@ export async function openMCPSession( // Only nodes host services; a router's or an admin's credential is a member, not a provider. requireRole(provider, ROLE_NODE); // And only a node the policy lets serve the named service is one. - await requirePublish(provider, decodeAuthFrame(frame).targetService); + await requirePublish(provider, decodeAuthFrame(frame).targetService, options.policyRules); requireLabels(provider, options.requiredLabels); requireEgressLabels(provider, egressRequireLabels); } catch (err) { diff --git a/sdk/js/src/session.ts b/sdk/js/src/session.ts index 17179095..3e3fe5d1 100644 --- a/sdk/js/src/session.ts +++ b/sdk/js/src/session.ts @@ -907,7 +907,7 @@ export class MeshSession { */ async openMCP(peer: Peer, targetService: string, options: MCPSessionOptions = {}): Promise { const conn = await this.connect(peer, options.signal); - return openMCPSession(conn, encodeAuthFrame(this.biscuit, targetService), this.mesh.credential.controlPlaneKeys, options, this.#egressRequireLabels); + return openMCPSession(conn, encodeAuthFrame(this.biscuit, targetService), this.mesh.credential.controlPlaneKeys, { ...options, policyRules: this.policyRules }, this.#egressRequireLabels); } /** Lists the tools a provider serves for a service. */ @@ -1006,12 +1006,13 @@ export class MeshSession { if (!this.#closed) { void this.keepRelay().catch(() => {}); } - if (this.endpoint !== undefined) { - try { - await this.syncPolicy(); - } catch (err) { - result.errors.push(`policy: ${err instanceof Error ? err.message : String(err)}`); - } + // Callers evaluate the policy too: it is what grants a provider the + // service it serves (requirePublish), not only what an A2A endpoint + // authorizes its callers with. + try { + await this.syncPolicy(); + } catch (err) { + result.errors.push(`policy: ${err instanceof Error ? err.message : String(err)}`); } return result; } @@ -1095,7 +1096,7 @@ export class MeshSession { /** * The mesh policy rules this member evaluates for callers, as the control * plane rendered them (PolicyConfigGetResponse.datalog_rules). Empty until - * acceptA2A() or syncPolicy(). + * the first control plane sync, acceptA2A() or syncPolicy(). */ get policyRules(): string[] { return this.#policyRules ?? []; @@ -1130,7 +1131,7 @@ export class MeshSession { const provider = await authenticateWithPeer(conn, this.mesh.authFrame(), this.mesh.credential.controlPlaneKeys); // Only nodes host services; a router's or an admin's credential is a member, not a provider. requireRole(provider, ROLE_NODE); - await requirePublish(provider, targetService); + await requirePublish(provider, targetService, this.policyRules); requireEgressLabels(provider, this.#egressRequireLabels); this.#egressVerdicts.set(verdict, new Date(Date.now() + EGRESS_VERDICT_TTL_MS)); return conn; diff --git a/sdk/js/src/sync.test.ts b/sdk/js/src/sync.test.ts index d9f24081..467fae44 100644 --- a/sdk/js/src/sync.test.ts +++ b/sdk/js/src/sync.test.ts @@ -44,6 +44,7 @@ import { KeysResponseSchema, MeshEvent_Type, MeshEventSchema, + PolicyConfigGetResponseSchema, TokenRefreshResponseSchema, } from "./gen/agentmesh_pb.ts"; import { Identity } from "./identity.ts"; @@ -125,6 +126,9 @@ class FakeControlPlane { ), ); } + if (req.method === "GET" && path === "/policies") { + return proto(toBinary(PolicyConfigGetResponseSchema, create(PolicyConfigGetResponseSchema, { datalogRules: ['granted_publish_all_types(true) <- role("mesh:role:node")'] }))); + } if (req.method === "POST" && path === "/refresh") { this.refreshes++; // The peer is whoever the presented biscuit is bound to; every valid key is tried. diff --git a/sdk/python/src/agent_mesh/biscuit.py b/sdk/python/src/agent_mesh/biscuit.py index 279a818e..8c7c3cd6 100644 --- a/sdk/python/src/agent_mesh/biscuit.py +++ b/sdk/python/src/agent_mesh/biscuit.py @@ -301,12 +301,15 @@ def require_role(verified: VerifiedBiscuit, role: str) -> None: raise BiscuitVerificationError(f"biscuit lacks expected role {role!r}") -def require_publish(verified: VerifiedBiscuit, target_service: str) -> None: +def require_publish(verified: VerifiedBiscuit, target_service: str, policy_rules: Sequence[str] = ()) -> None: """Requires that an already verified provider credential grants serving target_service ("://"), as identity.RequirePublish: the directory names whoever announced the service, only the credential says - who may serve it. The provider's own catalog names no service and needs - no grant; so does an empty target.""" + who may serve it. policy_rules is the mesh policy as the session synced + it; it derives grants from the credential's role() and label() facts, so + a policy change reaches this check before the provider's credential is + minted again. The provider's own catalog names no service and needs no + grant; so does an empty target.""" if target_service == "" or target_service == f"system://{_DATALOG['catalog_target']}": return scheme, sep, name = target_service.partition("://") @@ -316,6 +319,8 @@ def require_publish(verified: VerifiedBiscuit, target_service: str) -> None: builder = ba.AuthorizerBuilder() builder.set_limits(_limits()) builder.add_fact(ba.Fact(_DATALOG["fact_publish"] + "({t}, {n})", {"t": scheme, "n": name})) + for rule in policy_rules: + builder.add_rule(ba.Rule(rule)) for policy in _DATALOG["publish_policies"]: builder.add_policy(ba.Policy(policy)) try: diff --git a/sdk/python/src/agent_mesh/mcp_client.py b/sdk/python/src/agent_mesh/mcp_client.py index 3080efc2..5569e8f2 100644 --- a/sdk/python/src/agent_mesh/mcp_client.py +++ b/sdk/python/src/agent_mesh/mcp_client.py @@ -112,11 +112,13 @@ async def open_mcp_session( *, required_labels: Optional[Mapping[str, str]] = None, egress_require_labels: Optional[Mapping[str, str]] = None, + policy_rules: Sequence[str] = (), ) -> AsyncIterator[tuple[ClientSession, VerifiedBiscuit]]: """Opens /mesh/mcp/1.0.0 to a connected provider with `frame`, this member's AuthFrame naming the service, verifies the provider and yields an initialized MCP ClientSession with the provider's credential; egress_require_labels - is the session's, not the caller's (require_egress_labels).""" + and policy_rules are the session's, not the caller's (require_egress_labels, + Session.policy_rules).""" stream = await open_stream(host, peer_id, MCP_PROTOCOL, AUTH_HANDSHAKE_TIMEOUT) try: with trio.fail_after(AUTH_HANDSHAKE_TIMEOUT): @@ -133,7 +135,7 @@ async def open_mcp_session( # Only nodes host services; a router's or an admin's credential is a member, not a provider. require_role(provider, ROLE_NODE) # And only a node the policy lets serve the named service is one. - require_publish(provider, pb.AuthFrame.FromString(frame).target_service) + require_publish(provider, pb.AuthFrame.FromString(frame).target_service, policy_rules) except BiscuitVerificationError as err: raise AuthRejectedError(str(peer_id), f"provider credential rejected: {err}") from err require_labels(provider, required_labels) diff --git a/sdk/python/src/agent_mesh/session.py b/sdk/python/src/agent_mesh/session.py index 9d2a74aa..5345482f 100644 --- a/sdk/python/src/agent_mesh/session.py +++ b/sdk/python/src/agent_mesh/session.py @@ -732,7 +732,13 @@ async def opened() -> AsyncIterator[tuple[ClientSession, VerifiedBiscuit]]: peer_id = await self.connect(peer) frame = encode_auth_frame(self.biscuit, target_service) async with open_mcp_session( - self.host, peer_id, frame, self.mesh.credential.control_plane_keys, required_labels=required_labels, egress_require_labels=self.egress_require_labels + self.host, + peer_id, + frame, + self.mesh.credential.control_plane_keys, + required_labels=required_labels, + egress_require_labels=self.egress_require_labels, + policy_rules=self.policy_rules, ) as opened_session: yield opened_session @@ -757,7 +763,7 @@ async def call_tool( def policy_rules(self) -> list[str]: """The mesh policy rules this member evaluates for callers, as the control plane rendered them (PolicyConfigGetResponse.datalog_rules). - Empty until accept_a2a() or sync_policy().""" + Empty until the first control plane sync, accept_a2a() or sync_policy().""" return list(self._policy_rules or []) async def sync_policy(self) -> None: @@ -781,11 +787,13 @@ async def sync(self) -> "ControlPlaneSync": if len(self._held_router_ids) < self._want_routers(): await self.top_up_routers() self._relay_trigger.set() - if self.endpoint is not None: - try: - await self.sync_policy() - except Exception as err: # noqa: BLE001 - the last good policy stays in force - result.errors.append(f"policy: {err}") + # Callers evaluate the policy too: it is what grants a provider the + # service it serves (require_publish), not only what an A2A + # endpoint authorizes its callers with. + try: + await self.sync_policy() + except Exception as err: # noqa: BLE001 - the last good policy stays in force + result.errors.append(f"policy: {err}") return result def trigger_sync(self) -> None: @@ -1014,7 +1022,7 @@ async def _egress_peer(self, peer: Peer, target_service: str) -> ID: provider = await authenticate_with_peer(self.host, peer_id, self.mesh.auth_frame(), self.mesh.credential.control_plane_keys) # Only nodes host services; a router's or an admin's credential is a member, not a provider. require_role(provider, ROLE_NODE) - require_publish(provider, target_service) + require_publish(provider, target_service, self.policy_rules) require_egress_labels(provider, self.egress_require_labels) self._egress_verdicts[verdict] = time.monotonic() + EGRESS_VERDICT_TTL return peer_id diff --git a/sdk/python/tests/test_mcp.py b/sdk/python/tests/test_mcp.py index 9325feae..0523eb95 100644 --- a/sdk/python/tests/test_mcp.py +++ b/sdk/python/tests/test_mcp.py @@ -266,6 +266,18 @@ async def main(): pass async with open_mcp_session(caller, other.get_id(), frame(caller_biscuit, ""), [CP_KEY]): pass + + # The mesh policy the session synced grants too, from the + # provider's role: a policy change reaches the caller at its + # next sync, not at the provider's next credential. + ungranted, ungranted_addr, _ = await start_provider(nursery, publish=()) + await caller.connect(info_from_p2p_addr(ungranted_addr)) + rules = [f'granted_publish_set("mcp", ["calc"]) <- role("{ROLE_NODE}")'] + async with open_mcp_session(caller, ungranted.get_id(), frame(caller_biscuit, "mcp://calc"), [CP_KEY], policy_rules=rules): + pass + with pytest.raises(AuthRejectedError, match="is not granted serving mcp://weather"): + async with open_mcp_session(caller, ungranted.get_id(), frame(caller_biscuit, "mcp://weather"), [CP_KEY], policy_rules=rules): + pass nursery.cancel_scope.cancel() async def with_timeout(): diff --git a/sdk/python/tests/test_sync.py b/sdk/python/tests/test_sync.py index 709f43df..f5745875 100644 --- a/sdk/python/tests/test_sync.py +++ b/sdk/python/tests/test_sync.py @@ -101,6 +101,8 @@ def transport(self, method, url, headers, body): # type: ignore[no-untyped-def] return 200, pb.KeysResponse(public_keys=[k.pub for k in self.keys], sign_time=unsigned.sign_time, signatures=[k.identity.sign(payload) for k in self.keys]).SerializeToString() if (method, path) == ("GET", "/info"): return 200, pb.ControlPlaneInfoResponse(router_addresses=[self.router_addr], banned_peer_ids=self.banned).SerializeToString() + if (method, path) == ("GET", "/policies"): + return 200, pb.PolicyConfigGetResponse(datalog_rules=['granted_publish_all_types(true) <- role("mesh:role:node")']).SerializeToString() if (method, path) == ("POST", "/refresh"): self.refreshes += 1 presented = base64.b64decode(headers["Authorization"].removeprefix("Bearer ")) diff --git a/site/content/docs/reference/policy.md b/site/content/docs/reference/policy.md index bdabc4c8..ea14a783 100644 --- a/site/content/docs/reference/policy.md +++ b/site/content/docs/reference/policy.md @@ -121,6 +121,17 @@ An egress destination needs no entry: `served_by` selects the nodes that serve it, and the control plane mints `granted_publish_exact("egress", "")` into their credentials with the destination. +A change to `allowed_publish` takes effect without a new credential. The +grant is also rendered into the policy text as +`granted_publish_*(...) <- role("")` (and `<- label(...)` for +`served_by`), and every verifier adds that text to its authorizer: the +router, a node and an SDK member at their next sync, all of them at once on +the `POLICY_UPDATE` event +the control plane publishes. A node whose policy text changed announces its +services again, so a record the router refused under the previous policy is +in the directory within the sync interval; until then the router answers +`credential does not grant serving `, which the node logs. + ### Target patterns `fact:value`, where `fact` is one of the identity facts that a node's @@ -343,8 +354,9 @@ At enrollment, refresh, and token exchange, the control plane resolves the identity's roles from the bindings and mints into the credential one `role()` fact per role and the compiled `granted_*` facts of every role. The control plane also renders the policy as Datalog rules served as `datalog_rules` at -`GET /policies`. Nodes fetch this text (`--control-plane-sync-interval`, 15 -minutes) and add it to their authorizer as it arrives. +`GET /policies`. Nodes, routers and SDK members fetch this text +(`--control-plane-sync-interval`, 15 minutes) and add it to their authorizer +as it arrives. At the destination node, in this order: diff --git a/site/content/docs/reference/router.md b/site/content/docs/reference/router.md index 32eada20..390bb4f1 100644 --- a/site/content/docs/reference/router.md +++ b/site/content/docs/reference/router.md @@ -77,7 +77,9 @@ replica count. closest to the key, as many as the mesh's replication (`/info`). A record is kept only for a service the member's credential grants serving (the role's `allowed_publish`), checked against the credential the - handshake presented; a record for anything else is refused. A record + handshake presented and the mesh policy the router synced, so a grant + added to a role reaches the directory at the next pass; a record for + anything else is refused. A record lives for the mesh's record TTL after its last announcement, and an announcement replaces the addresses of the one before; a member that stops a service or leaves withdraws its records, and the router diff --git a/tests/integration/publish_grant_test.go b/tests/integration/publish_grant_test.go new file mode 100644 index 00000000..852cb4a3 --- /dev/null +++ b/tests/integration/publish_grant_test.go @@ -0,0 +1,151 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package integration_test + +import ( + "bytes" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "slices" + "strings" + "testing" + "time" + + "github.com/google/agentmesh/api" +) + +// TestUngrantedServiceIsNeitherPublishedNorServed pins the publish grant end +// to end, from the policy to both verifiers. The node role grants +// mcp://granted and nothing else; a node declares that service and a second +// one, mcp://ungranted, both running. The router the node holds keeps the +// granted record and refuses the other, so no caller finds the second by +// name, and a caller that names the node by peer ID anyway is refused by its +// own node before the request leaves it. Then the policy grants the second +// service too, and nothing is restarted: the POLICY_UPDATE event brings the +// change to the router, the provider and the caller, the provider announces +// again, and the service is found and called. +func TestUngrantedServiceIsNeitherPublishedNorServed(t *testing.T) { + m := startAttachmentMesh(t) + + grant := func(publish string) { + t.Helper() + policyFile := filepath.Join(m.tmpDir, "publish-policy.yaml") + policy := fmt.Sprintf(`roles: + - name: %s + allowed_services: [] + allowed_targets: ["*"] + - name: %s + allowed_services: ["mcp://*"] + allowed_publish: [%q] + allowed_targets: ["*"] +bindings: + - role: %s + members: ["group:routers"] + - role: %s + members: ["user:mock-user"] +`, api.RoleRouter, api.RoleNode, publish, api.RoleRouter, api.RoleNode) + if err := os.WriteFile(policyFile, []byte(policy), 0o644); err != nil { + t.Fatal(err) + } + injectPolicyYAML(t, m.cpPort, testAdminToken, policyFile) + } + grant("mcp://granted") + + router := m.startRouter(t, "router") + + mcpServer := httptest.NewServer(newBoundaryMCPHandler(t)) + t.Cleanup(mcpServer.Close) + + // A node acts on a POLICY_UPDATE event after a random delay of up to a + // tenth of its sync interval, so a fleet does not pull together; the + // default 15 minutes would make that up to 90 seconds here. + common := append(m.nodeArgs(), "--allow-loopback", "--control-plane-sync-interval", "5s") + provider := launchNode(t, m.nodeBin, m.nodeEnv("provider"), filepath.Join(m.tmpDir, "provider"), append(slices.Clone(common), + "--api-token-path", tokenPath(t, "token-provider"), + "--config", writeNodeConfig(t, m.tmpDir, nil, + svcDecl{Type: "mcp", Name: "granted", TargetURL: mcpServer.URL}, + svcDecl{Type: "mcp", Name: "ungranted", TargetURL: mcpServer.URL}), + )...) + caller := launchNode(t, m.nodeBin, m.nodeEnv("caller"), filepath.Join(m.tmpDir, "caller"), append(slices.Clone(common), + "--api-token-path", tokenPath(t, "token-caller"), + )...) + provider.waitForAPI(t) + caller.waitForAPI(t) + + // The granted service is found by name and answers. + callAcrossMesh(t, caller, "token-caller", provider, "granted") + + // The router refused the other service's records, by name and by type, + // and said so; the node logged the refusal it got back. + denied := `agentmesh_router_directory_requests_total{op="provide",result="denied"}` + deadline := time.Now().Add(10 * time.Second) + for router.gauge(t, denied) < 2 { + if time.Now().After(deadline) { + t.Fatalf("router counted %v denied records, want 2 (mcp://ungranted by name and by type)\n--- provider ---\n%s", router.gauge(t, denied), provider.log()) + } + time.Sleep(200 * time.Millisecond) + } + if log := provider.log(); !strings.Contains(log, "credential does not grant serving mcp://ungranted") { + t.Fatalf("the provider did not log the refusal\n--- provider ---\n%s", log) + } + + client := &http.Client{Timeout: 5 * time.Second} + discover, _ := http.NewRequest(http.MethodGet, "http://"+caller.apiAddr+"/mesh/service/discover?type=mcp&name=ungranted", nil) + discover.Header.Set(api.HeaderMeshAuthentication, "Bearer token-caller") + resp, err := client.Do(discover) + if err != nil { + t.Fatal(err) + } + body, _ := io.ReadAll(resp.Body) + _ = resp.Body.Close() + if strings.Contains(string(body), provider.peerID.String()) { + t.Fatalf("mcp://ungranted is in the directory: %s", body) + } + + // Named by peer ID, the caller's own node checks the provider's + // credential and refuses before anything crosses the mesh. + call, _ := http.NewRequest(http.MethodPost, fmt.Sprintf("http://%s/mesh/%s/mcp/ungranted", caller.apiAddr, provider.peerID), + bytes.NewBufferString(`{"jsonrpc":"2.0","id":1,"method":"tools/list"}`)) + call.Header.Set("Content-Type", "application/json") + call.Header.Set("Accept", "application/json, text/event-stream") + call.Header.Set(api.HeaderMeshAuthentication, "Bearer token-caller") + resp, err = client.Do(call) + if err != nil { + t.Fatal(err) + } + body, _ = io.ReadAll(resp.Body) + _ = resp.Body.Close() + if resp.StatusCode != http.StatusForbidden || !strings.Contains(string(body), "provider is not granted mcp://ungranted") { + t.Fatalf("call to mcp://ungranted by peer ID: %d %s, want 403 naming the missing grant\n--- caller ---\n%s", resp.StatusCode, body, caller.log()) + } + + // The policy now grants every mcp service. No credential is minted + // again: the grant travels in the policy text, the event brings it to + // the router and both nodes, the provider announces again, and the + // service is found and called. + grant("mcp://*") + deadline = time.Now().Add(10 * time.Second) + for !strings.Contains(provider.log(), "Mesh policy changed; announcing this node's records again") { + if time.Now().After(deadline) { + t.Fatalf("the provider did not learn of the policy change\n--- provider ---\n%s", provider.log()) + } + time.Sleep(200 * time.Millisecond) + } + callAcrossMesh(t, caller, "token-caller", provider, "ungranted") +} diff --git a/tests/integration/router_attachment_test.go b/tests/integration/router_attachment_test.go index 3a438e17..6440e066 100644 --- a/tests/integration/router_attachment_test.go +++ b/tests/integration/router_attachment_test.go @@ -53,6 +53,10 @@ type routerFixture struct { // test sets it before starting its routers and clears it after. var extraRouterArgs []string +// extraControlPlaneArgs is appended to the control plane startAttachmentMesh +// starts; a test sets it before starting the mesh and clears it after. +var extraControlPlaneArgs []string + // gauge reads one metric from the router's /metrics, by its full name with // labels, and fails when it is not there. func (r *routerFixture) gauge(t *testing.T, name string) float64 { @@ -374,14 +378,14 @@ bindings: } m.cpPort = getFreePort(t) - cpCmd := exec.Command(cpBin, + cpCmd := exec.Command(cpBin, append([]string{ "--bind-address", fmt.Sprintf("127.0.0.1:%d", m.cpPort), "--admin-token-path", tokenPath(t, testAdminToken), - "--db-dsn", filepath.Join(m.tmpDir, "cp.db")+"?_pragma=journal_mode(DELETE)&_pragma=busy_timeout(5000)", + "--db-dsn", filepath.Join(m.tmpDir, "cp.db") + "?_pragma=journal_mode(DELETE)&_pragma=busy_timeout(5000)", "--issuer", oidcURL, "--insecure-skip-tls-verify", "--mesh-reconnect-interval", "500ms", - ) + }, extraControlPlaneArgs...)...) cpLog := filepath.Join(m.tmpDir, "control-plane.log") cpLogFile, err := os.Create(cpLog) if err != nil { From 0a297554135d08c819011155d9ffb72767e552f8 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Wed, 14 Oct 2026 16:03:25 +0000 Subject: [PATCH 7/7] router: one control plane sync loop, --control-plane-sync-interval The router held five conversations with the control plane, each in its own goroutine on its own clock: the lease every --lease-renew-interval, /info every 30 seconds, /keys every --keys-sync-interval, /policies every 15 minutes, and a credential expiry check every 10 minutes. A node has had one pass for all of this since the start (SyncControlPlane). The router now has one too. Every --control-plane-sync-interval (5 minutes, what the lease was renewed at) and at once on a KEY_ROTATION or POLICY_UPDATE event, a pass re-reads /keys first, so a credential the control plane no longer signs for is refreshed before it is presented, then renews the lease, re-reads /info and /policies, and refreshes the router's own credential when less than a fifth of its lifetime is left. While /info names a router this one has not authenticated with, the federation step alone is retried every 2 seconds, as before; a dead listed router costs the control plane no more than it did. What has to be quick does not wait for the interval: a new router reads /info on its own first pass and handshakes with every listed router, a lookup that finds nothing re-reads /info (refederateIfStale), and an event runs the pass now. The control plane sees four requests per router per interval. --keys-sync-interval and --lease-renew-interval are gone, replaced by --control-plane-sync-interval, the node's flag name; agentmesh-one's --router-keys-sync-interval and --router-lease-renew-interval by --router-control-plane-sync-interval. Tests that set either now set the one interval to the shorter of the two. --- cmd/agentmesh-one/main.go | 3 +- cmd/agentmesh-router/main.go | 57 +++--- internal/router/config.go | 52 ++--- internal/router/router.go | 201 ++++++++----------- internal/router/router_test.go | 131 ++++++------ internal/router/trust_test.go | 31 ++- internal/standalone/standalone.go | 34 ++-- site/content/docs/concepts/identity.md | 8 +- site/content/docs/reference/agentmesh-one.md | 3 +- site/content/docs/reference/policy.md | 8 +- site/content/docs/reference/router.md | 15 +- tests/integration/failover_test.go | 4 +- tests/integration/minimal_helpers_test.go | 2 +- tests/integration/readmission_test.go | 15 +- tests/integration/revocation_test.go | 3 +- tests/integration/rotation_test.go | 3 +- tests/integration/router_attachment_test.go | 2 +- tests/integration/router_shutdown_test.go | 2 +- 18 files changed, 272 insertions(+), 302 deletions(-) diff --git a/cmd/agentmesh-one/main.go b/cmd/agentmesh-one/main.go index 9247fb3c..0e93a76e 100644 --- a/cmd/agentmesh-one/main.go +++ b/cmd/agentmesh-one/main.go @@ -234,8 +234,7 @@ func main() { rootCmd.Flags().BoolVar(&cpTunables.ManualEnrollment, "control-plane-manual-enrollment", false, "Queue bootstrap enrollments for admin approval instead of auto-approving") // Embedded router tunables. - rootCmd.Flags().DurationVar(&routerTunables.KeysSyncInterval, "router-keys-sync-interval", 0, "Biscuit public key refresh interval (0 keeps the component default)") - rootCmd.Flags().DurationVar(&routerTunables.LeaseRenewInterval, "router-lease-renew-interval", 0, "Lease renewal interval (0 keeps the component default)") + rootCmd.Flags().DurationVar(&routerTunables.ControlPlaneSyncInterval, "router-control-plane-sync-interval", 0, "How often the router renews its lease and pulls keys, routers, bans and policy from the control plane (0 keeps the component default)") rootCmd.Flags().IntVar(&routerTunables.LowWaterMark, "router-low-watermark", 0, "Connection manager low watermark (0 is 15% below the high watermark)") rootCmd.Flags().IntVar(&routerTunables.HighWaterMark, "router-high-watermark", 0, "Connection manager high watermark (0 keeps the component default)") rootCmd.Flags().IntVar(&routerTunables.ConnsPerSourceIP, "router-conns-per-source-ip", 0, "Per-source-IP connection budget (0 follows the high watermark; proxied peers share source IPs)") diff --git a/cmd/agentmesh-router/main.go b/cmd/agentmesh-router/main.go index ff801fb7..dc0f0962 100644 --- a/cmd/agentmesh-router/main.go +++ b/cmd/agentmesh-router/main.go @@ -51,32 +51,31 @@ func init() { } var ( - controlPlaneURL string - insecureControlPlane bool - listenAddrs []string - externalAddrs []string - keysSyncInterval time.Duration - leaseRenewInterval time.Duration - oidcToken string - bootstrapToken string - bootstrapTokenPath string - jwtPath string - keysPath string - allowLoopback bool - connsPerSourceIP int - logLevel string - logFormat string - lowWaterMark int - highWaterMark int - metricsAddr string - pprofAddr string - relayLimitDuration time.Duration - relayLimitData router.ByteSize - relayMaxReservations int - relayMaxCircuits int - shutdownLeaseTTL time.Duration - authDeadline time.Duration - labelFlags []string + controlPlaneURL string + insecureControlPlane bool + listenAddrs []string + externalAddrs []string + controlPlaneSyncInterval time.Duration + oidcToken string + bootstrapToken string + bootstrapTokenPath string + jwtPath string + keysPath string + allowLoopback bool + connsPerSourceIP int + logLevel string + logFormat string + lowWaterMark int + highWaterMark int + metricsAddr string + pprofAddr string + relayLimitDuration time.Duration + relayLimitData router.ByteSize + relayMaxReservations int + relayMaxCircuits int + shutdownLeaseTTL time.Duration + authDeadline time.Duration + labelFlags []string ) var logger = golog.Logger("agentmesh-router-cli") @@ -94,8 +93,7 @@ func main() { AllowInsecureControlPlane: insecureControlPlane, ListenAddrs: listenAddrs, ExternalAddrs: externalAddrs, - KeysSyncInterval: keysSyncInterval, - LeaseRenewInterval: leaseRenewInterval, + ControlPlaneSyncInterval: controlPlaneSyncInterval, OIDCToken: oidcToken, BootstrapToken: bootstrapToken, BootstrapTokenPath: bootstrapTokenPath, @@ -150,8 +148,7 @@ func main() { rootCmd.Flags().BoolVar(&insecureControlPlane, "insecure-control-plane", false, "Accept a plaintext http:// control plane URL to a non-loopback host (whoever answers it becomes this router's trust root; only for networks you already trust)") rootCmd.Flags().StringSliceVar(&listenAddrs, "listen", []string{"/ip4/0.0.0.0/tcp/5001", "/ip6/::/tcp/5001"}, "libp2p Listen Addresses") rootCmd.Flags().StringSliceVar(&externalAddrs, "external-addr", []string{}, "External addresses to announce to control plane") - rootCmd.Flags().DurationVar(&keysSyncInterval, "keys-sync-interval", 5*time.Minute, "Key synchronization polling interval") - rootCmd.Flags().DurationVar(&leaseRenewInterval, "lease-renew-interval", 300*time.Second, "Lease renewal registration interval") + rootCmd.Flags().DurationVar(&controlPlaneSyncInterval, "control-plane-sync-interval", router.DefaultControlPlaneSyncInterval, "How often the lease is renewed and signing keys, the router list, bans and the mesh policy are pulled from the control plane; a mesh event pulls at once. Keep it well below the control plane's --lease-duration and --key-grace-period.") rootCmd.Flags().StringVar(&oidcToken, "oidc-token", "", "OIDC ID token for enrollment") rootCmd.Flags().StringVar(&bootstrapToken, "bootstrap-token", "", "Pre-shared bootstrap token for enrollment") rootCmd.Flags().StringVar(&bootstrapTokenPath, "bootstrap-token-path", "", "Path to file containing bootstrap token for enrollment") diff --git a/internal/router/config.go b/internal/router/config.go index 915c72f3..382ee4eb 100644 --- a/internal/router/config.go +++ b/internal/router/config.go @@ -35,25 +35,33 @@ const DefaultShutdownLeaseTTL = 30 * time.Second // less. const DefaultAuthDeadline = time.Minute +// DefaultControlPlaneSyncInterval is how often a router talks to the control +// plane when nothing happens: the lease is renewed, and /info, /keys and +// /policies re-read, every five minutes. What has to be quick does not wait +// for it: a mesh event runs a pass at once, a new router reads /info on its +// own first pass and handshakes with every listed router, and a lookup that +// finds nothing re-reads /info (refederateIfStale). +const DefaultControlPlaneSyncInterval = 5 * time.Minute + // Options holds configuration details for the agentmesh-router. type Options struct { - ControlPlaneURL string - ListenAddrs []string - ExternalAddrs []string - KeysSyncInterval time.Duration - PolicySyncInterval time.Duration - LeaseRenewInterval time.Duration - OIDCProvider string // For OIDC enrollment - OIDCToken string // OIDC JWT token for enrollment - BootstrapToken string // Pre-shared bootstrap token for enrollment - BootstrapTokenPath string // File path containing pre-shared bootstrap token - JWTPath string // File path containing OIDC JWT token - KeysDBPath string // Path to save peer private key - AllowLoopback bool - BiscuitTimeout time.Duration - LogVerbose bool - LowWaterMark int - HighWaterMark int + ControlPlaneURL string + ListenAddrs []string + ExternalAddrs []string + // ControlPlaneSyncInterval is how often the router's one pass over the + // control plane runs: lease, /info, /keys, /policies, credential expiry. + ControlPlaneSyncInterval time.Duration + OIDCProvider string // For OIDC enrollment + OIDCToken string // OIDC JWT token for enrollment + BootstrapToken string // Pre-shared bootstrap token for enrollment + BootstrapTokenPath string // File path containing pre-shared bootstrap token + JWTPath string // File path containing OIDC JWT token + KeysDBPath string // Path to save peer private key + AllowLoopback bool + BiscuitTimeout time.Duration + LogVerbose bool + LowWaterMark int + HighWaterMark int // RelayLimitDuration / RelayLimitData cap each relayed connection; 0 is // no limit, so Default() leaves them alone. RelayLimitDuration time.Duration @@ -119,14 +127,8 @@ func (o *Options) Default() { if len(o.ListenAddrs) == 0 { o.ListenAddrs = []string{"/ip4/0.0.0.0/tcp/5001", "/ip6/::/tcp/5001"} } - if o.KeysSyncInterval <= 0 { - o.KeysSyncInterval = 5 * time.Minute - } - if o.PolicySyncInterval <= 0 { - o.PolicySyncInterval = 15 * time.Minute - } - if o.LeaseRenewInterval <= 0 { - o.LeaseRenewInterval = 30 * time.Second + if o.ControlPlaneSyncInterval <= 0 { + o.ControlPlaneSyncInterval = DefaultControlPlaneSyncInterval } if o.HighWaterMark <= 0 { o.HighWaterMark = DefaultHighWaterMark diff --git a/internal/router/router.go b/internal/router/router.go index 20c39f18..d0821de1 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -379,8 +379,9 @@ type Router struct { // only the last biscuit it issued. refreshMu sync.Mutex - keysSyncTrigger chan struct{} - policySyncTrigger chan struct{} + // syncTrigger asks the control plane sync loop for a pass now; a mesh + // event sends on it. + syncTrigger chan struct{} // Control contexts ctx context.Context @@ -415,12 +416,11 @@ func NewRouter(ctx context.Context, config Options) (*Router, error) { } return &Router{ - config: config, - ctx: ctx, - cancel: cancel, - handshakeLimiter: handshakeLimiter, - keysSyncTrigger: make(chan struct{}, 1), - policySyncTrigger: make(chan struct{}, 1), + config: config, + ctx: ctx, + cancel: cancel, + handshakeLimiter: handshakeLimiter, + syncTrigger: make(chan struct{}, 1), }, nil } @@ -599,12 +599,8 @@ func (r *Router) Start() (retErr error) { }) // 5. Start background routines - r.wg.Add(9) - go r.runLeaseRenewalLoop() - go r.runKeysSyncLoop() - go r.runPolicySyncLoop() - go r.runFederationLoop() - go r.runBiscuitRenewalLoop() + r.wg.Add(5) + go r.runControlPlaneSyncLoop() go r.runShedLoop() go r.listenForControlPlaneEvents(r.ctx) go func() { @@ -1049,65 +1045,93 @@ func (r *Router) listenForControlPlaneEvents(ctx context.Context) { } } case api.MeshEvent_KEY_ROTATION: - logger.Infof("[Router Event] Received KEY_ROTATION event, triggering key sync") - r.triggerKeysSync() + logger.Infof("[Router Event] Received KEY_ROTATION event, triggering control plane sync") + r.triggerSync() case api.MeshEvent_POLICY_UPDATE: - logger.Infof("[Router Event] Received POLICY_UPDATE event, triggering policy sync") - r.triggerPolicySync() + logger.Infof("[Router Event] Received POLICY_UPDATE event, triggering control plane sync") + r.triggerSync() } } } -func (r *Router) triggerKeysSync() { +func (r *Router) triggerSync() { select { - case r.keysSyncTrigger <- struct{}{}: + case r.syncTrigger <- struct{}{}: default: } } -func (r *Router) runKeysSyncLoop() { +// runControlPlaneSyncLoop is the router's one conversation with the control +// plane. Every ControlPlaneSyncInterval, and at once on a mesh event, a pass +// re-reads /keys, renews the lease, re-reads /info (the routers to federate +// with and the ban set), re-reads /policies and refreshes the router's own +// credential when it nears expiry. Between passes, while a listed router is +// not yet authenticated, the federation step alone is retried every +// federationCatchUp, so a router that cannot be reached costs the control +// plane nothing beyond the pass. +func (r *Router) runControlPlaneSyncLoop() { defer r.wg.Done() - ticker := time.NewTicker(r.config.KeysSyncInterval) - defer ticker.Stop() - + timer := time.NewTimer(0) + defer timer.Stop() + var lastPass time.Time for { + now := false select { - case <-ticker.C: - case <-r.keysSyncTrigger: + case <-timer.C: + case <-r.syncTrigger: + now = true + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } case <-r.ctx.Done(): return } - if err := r.syncKeys(); err != nil { - logger.Errorf("Failed keys sync: %v", err) + if now || time.Since(lastPass) >= r.config.ControlPlaneSyncInterval { + r.syncControlPlane() + lastPass = time.Now() + } else { + r.connectBootstrapRouters() + } + next := r.config.ControlPlaneSyncInterval - time.Since(lastPass) + if listed, authenticated := r.peerRouterCounts(); authenticated < listed && federationCatchUp < next { + next = federationCatchUp } + timer.Reset(max(next, 0)) } } -func (r *Router) triggerPolicySync() { - select { - case r.policySyncTrigger <- struct{}{}: - default: +// syncControlPlane is one pass of the sync loop. Keys come first: a lease +// or a policy read presented with a credential the control plane no longer +// signs for is refused until the rotation is learned and the credential +// refreshed (syncKeys). +func (r *Router) syncControlPlane() { + if err := r.syncKeys(); err != nil { + logger.Errorf("Failed keys sync: %v", err) + } + r.renewLease() + r.connectBootstrapRouters() + if err := r.syncPolicy(); err != nil { + logger.Errorf("Failed policy sync: %v", err) } + r.refreshExpiringCredential() } -// runPolicySyncLoop pulls the mesh policy at start, on each POLICY_UPDATE -// event and every PolicySyncInterval: the event is best effort, the pull is -// what bounds how long a policy change takes to reach this router. -func (r *Router) runPolicySyncLoop() { - defer r.wg.Done() - ticker := time.NewTicker(r.config.PolicySyncInterval) - defer ticker.Stop() - - for { - if err := r.syncPolicy(); err != nil { - logger.Errorf("Failed policy sync: %v", err) - } - select { - case <-ticker.C: - case <-r.policySyncTrigger: - case <-r.ctx.Done(): - return - } +// refreshExpiringCredential refreshes the router's own credential once less +// than a fifth of its lifetime is left, so a credential is never presented +// close to its expiry. +func (r *Router) refreshExpiringCredential() { + r.keysMu.RLock() + expiration := r.credential.expiration + r.keysMu.RUnlock() + if expiration.IsZero() || time.Until(expiration) >= api.BiscuitTokenTTL/5 { + return + } + logger.Infof("Router biscuit expiring in %v, triggering proactive refresh...", time.Until(expiration)) + if err := r.RefreshEnrollment(r.ctx); err != nil { + logger.Errorf("Proactive router biscuit refresh failed: %v", err) } } @@ -1157,24 +1181,6 @@ func (r *Router) meshPolicyRules() []biscuit.Rule { return nil } -func (r *Router) runLeaseRenewalLoop() { - defer r.wg.Done() - ticker := time.NewTicker(r.config.LeaseRenewInterval) - defer ticker.Stop() - - // Initial renewal after host is online - r.renewLease() - - for { - select { - case <-ticker.C: - r.renewLease() - case <-r.ctx.Done(): - return - } - } -} - // renewLease extends the lease for the control plane's duration. Once the // router is shutting down it does nothing: the withdrawal sent by Close is // the last word, and a renewal racing it would re-advertise a router that @@ -1414,35 +1420,12 @@ func (r *Router) reconcileBannedPeers(peerIDs []string, fetchedAt time.Time) { } } -// federationInterval is how often a router reads the control plane's list -// of routers and authenticates with the ones it has not; federationCatchUp -// is the interval while the list names a router it has not reached yet, -// which it does at start, when the other routers started at the same -// moment and leased after this one's first pass, and whenever a router -// joins. -const ( - federationInterval = 30 * time.Second - federationCatchUp = 2 * time.Second -) - -func (r *Router) runFederationLoop() { - defer r.wg.Done() - timer := time.NewTimer(0) - defer timer.Stop() - for { - select { - case <-timer.C: - r.connectBootstrapRouters() - next := federationInterval - if listed, authenticated := r.peerRouterCounts(); authenticated < listed { - next = federationCatchUp - } - timer.Reset(next) - case <-r.ctx.Done(): - return - } - } -} +// federationCatchUp is how often the sync loop retries the federation step +// alone while the control plane's list names a router this one has not +// authenticated with yet, which it does at start, when the other routers +// started at the same moment and leased after this one's first pass, and +// whenever a router joins. +const federationCatchUp = 2 * time.Second func (r *Router) connectBootstrapRouters() { // Taken before the request: anything banned after this point cannot be @@ -2017,27 +2000,3 @@ func (r *Router) verifyOwnBiscuit(token []byte, peerID peer.ID) error { } return identity.VerifyBiscuitRole(token, key, r.config.RequiredRole, r.config.BiscuitTimeout) } - -func (r *Router) runBiscuitRenewalLoop() { - defer r.wg.Done() - ticker := time.NewTicker(api.TokenRefreshCheckInterval) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - r.keysMu.RLock() - expiration := r.credential.expiration - r.keysMu.RUnlock() - - if !expiration.IsZero() && time.Until(expiration) < api.BiscuitTokenTTL/5 { // 80% elapsed lifespan of 24h (remaining time < 20% of TTL) - logger.Infof("Router biscuit expiring in %v, triggering proactive refresh...", time.Until(expiration)) - if err := r.RefreshEnrollment(r.ctx); err != nil { - logger.Errorf("Proactive router biscuit refresh failed: %v", err) - } - } - case <-r.ctx.Done(): - return - } - } -} diff --git a/internal/router/router_test.go b/internal/router/router_test.go index 279b42c5..874a156a 100644 --- a/internal/router/router_test.go +++ b/internal/router/router_test.go @@ -184,14 +184,13 @@ func TestRouterIntegration(t *testing.T) { // 1. Create and Start Router rOpts := Options{ - ControlPlaneURL: cpURL, - ListenAddrs: []string{"/ip4/127.0.0.1/tcp/0"}, - KeysSyncInterval: 2 * time.Second, - LeaseRenewInterval: 2 * time.Second, - OIDCToken: routerJWT, - KeysDBPath: routerKeyPath, - AllowLoopback: true, - BiscuitTimeout: 1 * time.Second, + ControlPlaneURL: cpURL, + ListenAddrs: []string{"/ip4/127.0.0.1/tcp/0"}, + ControlPlaneSyncInterval: 2 * time.Second, + OIDCToken: routerJWT, + KeysDBPath: routerKeyPath, + AllowLoopback: true, + BiscuitTimeout: 1 * time.Second, } r, err := NewRouter(context.Background(), rOpts) @@ -354,14 +353,13 @@ func TestRouterFederation(t *testing.T) { r1KeyPath := filepath.Join(tempDir, "router1.key") r1, err := NewRouter(context.Background(), Options{ - ControlPlaneURL: cpURL, - ListenAddrs: []string{"/ip4/127.0.0.1/tcp/0"}, - KeysSyncInterval: 2 * time.Second, - LeaseRenewInterval: 2 * time.Second, - OIDCToken: router1JWT, - KeysDBPath: r1KeyPath, - AllowLoopback: true, - BiscuitTimeout: 1 * time.Second, + ControlPlaneURL: cpURL, + ListenAddrs: []string{"/ip4/127.0.0.1/tcp/0"}, + ControlPlaneSyncInterval: 2 * time.Second, + OIDCToken: router1JWT, + KeysDBPath: r1KeyPath, + AllowLoopback: true, + BiscuitTimeout: 1 * time.Second, }) if err != nil { t.Fatal(err) @@ -373,14 +371,13 @@ func TestRouterFederation(t *testing.T) { r2KeyPath := filepath.Join(tempDir, "router2.key") r2, err := NewRouter(context.Background(), Options{ - ControlPlaneURL: cpURL, - ListenAddrs: []string{"/ip4/127.0.0.1/tcp/0"}, - KeysSyncInterval: 2 * time.Second, - LeaseRenewInterval: 2 * time.Second, - OIDCToken: router2JWT, - KeysDBPath: r2KeyPath, - AllowLoopback: true, - BiscuitTimeout: 1 * time.Second, + ControlPlaneURL: cpURL, + ListenAddrs: []string{"/ip4/127.0.0.1/tcp/0"}, + ControlPlaneSyncInterval: 2 * time.Second, + OIDCToken: router2JWT, + KeysDBPath: r2KeyPath, + AllowLoopback: true, + BiscuitTimeout: 1 * time.Second, }) if err != nil { t.Fatal(err) @@ -443,14 +440,13 @@ func TestRouterProactiveTokenRefresh(t *testing.T) { }) rOpts := Options{ - ControlPlaneURL: cpURL, - ListenAddrs: []string{"/ip4/127.0.0.1/tcp/0"}, - KeysSyncInterval: 20 * time.Second, - LeaseRenewInterval: 20 * time.Second, - OIDCToken: routerJWT, - KeysDBPath: routerKeyPath, - AllowLoopback: true, - BiscuitTimeout: 1 * time.Second, + ControlPlaneURL: cpURL, + ListenAddrs: []string{"/ip4/127.0.0.1/tcp/0"}, + ControlPlaneSyncInterval: 20 * time.Second, + OIDCToken: routerJWT, + KeysDBPath: routerKeyPath, + AllowLoopback: true, + BiscuitTimeout: 1 * time.Second, } r, err := NewRouter(context.Background(), rOpts) @@ -504,14 +500,13 @@ func TestRouterLeaseRenewalReEnrollOn401(t *testing.T) { }) rOpts := Options{ - ControlPlaneURL: cpURL, - ListenAddrs: []string{"/ip4/127.0.0.1/tcp/0"}, - KeysSyncInterval: 20 * time.Second, - LeaseRenewInterval: 20 * time.Second, - OIDCToken: routerJWT, - KeysDBPath: routerKeyPath, - AllowLoopback: true, - BiscuitTimeout: 1 * time.Second, + ControlPlaneURL: cpURL, + ListenAddrs: []string{"/ip4/127.0.0.1/tcp/0"}, + ControlPlaneSyncInterval: 20 * time.Second, + OIDCToken: routerJWT, + KeysDBPath: routerKeyPath, + AllowLoopback: true, + BiscuitTimeout: 1 * time.Second, } r, err := NewRouter(context.Background(), rOpts) @@ -705,14 +700,13 @@ func TestRouterProactiveRefreshReEnrollOn401(t *testing.T) { }) rOpts := Options{ - ControlPlaneURL: cpURL, - ListenAddrs: []string{"/ip4/127.0.0.1/tcp/0"}, - KeysSyncInterval: 20 * time.Second, - LeaseRenewInterval: 20 * time.Second, - OIDCToken: routerJWT, - KeysDBPath: routerKeyPath, - AllowLoopback: true, - BiscuitTimeout: 1 * time.Second, + ControlPlaneURL: cpURL, + ListenAddrs: []string{"/ip4/127.0.0.1/tcp/0"}, + ControlPlaneSyncInterval: 20 * time.Second, + OIDCToken: routerJWT, + KeysDBPath: routerKeyPath, + AllowLoopback: true, + BiscuitTimeout: 1 * time.Second, } r, err := NewRouter(context.Background(), rOpts) @@ -764,14 +758,13 @@ func TestRouterGossipSubBannedEvent(t *testing.T) { }) rOpts := Options{ - ControlPlaneURL: cpURL, - ListenAddrs: []string{"/ip4/127.0.0.1/tcp/0"}, - KeysSyncInterval: 20 * time.Second, - LeaseRenewInterval: 20 * time.Second, - OIDCToken: routerJWT, - KeysDBPath: routerKeyPath, - AllowLoopback: true, - BiscuitTimeout: 1 * time.Second, + ControlPlaneURL: cpURL, + ListenAddrs: []string{"/ip4/127.0.0.1/tcp/0"}, + ControlPlaneSyncInterval: 20 * time.Second, + OIDCToken: routerJWT, + KeysDBPath: routerKeyPath, + AllowLoopback: true, + BiscuitTimeout: 1 * time.Second, } r, err := NewRouter(context.Background(), rOpts) @@ -783,6 +776,17 @@ func TestRouterGossipSubBannedEvent(t *testing.T) { } defer func() { _ = r.Close() }() + // The control plane never learns of this ban, so the event must land + // after the first sync pass read /info: a pass that fetched after the + // event would read the ban's absence as an unban (reconcileBannedPeers), + // which is right for a real ban the control plane recorded first. + for deadline := time.Now().Add(5 * time.Second); r.lastFederation.Load() == 0; { + if time.Now().After(deadline) { + t.Fatal("the router's first sync pass did not read /info") + } + time.Sleep(20 * time.Millisecond) + } + privNode, _, err := crypto.GenerateKeyPair(crypto.Ed25519, -1) if err != nil { t.Fatalf("failed to generate node keypair: %v", err) @@ -1326,14 +1330,13 @@ func TestRouterRefreshEnrollmentReattestsJWT(t *testing.T) { } r, err := NewRouter(context.Background(), Options{ - ControlPlaneURL: cpURL, - ListenAddrs: []string{"/ip4/127.0.0.1/tcp/0"}, - KeysSyncInterval: time.Hour, - LeaseRenewInterval: time.Hour, - JWTPath: jwtPath, - KeysDBPath: filepath.Join(tempDir, "router.key"), - AllowLoopback: true, - BiscuitTimeout: time.Second, + ControlPlaneURL: cpURL, + ListenAddrs: []string{"/ip4/127.0.0.1/tcp/0"}, + ControlPlaneSyncInterval: time.Hour, + JWTPath: jwtPath, + KeysDBPath: filepath.Join(tempDir, "router.key"), + AllowLoopback: true, + BiscuitTimeout: time.Second, }) if err != nil { t.Fatalf("NewRouter: %v", err) diff --git a/internal/router/trust_test.go b/internal/router/trust_test.go index 63a47915..dbb51eba 100644 --- a/internal/router/trust_test.go +++ b/internal/router/trust_test.go @@ -163,7 +163,13 @@ func TestSyncKeysRequiresTrustedSignature(t *testing.T) { }) } -func TestKeysSyncLoopCoalescesTriggers(t *testing.T) { +// TestControlPlaneSyncLoopCoalescesTriggers pins two properties of the one +// control plane loop: a burst of mesh events while a pass is in flight costs +// one follow-up pass, and readers of the trust set never wait on the control +// plane. The fake control plane holds /keys until released and answers the +// rest of the pass with empty documents; the router is not enrolled, so the +// lease and policy steps log and move on. +func TestControlPlaneSyncLoopCoalescesTriggers(t *testing.T) { publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) @@ -179,6 +185,10 @@ func TestKeysSyncLoopCoalescesTriggers(t *testing.T) { requests := make(chan struct{}, 4) release := make(chan struct{}) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) { + if request.URL.Path != "/keys" { + w.Header().Set("Content-Type", "application/x-protobuf") + return + } requests <- struct{}{} select { case <-release: @@ -187,13 +197,13 @@ func TestKeysSyncLoopCoalescesTriggers(t *testing.T) { } })) t.Cleanup(server.Close) - router, err := NewRouter(context.Background(), Options{ControlPlaneURL: server.URL, KeysSyncInterval: time.Hour}) + router, err := NewRouter(context.Background(), Options{ControlPlaneURL: server.URL, ControlPlaneSyncInterval: time.Hour}) if err != nil { t.Fatal(err) } router.trustedPublicKeys = []ed25519.PublicKey{publicKey} router.wg.Add(1) - go router.runKeysSyncLoop() + go router.runControlPlaneSyncLoop() t.Cleanup(func() { router.cancel() router.wg.Wait() @@ -206,25 +216,26 @@ func TestKeysSyncLoopCoalescesTriggers(t *testing.T) { t.Fatal(message) } } - router.triggerKeysSync() - await(requests, "key sync did not start") + // The loop's first pass runs at start. + await(requests, "the first pass did not start") notified := make(chan struct{}) go func() { for range 10 { - router.triggerKeysSync() + router.triggerSync() } router.getTrustedPublicKeys() close(notified) }() await(notified, "notifications or key readers blocked on the control-plane request") - if pending := len(router.keysSyncTrigger); pending != 1 { + if pending := len(router.syncTrigger); pending != 1 { t.Fatalf("pending triggers = %d, want one coalesced follow-up", pending) } release <- struct{}{} - await(requests, "pending key sync was lost") - if pending := len(router.keysSyncTrigger); pending != 0 { - t.Fatalf("pending triggers = %d after follow-up", pending) + await(requests, "the follow-up pass was lost") + if pending := len(router.syncTrigger); pending != 0 { + t.Fatalf("pending triggers = %d after the follow-up", pending) } + release <- struct{}{} } func TestSyncKeysRefreshesOnRotation(t *testing.T) { diff --git a/internal/standalone/standalone.go b/internal/standalone/standalone.go index 82277fca..e5e5d105 100644 --- a/internal/standalone/standalone.go +++ b/internal/standalone/standalone.go @@ -144,10 +144,9 @@ type ControlPlaneTunables struct { // RouterTunables are the embedded router's operator knobs. type RouterTunables struct { - // KeysSyncInterval is how often biscuit public keys are refreshed. - KeysSyncInterval time.Duration - // LeaseRenewInterval is how often the router renews its lease. - LeaseRenewInterval time.Duration + // ControlPlaneSyncInterval is how often the router renews its lease and + // pulls keys, routers, bans and policy from the control plane. + ControlPlaneSyncInterval time.Duration // LowWaterMark / HighWaterMark bound the connection manager. LowWaterMark int HighWaterMark int @@ -367,20 +366,19 @@ func (s *Server) Start(ctx context.Context) (retErr error) { } rtr, err := router.NewRouter(ctx, router.Options{ - ControlPlaneURL: "http://" + cp.Addr(), - ListenAddrs: append([]string{wsAddr}, s.opts.P2PListen...), - ExternalAddrs: externalAddrs, - AllowLoopback: !s.opts.Router.DisallowLoopback, - KeysDBPath: routerKeyPath, - BootstrapToken: routerToken, - KeysSyncInterval: s.opts.Router.KeysSyncInterval, - LeaseRenewInterval: s.opts.Router.LeaseRenewInterval, - LowWaterMark: s.opts.Router.LowWaterMark, - HighWaterMark: s.opts.Router.HighWaterMark, - RelayLimitDuration: s.opts.Router.RelayLimitDuration, - RelayLimitData: int64(s.opts.Router.RelayLimitData), - ShutdownLeaseTTL: s.opts.Router.ShutdownLeaseTTL, - AuthDeadline: s.opts.Router.AuthDeadline, + ControlPlaneURL: "http://" + cp.Addr(), + ListenAddrs: append([]string{wsAddr}, s.opts.P2PListen...), + ExternalAddrs: externalAddrs, + AllowLoopback: !s.opts.Router.DisallowLoopback, + KeysDBPath: routerKeyPath, + BootstrapToken: routerToken, + ControlPlaneSyncInterval: s.opts.Router.ControlPlaneSyncInterval, + LowWaterMark: s.opts.Router.LowWaterMark, + HighWaterMark: s.opts.Router.HighWaterMark, + RelayLimitDuration: s.opts.Router.RelayLimitDuration, + RelayLimitData: int64(s.opts.Router.RelayLimitData), + ShutdownLeaseTTL: s.opts.Router.ShutdownLeaseTTL, + AuthDeadline: s.opts.Router.AuthDeadline, // Single-port deployments typically sit behind a TLS-terminating // proxy (Cloud Run, L7 LBs) or NAT where every peer shares a few // source IPs; libp2p's default 8-conns-per-IP cap would throttle diff --git a/site/content/docs/concepts/identity.md b/site/content/docs/concepts/identity.md index 328f31ab..4fb15ace 100644 --- a/site/content/docs/concepts/identity.md +++ b/site/content/docs/concepts/identity.md @@ -201,10 +201,10 @@ any user action. The control plane rotates its Biscuit Ed25519 signing key every `--key-rotation-interval` (24 hours by default). The previous key stays valid for `--key-grace-period` (1 hour by default) and is then retired. `GET /keys` -returns the current set of keys, signed by each key in the set. Routers poll -it every `--keys-sync-interval`; nodes fetch it at enrollment and then every -`--control-plane-sync-interval`, together with the ban set, revocation list, -and the mesh policy. A rotation event only brings the next pull forward. Both +returns the current set of keys, signed by each key in the set. Routers and +nodes fetch it at enrollment and then every `--control-plane-sync-interval`, +together with the ban set, revocation list, and the mesh policy. A rotation +event only brings the next pull forward. Both accept a new set only if one of its signatures verifies under a key they already trust. The first key comes from enrollment, and each later key is vouched for by the key it replaces. diff --git a/site/content/docs/reference/agentmesh-one.md b/site/content/docs/reference/agentmesh-one.md index c47cc539..0e3f572f 100644 --- a/site/content/docs/reference/agentmesh-one.md +++ b/site/content/docs/reference/agentmesh-one.md @@ -65,8 +65,7 @@ of zero leaves the default of that component unchanged. | `--control-plane-biscuit-ttl` | `--biscuit-ttl` | | `--control-plane-workload-session-ttl` | `--workload-session-ttl` | | `--control-plane-manual-enrollment` | The opposite of `--auto-approve-enrollment`: queue bootstrap enrollments for approval. | -| `--router-keys-sync-interval` | `agentmesh-router --keys-sync-interval` | -| `--router-lease-renew-interval` | `--lease-renew-interval` | +| `--router-control-plane-sync-interval` | `agentmesh-router --control-plane-sync-interval` | | `--router-low-watermark`, `--router-high-watermark` | `--low-watermark`, `--high-watermark` | | `--router-conns-per-source-ip` | `--conns-per-source-ip`. `0` follows the high watermark, because peers behind a proxy share source IPs. | | `--router-relay-limit-duration`, `--router-relay-limit-data` | `--relay-limit-duration`, `--relay-limit-data`. `0` keeps `1h` and no data limit, so set a long duration such as `24h` instead of an unlimited one. | diff --git a/site/content/docs/reference/policy.md b/site/content/docs/reference/policy.md index ea14a783..fd3ae7ba 100644 --- a/site/content/docs/reference/policy.md +++ b/site/content/docs/reference/policy.md @@ -125,8 +125,8 @@ A change to `allowed_publish` takes effect without a new credential. The grant is also rendered into the policy text as `granted_publish_*(...) <- role("")` (and `<- label(...)` for `served_by`), and every verifier adds that text to its authorizer: the -router, a node and an SDK member at their next sync, all of them at once on -the `POLICY_UPDATE` event +router at its next `--control-plane-sync-interval` pass, a node and an SDK +member at their next sync, all of them at once on the `POLICY_UPDATE` event the control plane publishes. A node whose policy text changed announces its services again, so a record the router refused under the previous policy is in the directory within the sync interval; until then the router answers @@ -355,8 +355,8 @@ identity's roles from the bindings and mints into the credential one `role()` fact per role and the compiled `granted_*` facts of every role. The control plane also renders the policy as Datalog rules served as `datalog_rules` at `GET /policies`. Nodes, routers and SDK members fetch this text -(`--control-plane-sync-interval`, 15 minutes) and add it to their authorizer -as it arrives. +(`--control-plane-sync-interval`, 15 minutes for a node, 5 for a router) and +add it to their authorizer as it arrives. At the destination node, in this order: diff --git a/site/content/docs/reference/router.md b/site/content/docs/reference/router.md index 390bb4f1..479850df 100644 --- a/site/content/docs/reference/router.md +++ b/site/content/docs/reference/router.md @@ -38,8 +38,7 @@ replica count. | `--listen` | `/ip4/0.0.0.0/tcp/5001`, `/ip6/::/tcp/5001` | libp2p listen addresses. Repeatable. Add `/ip4/0.0.0.0/udp/5001/quic-v1` for QUIC. | | `--external-addr` | none | Addresses to announce instead of the detected ones, such as `/dnsaddr/bootstrap.example.com` or `/ip4//tcp/5001`. Repeatable. | | `--keys-path` | `router.key` | The router's private key. It fixes the peer ID across restarts and belongs on persistent storage. | -| `--keys-sync-interval` | `5m` | How often `/keys` is polled for signing-key rotations. | -| `--lease-renew-interval` | `300s` | How often the lease is renewed. Must be well below the control plane's `--lease-duration`. | +| `--control-plane-sync-interval` | `5m` | How often the router's one pass over the control plane runs: it renews the lease, re-reads `/info`, `/keys` and `/policies`, and refreshes its own credential when it nears expiry. A mesh event runs a pass at once. Must be well below the control plane's `--lease-duration` and `--key-grace-period`. | | `--allow-loopback` | `false` | Announce and accept loopback and link-local addresses. For a router and nodes on one host. | | `--conns-per-source-ip` | a quarter of `--high-watermark` (`1000`) | Inbound connections accepted per source address. One address can hold at most this share of the router's connection budget, so filling a router takes at least four addresses. Members behind a NAT or a cluster's SNAT share one address and each holds one connection per router, two while enrolling. | | `--low-watermark`, `--high-watermark` | 15% below the high mark (`3400`), `4000` | How many members the router holds: peers that passed the handshake, other routers aside. At the high mark the router sends members away (see below) until it is back at the low mark, so the gap between the two is what one shedding moves; the default moves 15%. The same numbers bound connections: above the high mark the connection manager closes connections, oldest first among those that carry no session, then members; connections to the other routers are never closed. A connection without a session lasts at most `--auth-deadline`; the connection manager leaves a connection alone while it is younger than its grace period, and libp2p's connection cap, twice the high mark, is what bounds the total (see the [scalability guide](../../guides/scalability/#what-bounds-the-mesh)). | @@ -60,8 +59,13 @@ replica count. 2. Starts the libp2p host, the DHT it shares with the other routers, the directory it serves to its members, the relay service and GossipSub. 3. Registers a lease at `POST /routers/lease` with its announced addresses - and renews it every `--lease-renew-interval`. The control plane lists the - router on `/info` while the lease is live. + and renews it every `--control-plane-sync-interval`, in the same pass + that re-reads `/keys`, `/info` (the routers to federate with and the ban + set) and `/policies` (the mesh policy, which decides what a member may + publish). A `KEY_ROTATION` or `POLICY_UPDATE` event runs the pass at + once; while the list names a router this one has not authenticated with, + the federation step alone is retried every 2 seconds. The control plane + lists the router on `/info` while the lease is live. 4. Runs the mutual credential handshake on every inbound connection and refuses peers whose credential does not verify and peers on the ban list. Until a peer passes it, the router serves it nothing but the handshake: @@ -87,7 +91,8 @@ replica count. a key or for a peer; the router answers from its own records and from the routers closest to the key, asked at once. A member keeps no DHT and connects to no router it does not hold. -6. Refreshes its own credential before it expires, like a node. +6. Refreshes its own credential before it expires, like a node, at the + pass that finds less than a fifth of its lifetime left. 7. Every 5 seconds, compares the members it holds, the peers that passed the handshake, with `--high-watermark`. At the mark it picks members at random, as many as take it back to `--low-watermark`, and sends each a diff --git a/tests/integration/failover_test.go b/tests/integration/failover_test.go index 83fe69ff..10abe592 100644 --- a/tests/integration/failover_test.go +++ b/tests/integration/failover_test.go @@ -149,7 +149,7 @@ roles: [] "--keys-path", filepath.Join(tmpDir, "router_keysA.db"), "--allow-loopback", "--oidc-token", routerJWT, - "--lease-renew-interval", "250ms", + "--control-plane-sync-interval", "250ms", ) if err := cmdRouterA.Start(); err != nil { t.Fatalf("failed to start Router A: %v", err) @@ -182,7 +182,7 @@ roles: [] "--keys-path", filepath.Join(tmpDir, "router_keysB.db"), "--allow-loopback", "--oidc-token", routerJWT, - "--lease-renew-interval", "250ms", + "--control-plane-sync-interval", "250ms", ) if err := cmdRouterB.Start(); err != nil { t.Fatalf("failed to start Router B: %v", err) diff --git a/tests/integration/minimal_helpers_test.go b/tests/integration/minimal_helpers_test.go index 3f54ddeb..cbdfa765 100644 --- a/tests/integration/minimal_helpers_test.go +++ b/tests/integration/minimal_helpers_test.go @@ -471,7 +471,7 @@ func startRouter(t *testing.T, tmpDir string, cpPort int, mintToken func(map[str "--oidc-token", routerJWT, // Each renewal carries the router's connected peers, which is how // tests see a node reach the mesh through the control plane. - "--lease-renew-interval", "250ms", + "--control-plane-sync-interval", "250ms", ) routerCmd.Stdout = os.Stdout routerCmd.Stderr = os.Stderr diff --git a/tests/integration/readmission_test.go b/tests/integration/readmission_test.go index 53181521..ed84d62a 100644 --- a/tests/integration/readmission_test.go +++ b/tests/integration/readmission_test.go @@ -159,14 +159,13 @@ func startReadmissionMesh(t *testing.T, ctx context.Context) *readmissionMesh { cpPort, _ := strconv.Atoi(portStr) rtr, err := router.NewRouter(ctx, router.Options{ - ControlPlaneURL: baseURL, - ListenAddrs: []string{"/ip4/127.0.0.1/tcp/0"}, - KeysDBPath: filepath.Join(tmp, "router.key"), - OIDCToken: mintToken(map[string]interface{}{"sub": "router", "groups": []string{"routers"}, "roles": []string{api.RoleRouter}}), - AllowLoopback: true, - KeysSyncInterval: 20 * time.Second, - LeaseRenewInterval: 500 * time.Millisecond, - BiscuitTimeout: time.Second, + ControlPlaneURL: baseURL, + ListenAddrs: []string{"/ip4/127.0.0.1/tcp/0"}, + KeysDBPath: filepath.Join(tmp, "router.key"), + OIDCToken: mintToken(map[string]interface{}{"sub": "router", "groups": []string{"routers"}, "roles": []string{api.RoleRouter}}), + AllowLoopback: true, + ControlPlaneSyncInterval: 500 * time.Millisecond, + BiscuitTimeout: time.Second, }) if err != nil { t.Fatalf("failed to create router: %v", err) diff --git a/tests/integration/revocation_test.go b/tests/integration/revocation_test.go index b667cea5..0f3e7932 100644 --- a/tests/integration/revocation_test.go +++ b/tests/integration/revocation_test.go @@ -97,8 +97,7 @@ roles: "--keys-path", filepath.Join(tmpDir, "router-keys.db"), "--allow-loopback", "--oidc-token", routerJWT, - "--lease-renew-interval", "2s", - "--keys-sync-interval", "1s", + "--control-plane-sync-interval", "1s", ) routerCmd.Stdout = os.Stdout routerCmd.Stderr = os.Stderr diff --git a/tests/integration/rotation_test.go b/tests/integration/rotation_test.go index 9fd42c28..59c4344b 100644 --- a/tests/integration/rotation_test.go +++ b/tests/integration/rotation_test.go @@ -100,8 +100,7 @@ roles: "--keys-path", filepath.Join(tmpDir, "router-keys.db"), "--allow-loopback", "--oidc-token", routerJWT, - "--lease-renew-interval", "1s", - "--keys-sync-interval", "500ms", + "--control-plane-sync-interval", "500ms", ) routerCmd.Stdout = os.Stdout routerCmd.Stderr = os.Stderr diff --git a/tests/integration/router_attachment_test.go b/tests/integration/router_attachment_test.go index 6440e066..1eed597b 100644 --- a/tests/integration/router_attachment_test.go +++ b/tests/integration/router_attachment_test.go @@ -108,7 +108,7 @@ func startLabelledRouter(t *testing.T, routerBin, tmpDir, name string, cpPort in "--keys-path", keysPath, "--allow-loopback", "--oidc-token", routerJWT, - "--lease-renew-interval", "250ms", + "--control-plane-sync-interval", "250ms", "--metrics-addr", metricsAddr, } for _, l := range labels { diff --git a/tests/integration/router_shutdown_test.go b/tests/integration/router_shutdown_test.go index 006a5812..9d39a7bc 100644 --- a/tests/integration/router_shutdown_test.go +++ b/tests/integration/router_shutdown_test.go @@ -73,7 +73,7 @@ func TestRouterGracefulShutdownWithdrawsLease(t *testing.T) { "--oidc-token", routerJWT, // Long enough that only a withdrawal, never a renewal, changes // the active set during the test. - "--lease-renew-interval", "1m", + "--control-plane-sync-interval", "1m", "--shutdown-lease-ttl", shutdownTTL, ) if err := cmd.Start(); err != nil {