diff --git a/docs/specs/product/compass.md b/docs/specs/product/compass.md index 1659d8267..5bab04a35 100644 --- a/docs/specs/product/compass.md +++ b/docs/specs/product/compass.md @@ -974,6 +974,54 @@ on the answer's ask snapshot. The server SHALL NOT fill a default `ref`. - **Then** the server returns invalid-argument and the channel has no new message. +### Requirement: Users control ask mirroring to forge coordinates + +`ListForgeMirrorSettings` SHALL list every enabled forge repository and its +current mode. Any authenticated account may list settings. The response SHALL +set `can_edit` only for a tenant admin. `SetForgeMirrorSetting` SHALL require a +user with the tenant admin role; other callers receive permission-denied. +An UNSPECIFIED mode SHALL be rejected with invalid-argument. Setting OFF SHALL +remove the stored setting, and an absent setting SHALL behave as OFF. + +#### Scenario: An admin configures automatic ask mirroring + +- **Given** an enabled forge repository and a tenant admin +- **When** the admin sets its mode to AUTO +- **Then** the setting is returned by the list RPC with `can_edit` true + +#### Scenario: A queued ask is refused after the mode is turned off + +- **Given** an ask for an AUTO coordinate waiting for delivery +- **When** an admin turns the coordinate OFF before the post +- **Then** the mirror is marked failed and no forge comment is created + +#### Scenario: An answer edits the existing mirrored ask + +- **Given** an ask with a delivered forge mirror +- **When** a user answers the ask +- **Then** the same forge comment is edited to include the answer and answerer + +`MirrorToForge` SHALL accept exactly one ask, topic, or one-to-fifty-message +source. The caller SHALL be a user who can see the source channel. The destination +mode SHALL be MANUAL or AUTO. The agent scope check applies only to agent posts. +An invisible channel SHALL return not-found. An agent caller SHALL receive +permission-denied. Invalid source combinations SHALL receive invalid-argument. +The user header SHALL name the clicking user and source text SHALL not be +allowed to forge an owner header. A second user mirroring the same source to the +same ref SHALL be refused while the first user's mirror exists. + +Topic mirrors SHALL carry at most the newest 50 messages. If a topic has more +than 50, the mirror SHALL state, "Showing the newest 50 messages." + +### Requirement: Automatic mirroring follows the ask's explicit ref + +An ask with an explicit ref to an enabled AUTO coordinate SHALL be mirrored +after its message commits. An ask with no ref, or whose coordinate is OFF or +MANUAL, SHALL NOT be automatically posted. The server SHALL check mode again +immediately before every post; a coordinate turned OFF while queued SHALL not +be posted. Answering an ask SHALL update the same forge comment with its answer +and answerer rather than create a second comment. + ### Requirement: Channel membership carries a separate subscribe opt-in `UpdateChannelMembers` SHALL manage both the channel's member set and each diff --git a/go/gen/compass/v1/comms.pb.go b/go/gen/compass/v1/comms.pb.go index 553c6ccc5..fd0a24841 100644 --- a/go/gen/compass/v1/comms.pb.go +++ b/go/gen/compass/v1/comms.pb.go @@ -678,6 +678,63 @@ func (BoardPrGate) EnumDescriptor() ([]byte, []int) { return file_compass_v1_comms_proto_rawDescGZIP(), []int{10} } +// Delivery state of an ask mirror outbox row. +type BoardMirrorState int32 + +const ( + // No known mirror state was supplied. + BoardMirrorState_BOARD_MIRROR_STATE_UNSPECIFIED BoardMirrorState = 0 + // The comment is waiting for delivery or retry. + BoardMirrorState_BOARD_MIRROR_STATE_PENDING BoardMirrorState = 1 + // The forge comment was delivered. + BoardMirrorState_BOARD_MIRROR_STATE_DONE BoardMirrorState = 2 + // Delivery was refused or exhausted. + BoardMirrorState_BOARD_MIRROR_STATE_FAILED BoardMirrorState = 3 +) + +// Enum value maps for BoardMirrorState. +var ( + BoardMirrorState_name = map[int32]string{ + 0: "BOARD_MIRROR_STATE_UNSPECIFIED", + 1: "BOARD_MIRROR_STATE_PENDING", + 2: "BOARD_MIRROR_STATE_DONE", + 3: "BOARD_MIRROR_STATE_FAILED", + } + BoardMirrorState_value = map[string]int32{ + "BOARD_MIRROR_STATE_UNSPECIFIED": 0, + "BOARD_MIRROR_STATE_PENDING": 1, + "BOARD_MIRROR_STATE_DONE": 2, + "BOARD_MIRROR_STATE_FAILED": 3, + } +) + +func (x BoardMirrorState) Enum() *BoardMirrorState { + p := new(BoardMirrorState) + *p = x + return p +} + +func (x BoardMirrorState) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (BoardMirrorState) Descriptor() protoreflect.EnumDescriptor { + return file_compass_v1_comms_proto_enumTypes[11].Descriptor() +} + +func (BoardMirrorState) Type() protoreflect.EnumType { + return &file_compass_v1_comms_proto_enumTypes[11] +} + +func (x BoardMirrorState) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use BoardMirrorState.Descriptor instead. +func (BoardMirrorState) EnumDescriptor() ([]byte, []int) { + return file_compass_v1_comms_proto_rawDescGZIP(), []int{11} +} + type PeeringState int32 const ( @@ -714,11 +771,11 @@ func (x PeeringState) String() string { } func (PeeringState) Descriptor() protoreflect.EnumDescriptor { - return file_compass_v1_comms_proto_enumTypes[11].Descriptor() + return file_compass_v1_comms_proto_enumTypes[12].Descriptor() } func (PeeringState) Type() protoreflect.EnumType { - return &file_compass_v1_comms_proto_enumTypes[11] + return &file_compass_v1_comms_proto_enumTypes[12] } func (x PeeringState) Number() protoreflect.EnumNumber { @@ -727,7 +784,7 @@ func (x PeeringState) Number() protoreflect.EnumNumber { // Deprecated: Use PeeringState.Descriptor instead. func (PeeringState) EnumDescriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{11} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{12} } // The scope of a roster read, relative to a vantage agent. @@ -767,11 +824,11 @@ func (x RosterScope) String() string { } func (RosterScope) Descriptor() protoreflect.EnumDescriptor { - return file_compass_v1_comms_proto_enumTypes[12].Descriptor() + return file_compass_v1_comms_proto_enumTypes[13].Descriptor() } func (RosterScope) Type() protoreflect.EnumType { - return &file_compass_v1_comms_proto_enumTypes[12] + return &file_compass_v1_comms_proto_enumTypes[13] } func (x RosterScope) Number() protoreflect.EnumNumber { @@ -780,7 +837,7 @@ func (x RosterScope) Number() protoreflect.EnumNumber { // Deprecated: Use RosterScope.Descriptor instead. func (RosterScope) EnumDescriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{12} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{13} } // An account in the communication layer: a human user or an owned agent. Both @@ -3619,8 +3676,12 @@ type BoardAsk struct { Answered bool `protobuf:"varint,6,opt,name=answered,proto3" json:"answered,omitempty"` // When the ask was posted, in Unix milliseconds. PostedAtUnixMs int64 `protobuf:"varint,7,opt,name=posted_at_unix_ms,json=postedAtUnixMs,proto3" json:"posted_at_unix_ms,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Set by the board read to report this ask's current forge delivery state. + MirrorState BoardMirrorState `protobuf:"varint,8,opt,name=mirror_state,json=mirrorState,proto3,enum=compass.v1.BoardMirrorState" json:"mirror_state,omitempty"` + // Set by the board read when the forge comment was delivered. + MirrorUrl string `protobuf:"bytes,9,opt,name=mirror_url,json=mirrorUrl,proto3" json:"mirror_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *BoardAsk) Reset() { @@ -3702,6 +3763,148 @@ func (x *BoardAsk) GetPostedAtUnixMs() int64 { return 0 } +func (x *BoardAsk) GetMirrorState() BoardMirrorState { + if x != nil { + return x.MirrorState + } + return BoardMirrorState_BOARD_MIRROR_STATE_UNSPECIFIED +} + +func (x *BoardAsk) GetMirrorUrl() string { + if x != nil { + return x.MirrorUrl + } + return "" +} + +// Request to mirror a visible ask, topic, or message set to an allowed forge ref on the calling user's behalf. +type MirrorToForgeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The issue or pull request destination. + Ref string `protobuf:"bytes,1,opt,name=ref,proto3" json:"ref,omitempty"` + // The ask to mirror; mutually exclusive with the other source fields. + AskId string `protobuf:"bytes,2,opt,name=ask_id,json=askId,proto3" json:"ask_id,omitempty"` + // The topic to mirror; mutually exclusive with the other source fields. + TopicId string `protobuf:"bytes,3,opt,name=topic_id,json=topicId,proto3" json:"topic_id,omitempty"` + // One to fifty message IDs from channel_id, mutually exclusive with other sources. + MessageIds []string `protobuf:"bytes,4,rep,name=message_ids,json=messageIds,proto3" json:"message_ids,omitempty"` + // The visible channel containing the chosen source. + ChannelId string `protobuf:"bytes,5,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MirrorToForgeRequest) Reset() { + *x = MirrorToForgeRequest{} + mi := &file_compass_v1_comms_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MirrorToForgeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MirrorToForgeRequest) ProtoMessage() {} + +func (x *MirrorToForgeRequest) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_comms_proto_msgTypes[34] + 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 MirrorToForgeRequest.ProtoReflect.Descriptor instead. +func (*MirrorToForgeRequest) Descriptor() ([]byte, []int) { + return file_compass_v1_comms_proto_rawDescGZIP(), []int{34} +} + +func (x *MirrorToForgeRequest) GetRef() string { + if x != nil { + return x.Ref + } + return "" +} + +func (x *MirrorToForgeRequest) GetAskId() string { + if x != nil { + return x.AskId + } + return "" +} + +func (x *MirrorToForgeRequest) GetTopicId() string { + if x != nil { + return x.TopicId + } + return "" +} + +func (x *MirrorToForgeRequest) GetMessageIds() []string { + if x != nil { + return x.MessageIds + } + return nil +} + +func (x *MirrorToForgeRequest) GetChannelId() string { + if x != nil { + return x.ChannelId + } + return "" +} + +// The forge comment created or updated for the requested source. +type MirrorToForgeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The URL of the resulting forge comment. + CommentUrl string `protobuf:"bytes,1,opt,name=comment_url,json=commentUrl,proto3" json:"comment_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MirrorToForgeResponse) Reset() { + *x = MirrorToForgeResponse{} + mi := &file_compass_v1_comms_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MirrorToForgeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MirrorToForgeResponse) ProtoMessage() {} + +func (x *MirrorToForgeResponse) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_comms_proto_msgTypes[35] + 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 MirrorToForgeResponse.ProtoReflect.Descriptor instead. +func (*MirrorToForgeResponse) Descriptor() ([]byte, []int) { + return file_compass_v1_comms_proto_rawDescGZIP(), []int{35} +} + +func (x *MirrorToForgeResponse) GetCommentUrl() string { + if x != nil { + return x.CommentUrl + } + return "" +} + // The current board state for one agent. type AgentBoard struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -3735,7 +3938,7 @@ type AgentBoard struct { func (x *AgentBoard) Reset() { *x = AgentBoard{} - mi := &file_compass_v1_comms_proto_msgTypes[34] + mi := &file_compass_v1_comms_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3747,7 +3950,7 @@ func (x *AgentBoard) String() string { func (*AgentBoard) ProtoMessage() {} func (x *AgentBoard) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[34] + mi := &file_compass_v1_comms_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3760,7 +3963,7 @@ func (x *AgentBoard) ProtoReflect() protoreflect.Message { // Deprecated: Use AgentBoard.ProtoReflect.Descriptor instead. func (*AgentBoard) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{34} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{36} } func (x *AgentBoard) GetAgentAccountId() string { @@ -3868,7 +4071,7 @@ type AgentBoardSummary struct { func (x *AgentBoardSummary) Reset() { *x = AgentBoardSummary{} - mi := &file_compass_v1_comms_proto_msgTypes[35] + mi := &file_compass_v1_comms_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3880,7 +4083,7 @@ func (x *AgentBoardSummary) String() string { func (*AgentBoardSummary) ProtoMessage() {} func (x *AgentBoardSummary) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[35] + mi := &file_compass_v1_comms_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3893,7 +4096,7 @@ func (x *AgentBoardSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use AgentBoardSummary.ProtoReflect.Descriptor instead. func (*AgentBoardSummary) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{35} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{37} } func (x *AgentBoardSummary) GetFocus() string { @@ -3961,7 +4164,7 @@ type UpdateAgentBoardRequest struct { func (x *UpdateAgentBoardRequest) Reset() { *x = UpdateAgentBoardRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[36] + mi := &file_compass_v1_comms_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3973,7 +4176,7 @@ func (x *UpdateAgentBoardRequest) String() string { func (*UpdateAgentBoardRequest) ProtoMessage() {} func (x *UpdateAgentBoardRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[36] + mi := &file_compass_v1_comms_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3986,7 +4189,7 @@ func (x *UpdateAgentBoardRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateAgentBoardRequest.ProtoReflect.Descriptor instead. func (*UpdateAgentBoardRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{36} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{38} } func (x *UpdateAgentBoardRequest) GetUpsertTasks() []*BoardTaskPatch { @@ -4049,7 +4252,7 @@ type UpdateAgentBoardResponse struct { func (x *UpdateAgentBoardResponse) Reset() { *x = UpdateAgentBoardResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[37] + mi := &file_compass_v1_comms_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4061,7 +4264,7 @@ func (x *UpdateAgentBoardResponse) String() string { func (*UpdateAgentBoardResponse) ProtoMessage() {} func (x *UpdateAgentBoardResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[37] + mi := &file_compass_v1_comms_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4074,7 +4277,7 @@ func (x *UpdateAgentBoardResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateAgentBoardResponse.ProtoReflect.Descriptor instead. func (*UpdateAgentBoardResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{37} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{39} } func (x *UpdateAgentBoardResponse) GetBoard() *AgentBoard { @@ -4107,7 +4310,7 @@ type ListAgentBoardsRequest struct { func (x *ListAgentBoardsRequest) Reset() { *x = ListAgentBoardsRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[38] + mi := &file_compass_v1_comms_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4119,7 +4322,7 @@ func (x *ListAgentBoardsRequest) String() string { func (*ListAgentBoardsRequest) ProtoMessage() {} func (x *ListAgentBoardsRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[38] + mi := &file_compass_v1_comms_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4132,7 +4335,7 @@ func (x *ListAgentBoardsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAgentBoardsRequest.ProtoReflect.Descriptor instead. func (*ListAgentBoardsRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{38} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{40} } func (x *ListAgentBoardsRequest) GetAgentHandles() []string { @@ -4195,7 +4398,7 @@ type ListAgentBoardsResponse struct { func (x *ListAgentBoardsResponse) Reset() { *x = ListAgentBoardsResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[39] + mi := &file_compass_v1_comms_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4207,7 +4410,7 @@ func (x *ListAgentBoardsResponse) String() string { func (*ListAgentBoardsResponse) ProtoMessage() {} func (x *ListAgentBoardsResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[39] + mi := &file_compass_v1_comms_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4220,7 +4423,7 @@ func (x *ListAgentBoardsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAgentBoardsResponse.ProtoReflect.Descriptor instead. func (*ListAgentBoardsResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{39} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{41} } func (x *ListAgentBoardsResponse) GetBoards() []*AgentBoard { @@ -4243,7 +4446,7 @@ type AgentBoardChanged struct { func (x *AgentBoardChanged) Reset() { *x = AgentBoardChanged{} - mi := &file_compass_v1_comms_proto_msgTypes[40] + mi := &file_compass_v1_comms_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4255,7 +4458,7 @@ func (x *AgentBoardChanged) String() string { func (*AgentBoardChanged) ProtoMessage() {} func (x *AgentBoardChanged) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[40] + mi := &file_compass_v1_comms_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4268,7 +4471,7 @@ func (x *AgentBoardChanged) ProtoReflect() protoreflect.Message { // Deprecated: Use AgentBoardChanged.ProtoReflect.Descriptor instead. func (*AgentBoardChanged) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{40} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{42} } func (x *AgentBoardChanged) GetAgentAccountId() string { @@ -4296,7 +4499,7 @@ type Peering struct { func (x *Peering) Reset() { *x = Peering{} - mi := &file_compass_v1_comms_proto_msgTypes[41] + mi := &file_compass_v1_comms_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4308,7 +4511,7 @@ func (x *Peering) String() string { func (*Peering) ProtoMessage() {} func (x *Peering) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[41] + mi := &file_compass_v1_comms_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4321,7 +4524,7 @@ func (x *Peering) ProtoReflect() protoreflect.Message { // Deprecated: Use Peering.ProtoReflect.Descriptor instead. func (*Peering) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{41} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{43} } func (x *Peering) GetUserAccountId() string { @@ -4354,7 +4557,7 @@ type ApprovePeerRequest struct { func (x *ApprovePeerRequest) Reset() { *x = ApprovePeerRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[42] + mi := &file_compass_v1_comms_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4366,7 +4569,7 @@ func (x *ApprovePeerRequest) String() string { func (*ApprovePeerRequest) ProtoMessage() {} func (x *ApprovePeerRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[42] + mi := &file_compass_v1_comms_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4379,7 +4582,7 @@ func (x *ApprovePeerRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApprovePeerRequest.ProtoReflect.Descriptor instead. func (*ApprovePeerRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{42} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{44} } func (x *ApprovePeerRequest) GetPeerHandle() string { @@ -4398,7 +4601,7 @@ type ApprovePeerResponse struct { func (x *ApprovePeerResponse) Reset() { *x = ApprovePeerResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[43] + mi := &file_compass_v1_comms_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4410,7 +4613,7 @@ func (x *ApprovePeerResponse) String() string { func (*ApprovePeerResponse) ProtoMessage() {} func (x *ApprovePeerResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[43] + mi := &file_compass_v1_comms_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4423,7 +4626,7 @@ func (x *ApprovePeerResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApprovePeerResponse.ProtoReflect.Descriptor instead. func (*ApprovePeerResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{43} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{45} } func (x *ApprovePeerResponse) GetPeering() *Peering { @@ -4442,7 +4645,7 @@ type RevokePeerRequest struct { func (x *RevokePeerRequest) Reset() { *x = RevokePeerRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[44] + mi := &file_compass_v1_comms_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4454,7 +4657,7 @@ func (x *RevokePeerRequest) String() string { func (*RevokePeerRequest) ProtoMessage() {} func (x *RevokePeerRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[44] + mi := &file_compass_v1_comms_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4467,7 +4670,7 @@ func (x *RevokePeerRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokePeerRequest.ProtoReflect.Descriptor instead. func (*RevokePeerRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{44} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{46} } func (x *RevokePeerRequest) GetPeerHandle() string { @@ -4486,7 +4689,7 @@ type RevokePeerResponse struct { func (x *RevokePeerResponse) Reset() { *x = RevokePeerResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[45] + mi := &file_compass_v1_comms_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4498,7 +4701,7 @@ func (x *RevokePeerResponse) String() string { func (*RevokePeerResponse) ProtoMessage() {} func (x *RevokePeerResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[45] + mi := &file_compass_v1_comms_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4511,7 +4714,7 @@ func (x *RevokePeerResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokePeerResponse.ProtoReflect.Descriptor instead. func (*RevokePeerResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{45} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{47} } func (x *RevokePeerResponse) GetDeleted() bool { @@ -4529,7 +4732,7 @@ type ListPeersRequest struct { func (x *ListPeersRequest) Reset() { *x = ListPeersRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[46] + mi := &file_compass_v1_comms_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4541,7 +4744,7 @@ func (x *ListPeersRequest) String() string { func (*ListPeersRequest) ProtoMessage() {} func (x *ListPeersRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[46] + mi := &file_compass_v1_comms_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4554,7 +4757,7 @@ func (x *ListPeersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListPeersRequest.ProtoReflect.Descriptor instead. func (*ListPeersRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{46} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{48} } type ListPeersResponse struct { @@ -4566,7 +4769,7 @@ type ListPeersResponse struct { func (x *ListPeersResponse) Reset() { *x = ListPeersResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[47] + mi := &file_compass_v1_comms_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4578,7 +4781,7 @@ func (x *ListPeersResponse) String() string { func (*ListPeersResponse) ProtoMessage() {} func (x *ListPeersResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[47] + mi := &file_compass_v1_comms_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4591,7 +4794,7 @@ func (x *ListPeersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListPeersResponse.ProtoReflect.Descriptor instead. func (*ListPeersResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{47} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{49} } func (x *ListPeersResponse) GetPeerings() []*Peering { @@ -4611,7 +4814,7 @@ type CreateUserRequest struct { func (x *CreateUserRequest) Reset() { *x = CreateUserRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[48] + mi := &file_compass_v1_comms_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4623,7 +4826,7 @@ func (x *CreateUserRequest) String() string { func (*CreateUserRequest) ProtoMessage() {} func (x *CreateUserRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[48] + mi := &file_compass_v1_comms_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4636,7 +4839,7 @@ func (x *CreateUserRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateUserRequest.ProtoReflect.Descriptor instead. func (*CreateUserRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{48} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{50} } func (x *CreateUserRequest) GetHandle() string { @@ -4662,7 +4865,7 @@ type CreateUserResponse struct { func (x *CreateUserResponse) Reset() { *x = CreateUserResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[49] + mi := &file_compass_v1_comms_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4674,7 +4877,7 @@ func (x *CreateUserResponse) String() string { func (*CreateUserResponse) ProtoMessage() {} func (x *CreateUserResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[49] + mi := &file_compass_v1_comms_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4687,7 +4890,7 @@ func (x *CreateUserResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateUserResponse.ProtoReflect.Descriptor instead. func (*CreateUserResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{49} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{51} } func (x *CreateUserResponse) GetAccount() *Account { @@ -4716,7 +4919,7 @@ type CreateAgentRequest struct { func (x *CreateAgentRequest) Reset() { *x = CreateAgentRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[50] + mi := &file_compass_v1_comms_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4728,7 +4931,7 @@ func (x *CreateAgentRequest) String() string { func (*CreateAgentRequest) ProtoMessage() {} func (x *CreateAgentRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[50] + mi := &file_compass_v1_comms_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4741,7 +4944,7 @@ func (x *CreateAgentRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateAgentRequest.ProtoReflect.Descriptor instead. func (*CreateAgentRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{50} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{52} } func (x *CreateAgentRequest) GetHandle() string { @@ -4788,7 +4991,7 @@ type CreateAgentResponse struct { func (x *CreateAgentResponse) Reset() { *x = CreateAgentResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[51] + mi := &file_compass_v1_comms_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4800,7 +5003,7 @@ func (x *CreateAgentResponse) String() string { func (*CreateAgentResponse) ProtoMessage() {} func (x *CreateAgentResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[51] + mi := &file_compass_v1_comms_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4813,7 +5016,7 @@ func (x *CreateAgentResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateAgentResponse.ProtoReflect.Descriptor instead. func (*CreateAgentResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{51} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{53} } func (x *CreateAgentResponse) GetAccount() *Account { @@ -4834,7 +5037,7 @@ type ListAccountsRequest struct { func (x *ListAccountsRequest) Reset() { *x = ListAccountsRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[52] + mi := &file_compass_v1_comms_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4846,7 +5049,7 @@ func (x *ListAccountsRequest) String() string { func (*ListAccountsRequest) ProtoMessage() {} func (x *ListAccountsRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[52] + mi := &file_compass_v1_comms_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4859,7 +5062,7 @@ func (x *ListAccountsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAccountsRequest.ProtoReflect.Descriptor instead. func (*ListAccountsRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{52} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{54} } func (x *ListAccountsRequest) GetSnapshotSeq() uint64 { @@ -4878,7 +5081,7 @@ type ListAccountsResponse struct { func (x *ListAccountsResponse) Reset() { *x = ListAccountsResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[53] + mi := &file_compass_v1_comms_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4890,7 +5093,7 @@ func (x *ListAccountsResponse) String() string { func (*ListAccountsResponse) ProtoMessage() {} func (x *ListAccountsResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[53] + mi := &file_compass_v1_comms_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4903,7 +5106,7 @@ func (x *ListAccountsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAccountsResponse.ProtoReflect.Descriptor instead. func (*ListAccountsResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{53} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{55} } func (x *ListAccountsResponse) GetAccounts() []*Account { @@ -4935,7 +5138,7 @@ type CreateChannelGroupRequest struct { func (x *CreateChannelGroupRequest) Reset() { *x = CreateChannelGroupRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[54] + mi := &file_compass_v1_comms_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4947,7 +5150,7 @@ func (x *CreateChannelGroupRequest) String() string { func (*CreateChannelGroupRequest) ProtoMessage() {} func (x *CreateChannelGroupRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[54] + mi := &file_compass_v1_comms_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4960,7 +5163,7 @@ func (x *CreateChannelGroupRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateChannelGroupRequest.ProtoReflect.Descriptor instead. func (*CreateChannelGroupRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{54} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{56} } func (x *CreateChannelGroupRequest) GetName() string { @@ -5000,7 +5203,7 @@ type CreateChannelGroupResponse struct { func (x *CreateChannelGroupResponse) Reset() { *x = CreateChannelGroupResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[55] + mi := &file_compass_v1_comms_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5012,7 +5215,7 @@ func (x *CreateChannelGroupResponse) String() string { func (*CreateChannelGroupResponse) ProtoMessage() {} func (x *CreateChannelGroupResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[55] + mi := &file_compass_v1_comms_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5025,7 +5228,7 @@ func (x *CreateChannelGroupResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateChannelGroupResponse.ProtoReflect.Descriptor instead. func (*CreateChannelGroupResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{55} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{57} } func (x *CreateChannelGroupResponse) GetGroup() *ChannelGroup { @@ -5046,7 +5249,7 @@ type ListChannelGroupsRequest struct { func (x *ListChannelGroupsRequest) Reset() { *x = ListChannelGroupsRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[56] + mi := &file_compass_v1_comms_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5058,7 +5261,7 @@ func (x *ListChannelGroupsRequest) String() string { func (*ListChannelGroupsRequest) ProtoMessage() {} func (x *ListChannelGroupsRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[56] + mi := &file_compass_v1_comms_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5071,7 +5274,7 @@ func (x *ListChannelGroupsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChannelGroupsRequest.ProtoReflect.Descriptor instead. func (*ListChannelGroupsRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{56} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{58} } func (x *ListChannelGroupsRequest) GetSnapshotSeq() uint64 { @@ -5090,7 +5293,7 @@ type ListChannelGroupsResponse struct { func (x *ListChannelGroupsResponse) Reset() { *x = ListChannelGroupsResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[57] + mi := &file_compass_v1_comms_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5102,7 +5305,7 @@ func (x *ListChannelGroupsResponse) String() string { func (*ListChannelGroupsResponse) ProtoMessage() {} func (x *ListChannelGroupsResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[57] + mi := &file_compass_v1_comms_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5115,7 +5318,7 @@ func (x *ListChannelGroupsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChannelGroupsResponse.ProtoReflect.Descriptor instead. func (*ListChannelGroupsResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{57} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{59} } func (x *ListChannelGroupsResponse) GetGroups() []*ChannelGroup { @@ -5136,7 +5339,7 @@ type ListChannelsRequest struct { func (x *ListChannelsRequest) Reset() { *x = ListChannelsRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[58] + mi := &file_compass_v1_comms_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5148,7 +5351,7 @@ func (x *ListChannelsRequest) String() string { func (*ListChannelsRequest) ProtoMessage() {} func (x *ListChannelsRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[58] + mi := &file_compass_v1_comms_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5161,7 +5364,7 @@ func (x *ListChannelsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChannelsRequest.ProtoReflect.Descriptor instead. func (*ListChannelsRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{58} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{60} } func (x *ListChannelsRequest) GetSnapshotSeq() uint64 { @@ -5180,7 +5383,7 @@ type ListChannelsResponse struct { func (x *ListChannelsResponse) Reset() { *x = ListChannelsResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[59] + mi := &file_compass_v1_comms_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5192,7 +5395,7 @@ func (x *ListChannelsResponse) String() string { func (*ListChannelsResponse) ProtoMessage() {} func (x *ListChannelsResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[59] + mi := &file_compass_v1_comms_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5205,7 +5408,7 @@ func (x *ListChannelsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListChannelsResponse.ProtoReflect.Descriptor instead. func (*ListChannelsResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{59} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{61} } func (x *ListChannelsResponse) GetChannels() []*Channel { @@ -5245,7 +5448,7 @@ type CreateChannelRequest struct { func (x *CreateChannelRequest) Reset() { *x = CreateChannelRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[60] + mi := &file_compass_v1_comms_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5257,7 +5460,7 @@ func (x *CreateChannelRequest) String() string { func (*CreateChannelRequest) ProtoMessage() {} func (x *CreateChannelRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[60] + mi := &file_compass_v1_comms_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5270,7 +5473,7 @@ func (x *CreateChannelRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateChannelRequest.ProtoReflect.Descriptor instead. func (*CreateChannelRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{60} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{62} } func (x *CreateChannelRequest) GetName() string { @@ -5331,7 +5534,7 @@ type CreateChannelResponse struct { func (x *CreateChannelResponse) Reset() { *x = CreateChannelResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[61] + mi := &file_compass_v1_comms_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5343,7 +5546,7 @@ func (x *CreateChannelResponse) String() string { func (*CreateChannelResponse) ProtoMessage() {} func (x *CreateChannelResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[61] + mi := &file_compass_v1_comms_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5356,7 +5559,7 @@ func (x *CreateChannelResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateChannelResponse.ProtoReflect.Descriptor instead. func (*CreateChannelResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{61} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{63} } func (x *CreateChannelResponse) GetChannel() *Channel { @@ -5396,7 +5599,7 @@ type UpdateChannelMembersRequest struct { func (x *UpdateChannelMembersRequest) Reset() { *x = UpdateChannelMembersRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[62] + mi := &file_compass_v1_comms_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5408,7 +5611,7 @@ func (x *UpdateChannelMembersRequest) String() string { func (*UpdateChannelMembersRequest) ProtoMessage() {} func (x *UpdateChannelMembersRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[62] + mi := &file_compass_v1_comms_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5421,7 +5624,7 @@ func (x *UpdateChannelMembersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateChannelMembersRequest.ProtoReflect.Descriptor instead. func (*UpdateChannelMembersRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{62} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{64} } func (x *UpdateChannelMembersRequest) GetChannelId() string { @@ -5475,7 +5678,7 @@ type UpdateChannelMembersResponse struct { func (x *UpdateChannelMembersResponse) Reset() { *x = UpdateChannelMembersResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[63] + mi := &file_compass_v1_comms_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5487,7 +5690,7 @@ func (x *UpdateChannelMembersResponse) String() string { func (*UpdateChannelMembersResponse) ProtoMessage() {} func (x *UpdateChannelMembersResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[63] + mi := &file_compass_v1_comms_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5500,7 +5703,7 @@ func (x *UpdateChannelMembersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateChannelMembersResponse.ProtoReflect.Descriptor instead. func (*UpdateChannelMembersResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{63} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{65} } func (x *UpdateChannelMembersResponse) GetChannel() *Channel { @@ -5522,7 +5725,7 @@ type OpenDMRequest struct { func (x *OpenDMRequest) Reset() { *x = OpenDMRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[64] + mi := &file_compass_v1_comms_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5534,7 +5737,7 @@ func (x *OpenDMRequest) String() string { func (*OpenDMRequest) ProtoMessage() {} func (x *OpenDMRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[64] + mi := &file_compass_v1_comms_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5547,7 +5750,7 @@ func (x *OpenDMRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use OpenDMRequest.ProtoReflect.Descriptor instead. func (*OpenDMRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{64} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{66} } func (x *OpenDMRequest) GetPeerHandle() string { @@ -5569,7 +5772,7 @@ type OpenDMResponse struct { func (x *OpenDMResponse) Reset() { *x = OpenDMResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[65] + mi := &file_compass_v1_comms_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5581,7 +5784,7 @@ func (x *OpenDMResponse) String() string { func (*OpenDMResponse) ProtoMessage() {} func (x *OpenDMResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[65] + mi := &file_compass_v1_comms_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5594,7 +5797,7 @@ func (x *OpenDMResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use OpenDMResponse.ProtoReflect.Descriptor instead. func (*OpenDMResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{65} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{67} } func (x *OpenDMResponse) GetChannel() *Channel { @@ -5625,7 +5828,7 @@ type ReparentAgentRequest struct { func (x *ReparentAgentRequest) Reset() { *x = ReparentAgentRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[66] + mi := &file_compass_v1_comms_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5637,7 +5840,7 @@ func (x *ReparentAgentRequest) String() string { func (*ReparentAgentRequest) ProtoMessage() {} func (x *ReparentAgentRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[66] + mi := &file_compass_v1_comms_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5650,7 +5853,7 @@ func (x *ReparentAgentRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReparentAgentRequest.ProtoReflect.Descriptor instead. func (*ReparentAgentRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{66} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{68} } func (x *ReparentAgentRequest) GetAgentHandle() string { @@ -5676,7 +5879,7 @@ type ReparentAgentResponse struct { func (x *ReparentAgentResponse) Reset() { *x = ReparentAgentResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[67] + mi := &file_compass_v1_comms_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5688,7 +5891,7 @@ func (x *ReparentAgentResponse) String() string { func (*ReparentAgentResponse) ProtoMessage() {} func (x *ReparentAgentResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[67] + mi := &file_compass_v1_comms_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5701,7 +5904,7 @@ func (x *ReparentAgentResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReparentAgentResponse.ProtoReflect.Descriptor instead. func (*ReparentAgentResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{67} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{69} } func (x *ReparentAgentResponse) GetAccount() *Account { @@ -5723,7 +5926,7 @@ type ReparentChannelRequest struct { func (x *ReparentChannelRequest) Reset() { *x = ReparentChannelRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[68] + mi := &file_compass_v1_comms_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5735,7 +5938,7 @@ func (x *ReparentChannelRequest) String() string { func (*ReparentChannelRequest) ProtoMessage() {} func (x *ReparentChannelRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[68] + mi := &file_compass_v1_comms_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5748,7 +5951,7 @@ func (x *ReparentChannelRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReparentChannelRequest.ProtoReflect.Descriptor instead. func (*ReparentChannelRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{68} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{70} } func (x *ReparentChannelRequest) GetChannelId() string { @@ -5774,7 +5977,7 @@ type ReparentChannelResponse struct { func (x *ReparentChannelResponse) Reset() { *x = ReparentChannelResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[69] + mi := &file_compass_v1_comms_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5786,7 +5989,7 @@ func (x *ReparentChannelResponse) String() string { func (*ReparentChannelResponse) ProtoMessage() {} func (x *ReparentChannelResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[69] + mi := &file_compass_v1_comms_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5799,7 +6002,7 @@ func (x *ReparentChannelResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReparentChannelResponse.ProtoReflect.Descriptor instead. func (*ReparentChannelResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{69} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{71} } func (x *ReparentChannelResponse) GetChannel() *Channel { @@ -5826,7 +6029,7 @@ type SetChannelPolicyRequest struct { func (x *SetChannelPolicyRequest) Reset() { *x = SetChannelPolicyRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[70] + mi := &file_compass_v1_comms_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5838,7 +6041,7 @@ func (x *SetChannelPolicyRequest) String() string { func (*SetChannelPolicyRequest) ProtoMessage() {} func (x *SetChannelPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[70] + mi := &file_compass_v1_comms_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5851,7 +6054,7 @@ func (x *SetChannelPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetChannelPolicyRequest.ProtoReflect.Descriptor instead. func (*SetChannelPolicyRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{70} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{72} } func (x *SetChannelPolicyRequest) GetChannelId() string { @@ -5892,7 +6095,7 @@ type SetChannelPolicyResponse struct { func (x *SetChannelPolicyResponse) Reset() { *x = SetChannelPolicyResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[71] + mi := &file_compass_v1_comms_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5904,7 +6107,7 @@ func (x *SetChannelPolicyResponse) String() string { func (*SetChannelPolicyResponse) ProtoMessage() {} func (x *SetChannelPolicyResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[71] + mi := &file_compass_v1_comms_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5917,7 +6120,7 @@ func (x *SetChannelPolicyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetChannelPolicyResponse.ProtoReflect.Descriptor instead. func (*SetChannelPolicyResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{71} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{73} } func (x *SetChannelPolicyResponse) GetChannel() *Channel { @@ -5941,7 +6144,7 @@ type GetRosterRequest struct { func (x *GetRosterRequest) Reset() { *x = GetRosterRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[72] + mi := &file_compass_v1_comms_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5953,7 +6156,7 @@ func (x *GetRosterRequest) String() string { func (*GetRosterRequest) ProtoMessage() {} func (x *GetRosterRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[72] + mi := &file_compass_v1_comms_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5966,7 +6169,7 @@ func (x *GetRosterRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetRosterRequest.ProtoReflect.Descriptor instead. func (*GetRosterRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{72} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{74} } func (x *GetRosterRequest) GetScope() RosterScope { @@ -5992,7 +6195,7 @@ type GetRosterResponse struct { func (x *GetRosterResponse) Reset() { *x = GetRosterResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[73] + mi := &file_compass_v1_comms_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6004,7 +6207,7 @@ func (x *GetRosterResponse) String() string { func (*GetRosterResponse) ProtoMessage() {} func (x *GetRosterResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[73] + mi := &file_compass_v1_comms_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6017,7 +6220,7 @@ func (x *GetRosterResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetRosterResponse.ProtoReflect.Descriptor instead. func (*GetRosterResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{73} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{75} } func (x *GetRosterResponse) GetEntries() []*RosterEntry { @@ -6046,7 +6249,7 @@ type RosterEntry struct { func (x *RosterEntry) Reset() { *x = RosterEntry{} - mi := &file_compass_v1_comms_proto_msgTypes[74] + mi := &file_compass_v1_comms_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6058,7 +6261,7 @@ func (x *RosterEntry) String() string { func (*RosterEntry) ProtoMessage() {} func (x *RosterEntry) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[74] + mi := &file_compass_v1_comms_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6071,7 +6274,7 @@ func (x *RosterEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use RosterEntry.ProtoReflect.Descriptor instead. func (*RosterEntry) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{74} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{76} } func (x *RosterEntry) GetAgentAccountId() string { @@ -6145,7 +6348,7 @@ type UpdatePinnedBoardRequest struct { func (x *UpdatePinnedBoardRequest) Reset() { *x = UpdatePinnedBoardRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[75] + mi := &file_compass_v1_comms_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6157,7 +6360,7 @@ func (x *UpdatePinnedBoardRequest) String() string { func (*UpdatePinnedBoardRequest) ProtoMessage() {} func (x *UpdatePinnedBoardRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[75] + mi := &file_compass_v1_comms_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6170,7 +6373,7 @@ func (x *UpdatePinnedBoardRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdatePinnedBoardRequest.ProtoReflect.Descriptor instead. func (*UpdatePinnedBoardRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{75} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{77} } func (x *UpdatePinnedBoardRequest) GetChannelId() string { @@ -6236,7 +6439,7 @@ type PinMessage struct { func (x *PinMessage) Reset() { *x = PinMessage{} - mi := &file_compass_v1_comms_proto_msgTypes[76] + mi := &file_compass_v1_comms_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6248,7 +6451,7 @@ func (x *PinMessage) String() string { func (*PinMessage) ProtoMessage() {} func (x *PinMessage) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[76] + mi := &file_compass_v1_comms_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6261,7 +6464,7 @@ func (x *PinMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use PinMessage.ProtoReflect.Descriptor instead. func (*PinMessage) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{76} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{78} } func (x *PinMessage) GetMessageId() string { @@ -6288,7 +6491,7 @@ type UpdatePinnedBoardResponse struct { func (x *UpdatePinnedBoardResponse) Reset() { *x = UpdatePinnedBoardResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[77] + mi := &file_compass_v1_comms_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6300,7 +6503,7 @@ func (x *UpdatePinnedBoardResponse) String() string { func (*UpdatePinnedBoardResponse) ProtoMessage() {} func (x *UpdatePinnedBoardResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[77] + mi := &file_compass_v1_comms_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6313,7 +6516,7 @@ func (x *UpdatePinnedBoardResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdatePinnedBoardResponse.ProtoReflect.Descriptor instead. func (*UpdatePinnedBoardResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{77} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{79} } func (x *UpdatePinnedBoardResponse) GetChannel() *Channel { @@ -6334,7 +6537,7 @@ type OpenAgentWorkspaceRequest struct { func (x *OpenAgentWorkspaceRequest) Reset() { *x = OpenAgentWorkspaceRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[78] + mi := &file_compass_v1_comms_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6346,7 +6549,7 @@ func (x *OpenAgentWorkspaceRequest) String() string { func (*OpenAgentWorkspaceRequest) ProtoMessage() {} func (x *OpenAgentWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[78] + mi := &file_compass_v1_comms_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6359,7 +6562,7 @@ func (x *OpenAgentWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use OpenAgentWorkspaceRequest.ProtoReflect.Descriptor instead. func (*OpenAgentWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{78} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{80} } func (x *OpenAgentWorkspaceRequest) GetAgentHandle() string { @@ -6378,7 +6581,7 @@ type OpenAgentWorkspaceResponse struct { func (x *OpenAgentWorkspaceResponse) Reset() { *x = OpenAgentWorkspaceResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[79] + mi := &file_compass_v1_comms_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6390,7 +6593,7 @@ func (x *OpenAgentWorkspaceResponse) String() string { func (*OpenAgentWorkspaceResponse) ProtoMessage() {} func (x *OpenAgentWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[79] + mi := &file_compass_v1_comms_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6403,7 +6606,7 @@ func (x *OpenAgentWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use OpenAgentWorkspaceResponse.ProtoReflect.Descriptor instead. func (*OpenAgentWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{79} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{81} } func (x *OpenAgentWorkspaceResponse) GetWorkspace() *AgentWorkspace { @@ -6438,7 +6641,7 @@ type ListMessagesRequest struct { func (x *ListMessagesRequest) Reset() { *x = ListMessagesRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[80] + mi := &file_compass_v1_comms_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6450,7 +6653,7 @@ func (x *ListMessagesRequest) String() string { func (*ListMessagesRequest) ProtoMessage() {} func (x *ListMessagesRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[80] + mi := &file_compass_v1_comms_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6463,7 +6666,7 @@ func (x *ListMessagesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListMessagesRequest.ProtoReflect.Descriptor instead. func (*ListMessagesRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{80} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{82} } func (x *ListMessagesRequest) GetContainer() isListMessagesRequest_Container { @@ -6530,7 +6733,7 @@ type ListMessagesResponse struct { func (x *ListMessagesResponse) Reset() { *x = ListMessagesResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[81] + mi := &file_compass_v1_comms_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6542,7 +6745,7 @@ func (x *ListMessagesResponse) String() string { func (*ListMessagesResponse) ProtoMessage() {} func (x *ListMessagesResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[81] + mi := &file_compass_v1_comms_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6555,7 +6758,7 @@ func (x *ListMessagesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListMessagesResponse.ProtoReflect.Descriptor instead. func (*ListMessagesResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{81} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{83} } func (x *ListMessagesResponse) GetMessages() []*Message { @@ -6607,7 +6810,7 @@ type PostMessageRequest struct { func (x *PostMessageRequest) Reset() { *x = PostMessageRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[82] + mi := &file_compass_v1_comms_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6619,7 +6822,7 @@ func (x *PostMessageRequest) String() string { func (*PostMessageRequest) ProtoMessage() {} func (x *PostMessageRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[82] + mi := &file_compass_v1_comms_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6632,7 +6835,7 @@ func (x *PostMessageRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PostMessageRequest.ProtoReflect.Descriptor instead. func (*PostMessageRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{82} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{84} } func (x *PostMessageRequest) GetContainer() isPostMessageRequest_Container { @@ -6739,7 +6942,7 @@ type PostMessageResponse struct { func (x *PostMessageResponse) Reset() { *x = PostMessageResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[83] + mi := &file_compass_v1_comms_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6751,7 +6954,7 @@ func (x *PostMessageResponse) String() string { func (*PostMessageResponse) ProtoMessage() {} func (x *PostMessageResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[83] + mi := &file_compass_v1_comms_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6764,7 +6967,7 @@ func (x *PostMessageResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PostMessageResponse.ProtoReflect.Descriptor instead. func (*PostMessageResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{83} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{85} } func (x *PostMessageResponse) GetMessage() *Message { @@ -6786,7 +6989,7 @@ type ListTopicsRequest struct { func (x *ListTopicsRequest) Reset() { *x = ListTopicsRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[84] + mi := &file_compass_v1_comms_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6798,7 +7001,7 @@ func (x *ListTopicsRequest) String() string { func (*ListTopicsRequest) ProtoMessage() {} func (x *ListTopicsRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[84] + mi := &file_compass_v1_comms_proto_msgTypes[86] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6811,7 +7014,7 @@ func (x *ListTopicsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListTopicsRequest.ProtoReflect.Descriptor instead. func (*ListTopicsRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{84} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{86} } func (x *ListTopicsRequest) GetChannelId() string { @@ -6837,7 +7040,7 @@ type ListTopicsResponse struct { func (x *ListTopicsResponse) Reset() { *x = ListTopicsResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[85] + mi := &file_compass_v1_comms_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6849,7 +7052,7 @@ func (x *ListTopicsResponse) String() string { func (*ListTopicsResponse) ProtoMessage() {} func (x *ListTopicsResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[85] + mi := &file_compass_v1_comms_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6862,7 +7065,7 @@ func (x *ListTopicsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListTopicsResponse.ProtoReflect.Descriptor instead. func (*ListTopicsResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{85} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{87} } func (x *ListTopicsResponse) GetTopics() []*Topic { @@ -6887,7 +7090,7 @@ type UpdateTopicRequest struct { func (x *UpdateTopicRequest) Reset() { *x = UpdateTopicRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[86] + mi := &file_compass_v1_comms_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6899,7 +7102,7 @@ func (x *UpdateTopicRequest) String() string { func (*UpdateTopicRequest) ProtoMessage() {} func (x *UpdateTopicRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[86] + mi := &file_compass_v1_comms_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6912,7 +7115,7 @@ func (x *UpdateTopicRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateTopicRequest.ProtoReflect.Descriptor instead. func (*UpdateTopicRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{86} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{88} } func (x *UpdateTopicRequest) GetTopicId() string { @@ -6945,7 +7148,7 @@ type UpdateTopicResponse struct { func (x *UpdateTopicResponse) Reset() { *x = UpdateTopicResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[87] + mi := &file_compass_v1_comms_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6957,7 +7160,7 @@ func (x *UpdateTopicResponse) String() string { func (*UpdateTopicResponse) ProtoMessage() {} func (x *UpdateTopicResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[87] + mi := &file_compass_v1_comms_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6970,7 +7173,7 @@ func (x *UpdateTopicResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateTopicResponse.ProtoReflect.Descriptor instead. func (*UpdateTopicResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{87} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{89} } func (x *UpdateTopicResponse) GetTopic() *Topic { @@ -6993,7 +7196,7 @@ type RespondToAskRequest struct { func (x *RespondToAskRequest) Reset() { *x = RespondToAskRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[88] + mi := &file_compass_v1_comms_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7005,7 +7208,7 @@ func (x *RespondToAskRequest) String() string { func (*RespondToAskRequest) ProtoMessage() {} func (x *RespondToAskRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[88] + mi := &file_compass_v1_comms_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7018,7 +7221,7 @@ func (x *RespondToAskRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RespondToAskRequest.ProtoReflect.Descriptor instead. func (*RespondToAskRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{88} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{90} } func (x *RespondToAskRequest) GetAskId() string { @@ -7050,7 +7253,7 @@ type AskQuestionAnswer struct { func (x *AskQuestionAnswer) Reset() { *x = AskQuestionAnswer{} - mi := &file_compass_v1_comms_proto_msgTypes[89] + mi := &file_compass_v1_comms_proto_msgTypes[91] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7062,7 +7265,7 @@ func (x *AskQuestionAnswer) String() string { func (*AskQuestionAnswer) ProtoMessage() {} func (x *AskQuestionAnswer) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[89] + mi := &file_compass_v1_comms_proto_msgTypes[91] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7075,7 +7278,7 @@ func (x *AskQuestionAnswer) ProtoReflect() protoreflect.Message { // Deprecated: Use AskQuestionAnswer.ProtoReflect.Descriptor instead. func (*AskQuestionAnswer) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{89} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{91} } func (x *AskQuestionAnswer) GetQuestionId() string { @@ -7107,7 +7310,7 @@ type RespondToAskResponse struct { func (x *RespondToAskResponse) Reset() { *x = RespondToAskResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[90] + mi := &file_compass_v1_comms_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7119,7 +7322,7 @@ func (x *RespondToAskResponse) String() string { func (*RespondToAskResponse) ProtoMessage() {} func (x *RespondToAskResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[90] + mi := &file_compass_v1_comms_proto_msgTypes[92] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7132,7 +7335,7 @@ func (x *RespondToAskResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RespondToAskResponse.ProtoReflect.Descriptor instead. func (*RespondToAskResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{90} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{92} } type SearchMessagesRequest struct { @@ -7156,7 +7359,7 @@ type SearchMessagesRequest struct { func (x *SearchMessagesRequest) Reset() { *x = SearchMessagesRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[91] + mi := &file_compass_v1_comms_proto_msgTypes[93] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7168,7 +7371,7 @@ func (x *SearchMessagesRequest) String() string { func (*SearchMessagesRequest) ProtoMessage() {} func (x *SearchMessagesRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[91] + mi := &file_compass_v1_comms_proto_msgTypes[93] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7181,7 +7384,7 @@ func (x *SearchMessagesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SearchMessagesRequest.ProtoReflect.Descriptor instead. func (*SearchMessagesRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{91} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{93} } func (x *SearchMessagesRequest) GetQuery() string { @@ -7241,7 +7444,7 @@ type SearchMessagesResponse struct { func (x *SearchMessagesResponse) Reset() { *x = SearchMessagesResponse{} - mi := &file_compass_v1_comms_proto_msgTypes[92] + mi := &file_compass_v1_comms_proto_msgTypes[94] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7253,7 +7456,7 @@ func (x *SearchMessagesResponse) String() string { func (*SearchMessagesResponse) ProtoMessage() {} func (x *SearchMessagesResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[92] + mi := &file_compass_v1_comms_proto_msgTypes[94] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7266,7 +7469,7 @@ func (x *SearchMessagesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SearchMessagesResponse.ProtoReflect.Descriptor instead. func (*SearchMessagesResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{92} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{94} } func (x *SearchMessagesResponse) GetMessages() []*Message { @@ -7290,7 +7493,7 @@ type SubscribeCommsRequest struct { func (x *SubscribeCommsRequest) Reset() { *x = SubscribeCommsRequest{} - mi := &file_compass_v1_comms_proto_msgTypes[93] + mi := &file_compass_v1_comms_proto_msgTypes[95] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7302,7 +7505,7 @@ func (x *SubscribeCommsRequest) String() string { func (*SubscribeCommsRequest) ProtoMessage() {} func (x *SubscribeCommsRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_comms_proto_msgTypes[93] + mi := &file_compass_v1_comms_proto_msgTypes[95] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7315,7 +7518,7 @@ func (x *SubscribeCommsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SubscribeCommsRequest.ProtoReflect.Descriptor instead. func (*SubscribeCommsRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_comms_proto_rawDescGZIP(), []int{93} + return file_compass_v1_comms_proto_rawDescGZIP(), []int{95} } func (x *SubscribeCommsRequest) GetSinceSeq() uint64 { @@ -7545,7 +7748,7 @@ const file_compass_v1_comms_proto_rawDesc = "" + "\x13observed_at_unix_ms\x18\r \x01(\x03R\x10observedAtUnixMs\x12*\n" + "\x11merge_queue_state\x18\x0e \x01(\tR\x0fmergeQueueState\x12%\n" + "\x0estale_approval\x18\x0f \x01(\bR\rstaleApproval\x12%\n" + - "\x0etracker_status\x18\x10 \x01(\tR\rtrackerStatus\"\xd0\x01\n" + + "\x0etracker_status\x18\x10 \x01(\tR\rtrackerStatus\"\xb0\x02\n" + "\bBoardAsk\x12\x15\n" + "\x06ask_id\x18\x01 \x01(\tR\x05askId\x12\x1d\n" + "\n" + @@ -7555,7 +7758,21 @@ const file_compass_v1_comms_proto_rawDesc = "" + "\x03ref\x18\x04 \x01(\tR\x03ref\x12\x16\n" + "\x06header\x18\x05 \x01(\tR\x06header\x12\x1a\n" + "\banswered\x18\x06 \x01(\bR\banswered\x12)\n" + - "\x11posted_at_unix_ms\x18\a \x01(\x03R\x0epostedAtUnixMs\"\xd9\x03\n" + + "\x11posted_at_unix_ms\x18\a \x01(\x03R\x0epostedAtUnixMs\x12?\n" + + "\fmirror_state\x18\b \x01(\x0e2\x1c.compass.v1.BoardMirrorStateR\vmirrorState\x12\x1d\n" + + "\n" + + "mirror_url\x18\t \x01(\tR\tmirrorUrl\"\x9a\x01\n" + + "\x14MirrorToForgeRequest\x12\x10\n" + + "\x03ref\x18\x01 \x01(\tR\x03ref\x12\x15\n" + + "\x06ask_id\x18\x02 \x01(\tR\x05askId\x12\x19\n" + + "\btopic_id\x18\x03 \x01(\tR\atopicId\x12\x1f\n" + + "\vmessage_ids\x18\x04 \x03(\tR\n" + + "messageIds\x12\x1d\n" + + "\n" + + "channel_id\x18\x05 \x01(\tR\tchannelId\"8\n" + + "\x15MirrorToForgeResponse\x12\x1f\n" + + "\vcomment_url\x18\x01 \x01(\tR\n" + + "commentUrl\"\xd9\x03\n" + "\n" + "AgentBoard\x12(\n" + "\x10agent_account_id\x18\x01 \x01(\tR\x0eagentAccountId\x12\x16\n" + @@ -7851,7 +8068,12 @@ const file_compass_v1_comms_proto_rawDesc = "" + "\x1aBOARD_PR_GATE_QUEUE_FAILED\x10\n" + "\x12\x1f\n" + "\x1bBOARD_PR_GATE_QUEUE_REMOVED\x10\v\x12\x1b\n" + - "\x17BOARD_PR_GATE_CONFLICTS\x10\f*\x91\x01\n" + + "\x17BOARD_PR_GATE_CONFLICTS\x10\f*\x92\x01\n" + + "\x10BoardMirrorState\x12\"\n" + + "\x1eBOARD_MIRROR_STATE_UNSPECIFIED\x10\x00\x12\x1e\n" + + "\x1aBOARD_MIRROR_STATE_PENDING\x10\x01\x12\x1b\n" + + "\x17BOARD_MIRROR_STATE_DONE\x10\x02\x12\x1d\n" + + "\x19BOARD_MIRROR_STATE_FAILED\x10\x03*\x91\x01\n" + "\fPeeringState\x12\x1d\n" + "\x19PEERING_STATE_UNSPECIFIED\x10\x00\x12\"\n" + "\x1ePEERING_STATE_PENDING_OUTGOING\x10\x01\x12\"\n" + @@ -7860,7 +8082,7 @@ const file_compass_v1_comms_proto_rawDesc = "" + "\vRosterScope\x12\x1d\n" + "\x19ROSTER_SCOPE_NEIGHBORHOOD\x10\x00\x12\x18\n" + "\x14ROSTER_SCOPE_SUBTREE\x10\x01\x12\x16\n" + - "\x12ROSTER_SCOPE_OWNER\x10\x022\xea\x10\n" + + "\x12ROSTER_SCOPE_OWNER\x10\x022\xc0\x11\n" + "\fCommsService\x12K\n" + "\n" + "CreateUser\x12\x1d.compass.v1.CreateUserRequest\x1a\x1e.compass.v1.CreateUserResponse\x12N\n" + @@ -7880,7 +8102,8 @@ const file_compass_v1_comms_proto_rawDesc = "" + "\n" + "ListTopics\x12\x1d.compass.v1.ListTopicsRequest\x1a\x1e.compass.v1.ListTopicsResponse\x12N\n" + "\vUpdateTopic\x12\x1e.compass.v1.UpdateTopicRequest\x1a\x1f.compass.v1.UpdateTopicResponse\x12Q\n" + - "\fRespondToAsk\x12\x1f.compass.v1.RespondToAskRequest\x1a .compass.v1.RespondToAskResponse\x12W\n" + + "\fRespondToAsk\x12\x1f.compass.v1.RespondToAskRequest\x1a .compass.v1.RespondToAskResponse\x12T\n" + + "\rMirrorToForge\x12 .compass.v1.MirrorToForgeRequest\x1a!.compass.v1.MirrorToForgeResponse\x12W\n" + "\x0eSearchMessages\x12!.compass.v1.SearchMessagesRequest\x1a\".compass.v1.SearchMessagesResponse\x12]\n" + "\x10SetChannelPolicy\x12#.compass.v1.SetChannelPolicyRequest\x1a$.compass.v1.SetChannelPolicyResponse\x12H\n" + "\tGetRoster\x12\x1c.compass.v1.GetRosterRequest\x1a\x1d.compass.v1.GetRosterResponse\x12`\n" + @@ -7903,8 +8126,8 @@ func file_compass_v1_comms_proto_rawDescGZIP() []byte { return file_compass_v1_comms_proto_rawDescData } -var file_compass_v1_comms_proto_enumTypes = make([]protoimpl.EnumInfo, 13) -var file_compass_v1_comms_proto_msgTypes = make([]protoimpl.MessageInfo, 94) +var file_compass_v1_comms_proto_enumTypes = make([]protoimpl.EnumInfo, 14) +var file_compass_v1_comms_proto_msgTypes = make([]protoimpl.MessageInfo, 96) var file_compass_v1_comms_proto_goTypes = []any{ (UserRole)(0), // 0: compass.v1.UserRole (ChannelGroupVisibility)(0), // 1: compass.v1.ChannelGroupVisibility @@ -7917,252 +8140,258 @@ var file_compass_v1_comms_proto_goTypes = []any{ (BoardWaitingOn)(0), // 8: compass.v1.BoardWaitingOn (BoardRefKind)(0), // 9: compass.v1.BoardRefKind (BoardPrGate)(0), // 10: compass.v1.BoardPrGate - (PeeringState)(0), // 11: compass.v1.PeeringState - (RosterScope)(0), // 12: compass.v1.RosterScope - (*Account)(nil), // 13: compass.v1.Account - (*SystemAccount)(nil), // 14: compass.v1.SystemAccount - (*UserAccount)(nil), // 15: compass.v1.UserAccount - (*AgentAccount)(nil), // 16: compass.v1.AgentAccount - (*ChannelGroup)(nil), // 17: compass.v1.ChannelGroup - (*Channel)(nil), // 18: compass.v1.Channel - (*PinnedEntry)(nil), // 19: compass.v1.PinnedEntry - (*AgentWorkspace)(nil), // 20: compass.v1.AgentWorkspace - (*Topic)(nil), // 21: compass.v1.Topic - (*Message)(nil), // 22: compass.v1.Message - (*MessageBlock)(nil), // 23: compass.v1.MessageBlock - (*AskAnswerBlock)(nil), // 24: compass.v1.AskAnswerBlock - (*Ask)(nil), // 25: compass.v1.Ask - (*AskQuestion)(nil), // 26: compass.v1.AskQuestion - (*AskOption)(nil), // 27: compass.v1.AskOption - (*SubscribeCommsResponse)(nil), // 28: compass.v1.SubscribeCommsResponse - (*MessagePosted)(nil), // 29: compass.v1.MessagePosted - (*MessageUpdated)(nil), // 30: compass.v1.MessageUpdated - (*TopicUpserted)(nil), // 31: compass.v1.TopicUpserted - (*ChannelChanged)(nil), // 32: compass.v1.ChannelChanged - (*ChannelGroupChanged)(nil), // 33: compass.v1.ChannelGroupChanged - (*AccountChanged)(nil), // 34: compass.v1.AccountChanged - (*AgentWorkspaceChanged)(nil), // 35: compass.v1.AgentWorkspaceChanged - (*CommsResyncRequired)(nil), // 36: compass.v1.CommsResyncRequired - (*AgentPresenceChanged)(nil), // 37: compass.v1.AgentPresenceChanged - (*BoardWaiting)(nil), // 38: compass.v1.BoardWaiting - (*BoardSubagent)(nil), // 39: compass.v1.BoardSubagent - (*BoardTask)(nil), // 40: compass.v1.BoardTask - (*BoardRefsOp)(nil), // 41: compass.v1.BoardRefsOp - (*BoardWaitingOp)(nil), // 42: compass.v1.BoardWaitingOp - (*BoardTaskPatch)(nil), // 43: compass.v1.BoardTaskPatch - (*BoardHalt)(nil), // 44: compass.v1.BoardHalt - (*BoardRef)(nil), // 45: compass.v1.BoardRef - (*BoardAsk)(nil), // 46: compass.v1.BoardAsk - (*AgentBoard)(nil), // 47: compass.v1.AgentBoard - (*AgentBoardSummary)(nil), // 48: compass.v1.AgentBoardSummary - (*UpdateAgentBoardRequest)(nil), // 49: compass.v1.UpdateAgentBoardRequest - (*UpdateAgentBoardResponse)(nil), // 50: compass.v1.UpdateAgentBoardResponse - (*ListAgentBoardsRequest)(nil), // 51: compass.v1.ListAgentBoardsRequest - (*ListAgentBoardsResponse)(nil), // 52: compass.v1.ListAgentBoardsResponse - (*AgentBoardChanged)(nil), // 53: compass.v1.AgentBoardChanged - (*Peering)(nil), // 54: compass.v1.Peering - (*ApprovePeerRequest)(nil), // 55: compass.v1.ApprovePeerRequest - (*ApprovePeerResponse)(nil), // 56: compass.v1.ApprovePeerResponse - (*RevokePeerRequest)(nil), // 57: compass.v1.RevokePeerRequest - (*RevokePeerResponse)(nil), // 58: compass.v1.RevokePeerResponse - (*ListPeersRequest)(nil), // 59: compass.v1.ListPeersRequest - (*ListPeersResponse)(nil), // 60: compass.v1.ListPeersResponse - (*CreateUserRequest)(nil), // 61: compass.v1.CreateUserRequest - (*CreateUserResponse)(nil), // 62: compass.v1.CreateUserResponse - (*CreateAgentRequest)(nil), // 63: compass.v1.CreateAgentRequest - (*CreateAgentResponse)(nil), // 64: compass.v1.CreateAgentResponse - (*ListAccountsRequest)(nil), // 65: compass.v1.ListAccountsRequest - (*ListAccountsResponse)(nil), // 66: compass.v1.ListAccountsResponse - (*CreateChannelGroupRequest)(nil), // 67: compass.v1.CreateChannelGroupRequest - (*CreateChannelGroupResponse)(nil), // 68: compass.v1.CreateChannelGroupResponse - (*ListChannelGroupsRequest)(nil), // 69: compass.v1.ListChannelGroupsRequest - (*ListChannelGroupsResponse)(nil), // 70: compass.v1.ListChannelGroupsResponse - (*ListChannelsRequest)(nil), // 71: compass.v1.ListChannelsRequest - (*ListChannelsResponse)(nil), // 72: compass.v1.ListChannelsResponse - (*CreateChannelRequest)(nil), // 73: compass.v1.CreateChannelRequest - (*CreateChannelResponse)(nil), // 74: compass.v1.CreateChannelResponse - (*UpdateChannelMembersRequest)(nil), // 75: compass.v1.UpdateChannelMembersRequest - (*UpdateChannelMembersResponse)(nil), // 76: compass.v1.UpdateChannelMembersResponse - (*OpenDMRequest)(nil), // 77: compass.v1.OpenDMRequest - (*OpenDMResponse)(nil), // 78: compass.v1.OpenDMResponse - (*ReparentAgentRequest)(nil), // 79: compass.v1.ReparentAgentRequest - (*ReparentAgentResponse)(nil), // 80: compass.v1.ReparentAgentResponse - (*ReparentChannelRequest)(nil), // 81: compass.v1.ReparentChannelRequest - (*ReparentChannelResponse)(nil), // 82: compass.v1.ReparentChannelResponse - (*SetChannelPolicyRequest)(nil), // 83: compass.v1.SetChannelPolicyRequest - (*SetChannelPolicyResponse)(nil), // 84: compass.v1.SetChannelPolicyResponse - (*GetRosterRequest)(nil), // 85: compass.v1.GetRosterRequest - (*GetRosterResponse)(nil), // 86: compass.v1.GetRosterResponse - (*RosterEntry)(nil), // 87: compass.v1.RosterEntry - (*UpdatePinnedBoardRequest)(nil), // 88: compass.v1.UpdatePinnedBoardRequest - (*PinMessage)(nil), // 89: compass.v1.PinMessage - (*UpdatePinnedBoardResponse)(nil), // 90: compass.v1.UpdatePinnedBoardResponse - (*OpenAgentWorkspaceRequest)(nil), // 91: compass.v1.OpenAgentWorkspaceRequest - (*OpenAgentWorkspaceResponse)(nil), // 92: compass.v1.OpenAgentWorkspaceResponse - (*ListMessagesRequest)(nil), // 93: compass.v1.ListMessagesRequest - (*ListMessagesResponse)(nil), // 94: compass.v1.ListMessagesResponse - (*PostMessageRequest)(nil), // 95: compass.v1.PostMessageRequest - (*PostMessageResponse)(nil), // 96: compass.v1.PostMessageResponse - (*ListTopicsRequest)(nil), // 97: compass.v1.ListTopicsRequest - (*ListTopicsResponse)(nil), // 98: compass.v1.ListTopicsResponse - (*UpdateTopicRequest)(nil), // 99: compass.v1.UpdateTopicRequest - (*UpdateTopicResponse)(nil), // 100: compass.v1.UpdateTopicResponse - (*RespondToAskRequest)(nil), // 101: compass.v1.RespondToAskRequest - (*AskQuestionAnswer)(nil), // 102: compass.v1.AskQuestionAnswer - (*RespondToAskResponse)(nil), // 103: compass.v1.RespondToAskResponse - (*SearchMessagesRequest)(nil), // 104: compass.v1.SearchMessagesRequest - (*SearchMessagesResponse)(nil), // 105: compass.v1.SearchMessagesResponse - (*SubscribeCommsRequest)(nil), // 106: compass.v1.SubscribeCommsRequest - (IssueState)(0), // 107: compass.v1.IssueState + (BoardMirrorState)(0), // 11: compass.v1.BoardMirrorState + (PeeringState)(0), // 12: compass.v1.PeeringState + (RosterScope)(0), // 13: compass.v1.RosterScope + (*Account)(nil), // 14: compass.v1.Account + (*SystemAccount)(nil), // 15: compass.v1.SystemAccount + (*UserAccount)(nil), // 16: compass.v1.UserAccount + (*AgentAccount)(nil), // 17: compass.v1.AgentAccount + (*ChannelGroup)(nil), // 18: compass.v1.ChannelGroup + (*Channel)(nil), // 19: compass.v1.Channel + (*PinnedEntry)(nil), // 20: compass.v1.PinnedEntry + (*AgentWorkspace)(nil), // 21: compass.v1.AgentWorkspace + (*Topic)(nil), // 22: compass.v1.Topic + (*Message)(nil), // 23: compass.v1.Message + (*MessageBlock)(nil), // 24: compass.v1.MessageBlock + (*AskAnswerBlock)(nil), // 25: compass.v1.AskAnswerBlock + (*Ask)(nil), // 26: compass.v1.Ask + (*AskQuestion)(nil), // 27: compass.v1.AskQuestion + (*AskOption)(nil), // 28: compass.v1.AskOption + (*SubscribeCommsResponse)(nil), // 29: compass.v1.SubscribeCommsResponse + (*MessagePosted)(nil), // 30: compass.v1.MessagePosted + (*MessageUpdated)(nil), // 31: compass.v1.MessageUpdated + (*TopicUpserted)(nil), // 32: compass.v1.TopicUpserted + (*ChannelChanged)(nil), // 33: compass.v1.ChannelChanged + (*ChannelGroupChanged)(nil), // 34: compass.v1.ChannelGroupChanged + (*AccountChanged)(nil), // 35: compass.v1.AccountChanged + (*AgentWorkspaceChanged)(nil), // 36: compass.v1.AgentWorkspaceChanged + (*CommsResyncRequired)(nil), // 37: compass.v1.CommsResyncRequired + (*AgentPresenceChanged)(nil), // 38: compass.v1.AgentPresenceChanged + (*BoardWaiting)(nil), // 39: compass.v1.BoardWaiting + (*BoardSubagent)(nil), // 40: compass.v1.BoardSubagent + (*BoardTask)(nil), // 41: compass.v1.BoardTask + (*BoardRefsOp)(nil), // 42: compass.v1.BoardRefsOp + (*BoardWaitingOp)(nil), // 43: compass.v1.BoardWaitingOp + (*BoardTaskPatch)(nil), // 44: compass.v1.BoardTaskPatch + (*BoardHalt)(nil), // 45: compass.v1.BoardHalt + (*BoardRef)(nil), // 46: compass.v1.BoardRef + (*BoardAsk)(nil), // 47: compass.v1.BoardAsk + (*MirrorToForgeRequest)(nil), // 48: compass.v1.MirrorToForgeRequest + (*MirrorToForgeResponse)(nil), // 49: compass.v1.MirrorToForgeResponse + (*AgentBoard)(nil), // 50: compass.v1.AgentBoard + (*AgentBoardSummary)(nil), // 51: compass.v1.AgentBoardSummary + (*UpdateAgentBoardRequest)(nil), // 52: compass.v1.UpdateAgentBoardRequest + (*UpdateAgentBoardResponse)(nil), // 53: compass.v1.UpdateAgentBoardResponse + (*ListAgentBoardsRequest)(nil), // 54: compass.v1.ListAgentBoardsRequest + (*ListAgentBoardsResponse)(nil), // 55: compass.v1.ListAgentBoardsResponse + (*AgentBoardChanged)(nil), // 56: compass.v1.AgentBoardChanged + (*Peering)(nil), // 57: compass.v1.Peering + (*ApprovePeerRequest)(nil), // 58: compass.v1.ApprovePeerRequest + (*ApprovePeerResponse)(nil), // 59: compass.v1.ApprovePeerResponse + (*RevokePeerRequest)(nil), // 60: compass.v1.RevokePeerRequest + (*RevokePeerResponse)(nil), // 61: compass.v1.RevokePeerResponse + (*ListPeersRequest)(nil), // 62: compass.v1.ListPeersRequest + (*ListPeersResponse)(nil), // 63: compass.v1.ListPeersResponse + (*CreateUserRequest)(nil), // 64: compass.v1.CreateUserRequest + (*CreateUserResponse)(nil), // 65: compass.v1.CreateUserResponse + (*CreateAgentRequest)(nil), // 66: compass.v1.CreateAgentRequest + (*CreateAgentResponse)(nil), // 67: compass.v1.CreateAgentResponse + (*ListAccountsRequest)(nil), // 68: compass.v1.ListAccountsRequest + (*ListAccountsResponse)(nil), // 69: compass.v1.ListAccountsResponse + (*CreateChannelGroupRequest)(nil), // 70: compass.v1.CreateChannelGroupRequest + (*CreateChannelGroupResponse)(nil), // 71: compass.v1.CreateChannelGroupResponse + (*ListChannelGroupsRequest)(nil), // 72: compass.v1.ListChannelGroupsRequest + (*ListChannelGroupsResponse)(nil), // 73: compass.v1.ListChannelGroupsResponse + (*ListChannelsRequest)(nil), // 74: compass.v1.ListChannelsRequest + (*ListChannelsResponse)(nil), // 75: compass.v1.ListChannelsResponse + (*CreateChannelRequest)(nil), // 76: compass.v1.CreateChannelRequest + (*CreateChannelResponse)(nil), // 77: compass.v1.CreateChannelResponse + (*UpdateChannelMembersRequest)(nil), // 78: compass.v1.UpdateChannelMembersRequest + (*UpdateChannelMembersResponse)(nil), // 79: compass.v1.UpdateChannelMembersResponse + (*OpenDMRequest)(nil), // 80: compass.v1.OpenDMRequest + (*OpenDMResponse)(nil), // 81: compass.v1.OpenDMResponse + (*ReparentAgentRequest)(nil), // 82: compass.v1.ReparentAgentRequest + (*ReparentAgentResponse)(nil), // 83: compass.v1.ReparentAgentResponse + (*ReparentChannelRequest)(nil), // 84: compass.v1.ReparentChannelRequest + (*ReparentChannelResponse)(nil), // 85: compass.v1.ReparentChannelResponse + (*SetChannelPolicyRequest)(nil), // 86: compass.v1.SetChannelPolicyRequest + (*SetChannelPolicyResponse)(nil), // 87: compass.v1.SetChannelPolicyResponse + (*GetRosterRequest)(nil), // 88: compass.v1.GetRosterRequest + (*GetRosterResponse)(nil), // 89: compass.v1.GetRosterResponse + (*RosterEntry)(nil), // 90: compass.v1.RosterEntry + (*UpdatePinnedBoardRequest)(nil), // 91: compass.v1.UpdatePinnedBoardRequest + (*PinMessage)(nil), // 92: compass.v1.PinMessage + (*UpdatePinnedBoardResponse)(nil), // 93: compass.v1.UpdatePinnedBoardResponse + (*OpenAgentWorkspaceRequest)(nil), // 94: compass.v1.OpenAgentWorkspaceRequest + (*OpenAgentWorkspaceResponse)(nil), // 95: compass.v1.OpenAgentWorkspaceResponse + (*ListMessagesRequest)(nil), // 96: compass.v1.ListMessagesRequest + (*ListMessagesResponse)(nil), // 97: compass.v1.ListMessagesResponse + (*PostMessageRequest)(nil), // 98: compass.v1.PostMessageRequest + (*PostMessageResponse)(nil), // 99: compass.v1.PostMessageResponse + (*ListTopicsRequest)(nil), // 100: compass.v1.ListTopicsRequest + (*ListTopicsResponse)(nil), // 101: compass.v1.ListTopicsResponse + (*UpdateTopicRequest)(nil), // 102: compass.v1.UpdateTopicRequest + (*UpdateTopicResponse)(nil), // 103: compass.v1.UpdateTopicResponse + (*RespondToAskRequest)(nil), // 104: compass.v1.RespondToAskRequest + (*AskQuestionAnswer)(nil), // 105: compass.v1.AskQuestionAnswer + (*RespondToAskResponse)(nil), // 106: compass.v1.RespondToAskResponse + (*SearchMessagesRequest)(nil), // 107: compass.v1.SearchMessagesRequest + (*SearchMessagesResponse)(nil), // 108: compass.v1.SearchMessagesResponse + (*SubscribeCommsRequest)(nil), // 109: compass.v1.SubscribeCommsRequest + (IssueState)(0), // 110: compass.v1.IssueState } var file_compass_v1_comms_proto_depIdxs = []int32{ - 15, // 0: compass.v1.Account.user:type_name -> compass.v1.UserAccount - 16, // 1: compass.v1.Account.agent:type_name -> compass.v1.AgentAccount - 14, // 2: compass.v1.Account.system:type_name -> compass.v1.SystemAccount + 16, // 0: compass.v1.Account.user:type_name -> compass.v1.UserAccount + 17, // 1: compass.v1.Account.agent:type_name -> compass.v1.AgentAccount + 15, // 2: compass.v1.Account.system:type_name -> compass.v1.SystemAccount 0, // 3: compass.v1.UserAccount.role:type_name -> compass.v1.UserRole 1, // 4: compass.v1.ChannelGroup.visibility:type_name -> compass.v1.ChannelGroupVisibility 4, // 5: compass.v1.Channel.kind:type_name -> compass.v1.ChannelKind 2, // 6: compass.v1.Channel.post_policy:type_name -> compass.v1.ChannelPostPolicy - 19, // 7: compass.v1.Channel.pinned_entries:type_name -> compass.v1.PinnedEntry + 20, // 7: compass.v1.Channel.pinned_entries:type_name -> compass.v1.PinnedEntry 3, // 8: compass.v1.Channel.membership_mode:type_name -> compass.v1.ChannelMembershipMode - 23, // 9: compass.v1.Message.blocks:type_name -> compass.v1.MessageBlock - 25, // 10: compass.v1.MessageBlock.ask:type_name -> compass.v1.Ask - 24, // 11: compass.v1.MessageBlock.ask_answer:type_name -> compass.v1.AskAnswerBlock - 25, // 12: compass.v1.AskAnswerBlock.ask:type_name -> compass.v1.Ask - 26, // 13: compass.v1.Ask.questions:type_name -> compass.v1.AskQuestion - 27, // 14: compass.v1.AskQuestion.options:type_name -> compass.v1.AskOption - 29, // 15: compass.v1.SubscribeCommsResponse.message_posted:type_name -> compass.v1.MessagePosted - 30, // 16: compass.v1.SubscribeCommsResponse.message_updated:type_name -> compass.v1.MessageUpdated - 32, // 17: compass.v1.SubscribeCommsResponse.channel_changed:type_name -> compass.v1.ChannelChanged - 33, // 18: compass.v1.SubscribeCommsResponse.channel_group_changed:type_name -> compass.v1.ChannelGroupChanged - 34, // 19: compass.v1.SubscribeCommsResponse.account_changed:type_name -> compass.v1.AccountChanged - 35, // 20: compass.v1.SubscribeCommsResponse.agent_workspace_changed:type_name -> compass.v1.AgentWorkspaceChanged - 36, // 21: compass.v1.SubscribeCommsResponse.resync_required:type_name -> compass.v1.CommsResyncRequired - 37, // 22: compass.v1.SubscribeCommsResponse.agent_presence_changed:type_name -> compass.v1.AgentPresenceChanged - 31, // 23: compass.v1.SubscribeCommsResponse.topic_upserted:type_name -> compass.v1.TopicUpserted - 53, // 24: compass.v1.SubscribeCommsResponse.agent_board_changed:type_name -> compass.v1.AgentBoardChanged - 22, // 25: compass.v1.MessagePosted.message:type_name -> compass.v1.Message - 22, // 26: compass.v1.MessageUpdated.message:type_name -> compass.v1.Message - 21, // 27: compass.v1.TopicUpserted.topic:type_name -> compass.v1.Topic - 18, // 28: compass.v1.ChannelChanged.channel:type_name -> compass.v1.Channel - 17, // 29: compass.v1.ChannelGroupChanged.group:type_name -> compass.v1.ChannelGroup - 13, // 30: compass.v1.AccountChanged.account:type_name -> compass.v1.Account - 20, // 31: compass.v1.AgentWorkspaceChanged.workspace:type_name -> compass.v1.AgentWorkspace + 24, // 9: compass.v1.Message.blocks:type_name -> compass.v1.MessageBlock + 26, // 10: compass.v1.MessageBlock.ask:type_name -> compass.v1.Ask + 25, // 11: compass.v1.MessageBlock.ask_answer:type_name -> compass.v1.AskAnswerBlock + 26, // 12: compass.v1.AskAnswerBlock.ask:type_name -> compass.v1.Ask + 27, // 13: compass.v1.Ask.questions:type_name -> compass.v1.AskQuestion + 28, // 14: compass.v1.AskQuestion.options:type_name -> compass.v1.AskOption + 30, // 15: compass.v1.SubscribeCommsResponse.message_posted:type_name -> compass.v1.MessagePosted + 31, // 16: compass.v1.SubscribeCommsResponse.message_updated:type_name -> compass.v1.MessageUpdated + 33, // 17: compass.v1.SubscribeCommsResponse.channel_changed:type_name -> compass.v1.ChannelChanged + 34, // 18: compass.v1.SubscribeCommsResponse.channel_group_changed:type_name -> compass.v1.ChannelGroupChanged + 35, // 19: compass.v1.SubscribeCommsResponse.account_changed:type_name -> compass.v1.AccountChanged + 36, // 20: compass.v1.SubscribeCommsResponse.agent_workspace_changed:type_name -> compass.v1.AgentWorkspaceChanged + 37, // 21: compass.v1.SubscribeCommsResponse.resync_required:type_name -> compass.v1.CommsResyncRequired + 38, // 22: compass.v1.SubscribeCommsResponse.agent_presence_changed:type_name -> compass.v1.AgentPresenceChanged + 32, // 23: compass.v1.SubscribeCommsResponse.topic_upserted:type_name -> compass.v1.TopicUpserted + 56, // 24: compass.v1.SubscribeCommsResponse.agent_board_changed:type_name -> compass.v1.AgentBoardChanged + 23, // 25: compass.v1.MessagePosted.message:type_name -> compass.v1.Message + 23, // 26: compass.v1.MessageUpdated.message:type_name -> compass.v1.Message + 22, // 27: compass.v1.TopicUpserted.topic:type_name -> compass.v1.Topic + 19, // 28: compass.v1.ChannelChanged.channel:type_name -> compass.v1.Channel + 18, // 29: compass.v1.ChannelGroupChanged.group:type_name -> compass.v1.ChannelGroup + 14, // 30: compass.v1.AccountChanged.account:type_name -> compass.v1.Account + 21, // 31: compass.v1.AgentWorkspaceChanged.workspace:type_name -> compass.v1.AgentWorkspace 5, // 32: compass.v1.AgentPresenceChanged.presence:type_name -> compass.v1.AgentPresence 8, // 33: compass.v1.BoardWaiting.on:type_name -> compass.v1.BoardWaitingOn 6, // 34: compass.v1.BoardTask.state:type_name -> compass.v1.BoardTaskState - 38, // 35: compass.v1.BoardTask.waiting:type_name -> compass.v1.BoardWaiting - 39, // 36: compass.v1.BoardTask.subagents:type_name -> compass.v1.BoardSubagent - 38, // 37: compass.v1.BoardWaitingOp.values:type_name -> compass.v1.BoardWaiting + 39, // 35: compass.v1.BoardTask.waiting:type_name -> compass.v1.BoardWaiting + 40, // 36: compass.v1.BoardTask.subagents:type_name -> compass.v1.BoardSubagent + 39, // 37: compass.v1.BoardWaitingOp.values:type_name -> compass.v1.BoardWaiting 6, // 38: compass.v1.BoardTaskPatch.state:type_name -> compass.v1.BoardTaskState - 41, // 39: compass.v1.BoardTaskPatch.refs:type_name -> compass.v1.BoardRefsOp - 42, // 40: compass.v1.BoardTaskPatch.waiting:type_name -> compass.v1.BoardWaitingOp - 39, // 41: compass.v1.BoardTaskPatch.upsert_subagents:type_name -> compass.v1.BoardSubagent + 42, // 39: compass.v1.BoardTaskPatch.refs:type_name -> compass.v1.BoardRefsOp + 43, // 40: compass.v1.BoardTaskPatch.waiting:type_name -> compass.v1.BoardWaitingOp + 40, // 41: compass.v1.BoardTaskPatch.upsert_subagents:type_name -> compass.v1.BoardSubagent 7, // 42: compass.v1.BoardHalt.kind:type_name -> compass.v1.BoardHaltKind 9, // 43: compass.v1.BoardRef.kind:type_name -> compass.v1.BoardRefKind - 107, // 44: compass.v1.BoardRef.issue_state:type_name -> compass.v1.IssueState + 110, // 44: compass.v1.BoardRef.issue_state:type_name -> compass.v1.IssueState 10, // 45: compass.v1.BoardRef.gate:type_name -> compass.v1.BoardPrGate - 44, // 46: compass.v1.AgentBoard.halt:type_name -> compass.v1.BoardHalt - 40, // 47: compass.v1.AgentBoard.tasks:type_name -> compass.v1.BoardTask - 45, // 48: compass.v1.AgentBoard.refs:type_name -> compass.v1.BoardRef - 46, // 49: compass.v1.AgentBoard.asks:type_name -> compass.v1.BoardAsk - 44, // 50: compass.v1.AgentBoardSummary.halt:type_name -> compass.v1.BoardHalt - 43, // 51: compass.v1.UpdateAgentBoardRequest.upsert_tasks:type_name -> compass.v1.BoardTaskPatch - 44, // 52: compass.v1.UpdateAgentBoardRequest.halt:type_name -> compass.v1.BoardHalt - 47, // 53: compass.v1.UpdateAgentBoardResponse.board:type_name -> compass.v1.AgentBoard - 8, // 54: compass.v1.ListAgentBoardsRequest.waiting_on:type_name -> compass.v1.BoardWaitingOn - 6, // 55: compass.v1.ListAgentBoardsRequest.state:type_name -> compass.v1.BoardTaskState - 7, // 56: compass.v1.ListAgentBoardsRequest.halt:type_name -> compass.v1.BoardHaltKind - 47, // 57: compass.v1.ListAgentBoardsResponse.boards:type_name -> compass.v1.AgentBoard - 47, // 58: compass.v1.AgentBoardChanged.board:type_name -> compass.v1.AgentBoard - 11, // 59: compass.v1.Peering.state:type_name -> compass.v1.PeeringState - 54, // 60: compass.v1.ApprovePeerResponse.peering:type_name -> compass.v1.Peering - 54, // 61: compass.v1.ListPeersResponse.peerings:type_name -> compass.v1.Peering - 13, // 62: compass.v1.CreateUserResponse.account:type_name -> compass.v1.Account - 13, // 63: compass.v1.CreateAgentResponse.account:type_name -> compass.v1.Account - 13, // 64: compass.v1.ListAccountsResponse.accounts:type_name -> compass.v1.Account - 1, // 65: compass.v1.CreateChannelGroupRequest.visibility:type_name -> compass.v1.ChannelGroupVisibility - 17, // 66: compass.v1.CreateChannelGroupResponse.group:type_name -> compass.v1.ChannelGroup - 17, // 67: compass.v1.ListChannelGroupsResponse.groups:type_name -> compass.v1.ChannelGroup - 18, // 68: compass.v1.ListChannelsResponse.channels:type_name -> compass.v1.Channel - 4, // 69: compass.v1.CreateChannelRequest.kind:type_name -> compass.v1.ChannelKind - 3, // 70: compass.v1.CreateChannelRequest.membership_mode:type_name -> compass.v1.ChannelMembershipMode - 18, // 71: compass.v1.CreateChannelResponse.channel:type_name -> compass.v1.Channel - 18, // 72: compass.v1.UpdateChannelMembersResponse.channel:type_name -> compass.v1.Channel - 18, // 73: compass.v1.OpenDMResponse.channel:type_name -> compass.v1.Channel - 13, // 74: compass.v1.ReparentAgentResponse.account:type_name -> compass.v1.Account - 18, // 75: compass.v1.ReparentChannelResponse.channel:type_name -> compass.v1.Channel - 2, // 76: compass.v1.SetChannelPolicyRequest.post_policy:type_name -> compass.v1.ChannelPostPolicy - 18, // 77: compass.v1.SetChannelPolicyResponse.channel:type_name -> compass.v1.Channel - 12, // 78: compass.v1.GetRosterRequest.scope:type_name -> compass.v1.RosterScope - 87, // 79: compass.v1.GetRosterResponse.entries:type_name -> compass.v1.RosterEntry - 5, // 80: compass.v1.RosterEntry.presence:type_name -> compass.v1.AgentPresence - 48, // 81: compass.v1.RosterEntry.board:type_name -> compass.v1.AgentBoardSummary - 89, // 82: compass.v1.UpdatePinnedBoardRequest.pin:type_name -> compass.v1.PinMessage - 18, // 83: compass.v1.UpdatePinnedBoardResponse.channel:type_name -> compass.v1.Channel - 20, // 84: compass.v1.OpenAgentWorkspaceResponse.workspace:type_name -> compass.v1.AgentWorkspace - 22, // 85: compass.v1.ListMessagesResponse.messages:type_name -> compass.v1.Message - 23, // 86: compass.v1.PostMessageRequest.blocks:type_name -> compass.v1.MessageBlock - 22, // 87: compass.v1.PostMessageResponse.message:type_name -> compass.v1.Message - 21, // 88: compass.v1.ListTopicsResponse.topics:type_name -> compass.v1.Topic - 21, // 89: compass.v1.UpdateTopicResponse.topic:type_name -> compass.v1.Topic - 102, // 90: compass.v1.RespondToAskRequest.answers:type_name -> compass.v1.AskQuestionAnswer - 22, // 91: compass.v1.SearchMessagesResponse.messages:type_name -> compass.v1.Message - 61, // 92: compass.v1.CommsService.CreateUser:input_type -> compass.v1.CreateUserRequest - 63, // 93: compass.v1.CommsService.CreateAgent:input_type -> compass.v1.CreateAgentRequest - 65, // 94: compass.v1.CommsService.ListAccounts:input_type -> compass.v1.ListAccountsRequest - 67, // 95: compass.v1.CommsService.CreateChannelGroup:input_type -> compass.v1.CreateChannelGroupRequest - 69, // 96: compass.v1.CommsService.ListChannelGroups:input_type -> compass.v1.ListChannelGroupsRequest - 71, // 97: compass.v1.CommsService.ListChannels:input_type -> compass.v1.ListChannelsRequest - 73, // 98: compass.v1.CommsService.CreateChannel:input_type -> compass.v1.CreateChannelRequest - 75, // 99: compass.v1.CommsService.UpdateChannelMembers:input_type -> compass.v1.UpdateChannelMembersRequest - 77, // 100: compass.v1.CommsService.OpenDM:input_type -> compass.v1.OpenDMRequest - 79, // 101: compass.v1.CommsService.ReparentAgent:input_type -> compass.v1.ReparentAgentRequest - 81, // 102: compass.v1.CommsService.ReparentChannel:input_type -> compass.v1.ReparentChannelRequest - 91, // 103: compass.v1.CommsService.OpenAgentWorkspace:input_type -> compass.v1.OpenAgentWorkspaceRequest - 93, // 104: compass.v1.CommsService.ListMessages:input_type -> compass.v1.ListMessagesRequest - 95, // 105: compass.v1.CommsService.PostMessage:input_type -> compass.v1.PostMessageRequest - 97, // 106: compass.v1.CommsService.ListTopics:input_type -> compass.v1.ListTopicsRequest - 99, // 107: compass.v1.CommsService.UpdateTopic:input_type -> compass.v1.UpdateTopicRequest - 101, // 108: compass.v1.CommsService.RespondToAsk:input_type -> compass.v1.RespondToAskRequest - 104, // 109: compass.v1.CommsService.SearchMessages:input_type -> compass.v1.SearchMessagesRequest - 83, // 110: compass.v1.CommsService.SetChannelPolicy:input_type -> compass.v1.SetChannelPolicyRequest - 85, // 111: compass.v1.CommsService.GetRoster:input_type -> compass.v1.GetRosterRequest - 88, // 112: compass.v1.CommsService.UpdatePinnedBoard:input_type -> compass.v1.UpdatePinnedBoardRequest - 55, // 113: compass.v1.CommsService.ApprovePeer:input_type -> compass.v1.ApprovePeerRequest - 57, // 114: compass.v1.CommsService.RevokePeer:input_type -> compass.v1.RevokePeerRequest - 59, // 115: compass.v1.CommsService.ListPeers:input_type -> compass.v1.ListPeersRequest - 106, // 116: compass.v1.CommsService.SubscribeComms:input_type -> compass.v1.SubscribeCommsRequest - 62, // 117: compass.v1.CommsService.CreateUser:output_type -> compass.v1.CreateUserResponse - 64, // 118: compass.v1.CommsService.CreateAgent:output_type -> compass.v1.CreateAgentResponse - 66, // 119: compass.v1.CommsService.ListAccounts:output_type -> compass.v1.ListAccountsResponse - 68, // 120: compass.v1.CommsService.CreateChannelGroup:output_type -> compass.v1.CreateChannelGroupResponse - 70, // 121: compass.v1.CommsService.ListChannelGroups:output_type -> compass.v1.ListChannelGroupsResponse - 72, // 122: compass.v1.CommsService.ListChannels:output_type -> compass.v1.ListChannelsResponse - 74, // 123: compass.v1.CommsService.CreateChannel:output_type -> compass.v1.CreateChannelResponse - 76, // 124: compass.v1.CommsService.UpdateChannelMembers:output_type -> compass.v1.UpdateChannelMembersResponse - 78, // 125: compass.v1.CommsService.OpenDM:output_type -> compass.v1.OpenDMResponse - 80, // 126: compass.v1.CommsService.ReparentAgent:output_type -> compass.v1.ReparentAgentResponse - 82, // 127: compass.v1.CommsService.ReparentChannel:output_type -> compass.v1.ReparentChannelResponse - 92, // 128: compass.v1.CommsService.OpenAgentWorkspace:output_type -> compass.v1.OpenAgentWorkspaceResponse - 94, // 129: compass.v1.CommsService.ListMessages:output_type -> compass.v1.ListMessagesResponse - 96, // 130: compass.v1.CommsService.PostMessage:output_type -> compass.v1.PostMessageResponse - 98, // 131: compass.v1.CommsService.ListTopics:output_type -> compass.v1.ListTopicsResponse - 100, // 132: compass.v1.CommsService.UpdateTopic:output_type -> compass.v1.UpdateTopicResponse - 103, // 133: compass.v1.CommsService.RespondToAsk:output_type -> compass.v1.RespondToAskResponse - 105, // 134: compass.v1.CommsService.SearchMessages:output_type -> compass.v1.SearchMessagesResponse - 84, // 135: compass.v1.CommsService.SetChannelPolicy:output_type -> compass.v1.SetChannelPolicyResponse - 86, // 136: compass.v1.CommsService.GetRoster:output_type -> compass.v1.GetRosterResponse - 90, // 137: compass.v1.CommsService.UpdatePinnedBoard:output_type -> compass.v1.UpdatePinnedBoardResponse - 56, // 138: compass.v1.CommsService.ApprovePeer:output_type -> compass.v1.ApprovePeerResponse - 58, // 139: compass.v1.CommsService.RevokePeer:output_type -> compass.v1.RevokePeerResponse - 60, // 140: compass.v1.CommsService.ListPeers:output_type -> compass.v1.ListPeersResponse - 28, // 141: compass.v1.CommsService.SubscribeComms:output_type -> compass.v1.SubscribeCommsResponse - 117, // [117:142] is the sub-list for method output_type - 92, // [92:117] is the sub-list for method input_type - 92, // [92:92] is the sub-list for extension type_name - 92, // [92:92] is the sub-list for extension extendee - 0, // [0:92] is the sub-list for field type_name + 11, // 46: compass.v1.BoardAsk.mirror_state:type_name -> compass.v1.BoardMirrorState + 45, // 47: compass.v1.AgentBoard.halt:type_name -> compass.v1.BoardHalt + 41, // 48: compass.v1.AgentBoard.tasks:type_name -> compass.v1.BoardTask + 46, // 49: compass.v1.AgentBoard.refs:type_name -> compass.v1.BoardRef + 47, // 50: compass.v1.AgentBoard.asks:type_name -> compass.v1.BoardAsk + 45, // 51: compass.v1.AgentBoardSummary.halt:type_name -> compass.v1.BoardHalt + 44, // 52: compass.v1.UpdateAgentBoardRequest.upsert_tasks:type_name -> compass.v1.BoardTaskPatch + 45, // 53: compass.v1.UpdateAgentBoardRequest.halt:type_name -> compass.v1.BoardHalt + 50, // 54: compass.v1.UpdateAgentBoardResponse.board:type_name -> compass.v1.AgentBoard + 8, // 55: compass.v1.ListAgentBoardsRequest.waiting_on:type_name -> compass.v1.BoardWaitingOn + 6, // 56: compass.v1.ListAgentBoardsRequest.state:type_name -> compass.v1.BoardTaskState + 7, // 57: compass.v1.ListAgentBoardsRequest.halt:type_name -> compass.v1.BoardHaltKind + 50, // 58: compass.v1.ListAgentBoardsResponse.boards:type_name -> compass.v1.AgentBoard + 50, // 59: compass.v1.AgentBoardChanged.board:type_name -> compass.v1.AgentBoard + 12, // 60: compass.v1.Peering.state:type_name -> compass.v1.PeeringState + 57, // 61: compass.v1.ApprovePeerResponse.peering:type_name -> compass.v1.Peering + 57, // 62: compass.v1.ListPeersResponse.peerings:type_name -> compass.v1.Peering + 14, // 63: compass.v1.CreateUserResponse.account:type_name -> compass.v1.Account + 14, // 64: compass.v1.CreateAgentResponse.account:type_name -> compass.v1.Account + 14, // 65: compass.v1.ListAccountsResponse.accounts:type_name -> compass.v1.Account + 1, // 66: compass.v1.CreateChannelGroupRequest.visibility:type_name -> compass.v1.ChannelGroupVisibility + 18, // 67: compass.v1.CreateChannelGroupResponse.group:type_name -> compass.v1.ChannelGroup + 18, // 68: compass.v1.ListChannelGroupsResponse.groups:type_name -> compass.v1.ChannelGroup + 19, // 69: compass.v1.ListChannelsResponse.channels:type_name -> compass.v1.Channel + 4, // 70: compass.v1.CreateChannelRequest.kind:type_name -> compass.v1.ChannelKind + 3, // 71: compass.v1.CreateChannelRequest.membership_mode:type_name -> compass.v1.ChannelMembershipMode + 19, // 72: compass.v1.CreateChannelResponse.channel:type_name -> compass.v1.Channel + 19, // 73: compass.v1.UpdateChannelMembersResponse.channel:type_name -> compass.v1.Channel + 19, // 74: compass.v1.OpenDMResponse.channel:type_name -> compass.v1.Channel + 14, // 75: compass.v1.ReparentAgentResponse.account:type_name -> compass.v1.Account + 19, // 76: compass.v1.ReparentChannelResponse.channel:type_name -> compass.v1.Channel + 2, // 77: compass.v1.SetChannelPolicyRequest.post_policy:type_name -> compass.v1.ChannelPostPolicy + 19, // 78: compass.v1.SetChannelPolicyResponse.channel:type_name -> compass.v1.Channel + 13, // 79: compass.v1.GetRosterRequest.scope:type_name -> compass.v1.RosterScope + 90, // 80: compass.v1.GetRosterResponse.entries:type_name -> compass.v1.RosterEntry + 5, // 81: compass.v1.RosterEntry.presence:type_name -> compass.v1.AgentPresence + 51, // 82: compass.v1.RosterEntry.board:type_name -> compass.v1.AgentBoardSummary + 92, // 83: compass.v1.UpdatePinnedBoardRequest.pin:type_name -> compass.v1.PinMessage + 19, // 84: compass.v1.UpdatePinnedBoardResponse.channel:type_name -> compass.v1.Channel + 21, // 85: compass.v1.OpenAgentWorkspaceResponse.workspace:type_name -> compass.v1.AgentWorkspace + 23, // 86: compass.v1.ListMessagesResponse.messages:type_name -> compass.v1.Message + 24, // 87: compass.v1.PostMessageRequest.blocks:type_name -> compass.v1.MessageBlock + 23, // 88: compass.v1.PostMessageResponse.message:type_name -> compass.v1.Message + 22, // 89: compass.v1.ListTopicsResponse.topics:type_name -> compass.v1.Topic + 22, // 90: compass.v1.UpdateTopicResponse.topic:type_name -> compass.v1.Topic + 105, // 91: compass.v1.RespondToAskRequest.answers:type_name -> compass.v1.AskQuestionAnswer + 23, // 92: compass.v1.SearchMessagesResponse.messages:type_name -> compass.v1.Message + 64, // 93: compass.v1.CommsService.CreateUser:input_type -> compass.v1.CreateUserRequest + 66, // 94: compass.v1.CommsService.CreateAgent:input_type -> compass.v1.CreateAgentRequest + 68, // 95: compass.v1.CommsService.ListAccounts:input_type -> compass.v1.ListAccountsRequest + 70, // 96: compass.v1.CommsService.CreateChannelGroup:input_type -> compass.v1.CreateChannelGroupRequest + 72, // 97: compass.v1.CommsService.ListChannelGroups:input_type -> compass.v1.ListChannelGroupsRequest + 74, // 98: compass.v1.CommsService.ListChannels:input_type -> compass.v1.ListChannelsRequest + 76, // 99: compass.v1.CommsService.CreateChannel:input_type -> compass.v1.CreateChannelRequest + 78, // 100: compass.v1.CommsService.UpdateChannelMembers:input_type -> compass.v1.UpdateChannelMembersRequest + 80, // 101: compass.v1.CommsService.OpenDM:input_type -> compass.v1.OpenDMRequest + 82, // 102: compass.v1.CommsService.ReparentAgent:input_type -> compass.v1.ReparentAgentRequest + 84, // 103: compass.v1.CommsService.ReparentChannel:input_type -> compass.v1.ReparentChannelRequest + 94, // 104: compass.v1.CommsService.OpenAgentWorkspace:input_type -> compass.v1.OpenAgentWorkspaceRequest + 96, // 105: compass.v1.CommsService.ListMessages:input_type -> compass.v1.ListMessagesRequest + 98, // 106: compass.v1.CommsService.PostMessage:input_type -> compass.v1.PostMessageRequest + 100, // 107: compass.v1.CommsService.ListTopics:input_type -> compass.v1.ListTopicsRequest + 102, // 108: compass.v1.CommsService.UpdateTopic:input_type -> compass.v1.UpdateTopicRequest + 104, // 109: compass.v1.CommsService.RespondToAsk:input_type -> compass.v1.RespondToAskRequest + 48, // 110: compass.v1.CommsService.MirrorToForge:input_type -> compass.v1.MirrorToForgeRequest + 107, // 111: compass.v1.CommsService.SearchMessages:input_type -> compass.v1.SearchMessagesRequest + 86, // 112: compass.v1.CommsService.SetChannelPolicy:input_type -> compass.v1.SetChannelPolicyRequest + 88, // 113: compass.v1.CommsService.GetRoster:input_type -> compass.v1.GetRosterRequest + 91, // 114: compass.v1.CommsService.UpdatePinnedBoard:input_type -> compass.v1.UpdatePinnedBoardRequest + 58, // 115: compass.v1.CommsService.ApprovePeer:input_type -> compass.v1.ApprovePeerRequest + 60, // 116: compass.v1.CommsService.RevokePeer:input_type -> compass.v1.RevokePeerRequest + 62, // 117: compass.v1.CommsService.ListPeers:input_type -> compass.v1.ListPeersRequest + 109, // 118: compass.v1.CommsService.SubscribeComms:input_type -> compass.v1.SubscribeCommsRequest + 65, // 119: compass.v1.CommsService.CreateUser:output_type -> compass.v1.CreateUserResponse + 67, // 120: compass.v1.CommsService.CreateAgent:output_type -> compass.v1.CreateAgentResponse + 69, // 121: compass.v1.CommsService.ListAccounts:output_type -> compass.v1.ListAccountsResponse + 71, // 122: compass.v1.CommsService.CreateChannelGroup:output_type -> compass.v1.CreateChannelGroupResponse + 73, // 123: compass.v1.CommsService.ListChannelGroups:output_type -> compass.v1.ListChannelGroupsResponse + 75, // 124: compass.v1.CommsService.ListChannels:output_type -> compass.v1.ListChannelsResponse + 77, // 125: compass.v1.CommsService.CreateChannel:output_type -> compass.v1.CreateChannelResponse + 79, // 126: compass.v1.CommsService.UpdateChannelMembers:output_type -> compass.v1.UpdateChannelMembersResponse + 81, // 127: compass.v1.CommsService.OpenDM:output_type -> compass.v1.OpenDMResponse + 83, // 128: compass.v1.CommsService.ReparentAgent:output_type -> compass.v1.ReparentAgentResponse + 85, // 129: compass.v1.CommsService.ReparentChannel:output_type -> compass.v1.ReparentChannelResponse + 95, // 130: compass.v1.CommsService.OpenAgentWorkspace:output_type -> compass.v1.OpenAgentWorkspaceResponse + 97, // 131: compass.v1.CommsService.ListMessages:output_type -> compass.v1.ListMessagesResponse + 99, // 132: compass.v1.CommsService.PostMessage:output_type -> compass.v1.PostMessageResponse + 101, // 133: compass.v1.CommsService.ListTopics:output_type -> compass.v1.ListTopicsResponse + 103, // 134: compass.v1.CommsService.UpdateTopic:output_type -> compass.v1.UpdateTopicResponse + 106, // 135: compass.v1.CommsService.RespondToAsk:output_type -> compass.v1.RespondToAskResponse + 49, // 136: compass.v1.CommsService.MirrorToForge:output_type -> compass.v1.MirrorToForgeResponse + 108, // 137: compass.v1.CommsService.SearchMessages:output_type -> compass.v1.SearchMessagesResponse + 87, // 138: compass.v1.CommsService.SetChannelPolicy:output_type -> compass.v1.SetChannelPolicyResponse + 89, // 139: compass.v1.CommsService.GetRoster:output_type -> compass.v1.GetRosterResponse + 93, // 140: compass.v1.CommsService.UpdatePinnedBoard:output_type -> compass.v1.UpdatePinnedBoardResponse + 59, // 141: compass.v1.CommsService.ApprovePeer:output_type -> compass.v1.ApprovePeerResponse + 61, // 142: compass.v1.CommsService.RevokePeer:output_type -> compass.v1.RevokePeerResponse + 63, // 143: compass.v1.CommsService.ListPeers:output_type -> compass.v1.ListPeersResponse + 29, // 144: compass.v1.CommsService.SubscribeComms:output_type -> compass.v1.SubscribeCommsResponse + 119, // [119:145] is the sub-list for method output_type + 93, // [93:119] is the sub-list for method input_type + 93, // [93:93] is the sub-list for extension type_name + 93, // [93:93] is the sub-list for extension extendee + 0, // [0:93] is the sub-list for field type_name } func init() { file_compass_v1_comms_proto_init() } @@ -8196,21 +8425,21 @@ func file_compass_v1_comms_proto_init() { (*SubscribeCommsResponse_AgentBoardChanged)(nil), } file_compass_v1_comms_proto_msgTypes[30].OneofWrappers = []any{} - file_compass_v1_comms_proto_msgTypes[36].OneofWrappers = []any{} - file_compass_v1_comms_proto_msgTypes[75].OneofWrappers = []any{ + file_compass_v1_comms_proto_msgTypes[38].OneofWrappers = []any{} + file_compass_v1_comms_proto_msgTypes[77].OneofWrappers = []any{ (*UpdatePinnedBoardRequest_Pin)(nil), (*UpdatePinnedBoardRequest_UnpinMessageId)(nil), } - file_compass_v1_comms_proto_msgTypes[80].OneofWrappers = []any{ + file_compass_v1_comms_proto_msgTypes[82].OneofWrappers = []any{ (*ListMessagesRequest_ChannelId)(nil), } - file_compass_v1_comms_proto_msgTypes[82].OneofWrappers = []any{ + file_compass_v1_comms_proto_msgTypes[84].OneofWrappers = []any{ (*PostMessageRequest_ChannelId)(nil), (*PostMessageRequest_TopicId)(nil), (*PostMessageRequest_TopicName)(nil), } - file_compass_v1_comms_proto_msgTypes[86].OneofWrappers = []any{} - file_compass_v1_comms_proto_msgTypes[91].OneofWrappers = []any{ + file_compass_v1_comms_proto_msgTypes[88].OneofWrappers = []any{} + file_compass_v1_comms_proto_msgTypes[93].OneofWrappers = []any{ (*SearchMessagesRequest_ChannelId)(nil), } type x struct{} @@ -8218,8 +8447,8 @@ func file_compass_v1_comms_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_compass_v1_comms_proto_rawDesc), len(file_compass_v1_comms_proto_rawDesc)), - NumEnums: 13, - NumMessages: 94, + NumEnums: 14, + NumMessages: 96, NumExtensions: 0, NumServices: 1, }, diff --git a/go/gen/compass/v1/compass.pb.go b/go/gen/compass/v1/compass.pb.go index 2333a423e..0aefea151 100644 --- a/go/gen/compass/v1/compass.pb.go +++ b/go/gen/compass/v1/compass.pb.go @@ -820,6 +820,63 @@ func (ForgeProvider) EnumDescriptor() ([]byte, []int) { return file_compass_v1_compass_proto_rawDescGZIP(), []int{13} } +// Whether asks naming a forge coordinate may be mirrored by an agent or user. +type ForgeMirrorMode int32 + +const ( + // The mode is omitted or invalid. + ForgeMirrorMode_FORGE_MIRROR_MODE_UNSPECIFIED ForgeMirrorMode = 0 + // Mirroring is disabled. No settings row is stored for this mode. + ForgeMirrorMode_FORGE_MIRROR_MODE_OFF ForgeMirrorMode = 1 + // A user may click to mirror; agents may not mirror automatically. + ForgeMirrorMode_FORGE_MIRROR_MODE_MANUAL ForgeMirrorMode = 2 + // A user may click and agents may mirror asks automatically. + ForgeMirrorMode_FORGE_MIRROR_MODE_AUTO ForgeMirrorMode = 3 +) + +// Enum value maps for ForgeMirrorMode. +var ( + ForgeMirrorMode_name = map[int32]string{ + 0: "FORGE_MIRROR_MODE_UNSPECIFIED", + 1: "FORGE_MIRROR_MODE_OFF", + 2: "FORGE_MIRROR_MODE_MANUAL", + 3: "FORGE_MIRROR_MODE_AUTO", + } + ForgeMirrorMode_value = map[string]int32{ + "FORGE_MIRROR_MODE_UNSPECIFIED": 0, + "FORGE_MIRROR_MODE_OFF": 1, + "FORGE_MIRROR_MODE_MANUAL": 2, + "FORGE_MIRROR_MODE_AUTO": 3, + } +) + +func (x ForgeMirrorMode) Enum() *ForgeMirrorMode { + p := new(ForgeMirrorMode) + *p = x + return p +} + +func (x ForgeMirrorMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ForgeMirrorMode) Descriptor() protoreflect.EnumDescriptor { + return file_compass_v1_compass_proto_enumTypes[14].Descriptor() +} + +func (ForgeMirrorMode) Type() protoreflect.EnumType { + return &file_compass_v1_compass_proto_enumTypes[14] +} + +func (x ForgeMirrorMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ForgeMirrorMode.Descriptor instead. +func (ForgeMirrorMode) EnumDescriptor() ([]byte, []int) { + return file_compass_v1_compass_proto_rawDescGZIP(), []int{14} +} + type GrantAgentRepositoryRequest struct { state protoimpl.MessageState `protogen:"open.v1"` AgentHandle string `protobuf:"bytes,1,opt,name=agent_handle,json=agentHandle,proto3" json:"agent_handle,omitempty"` // bare (the caller's agent) or `owner/agent` @@ -6215,6 +6272,254 @@ func (x *ForgeRef) GetHost() string { return "" } +// The tenant-selected mirror mode for one forge repository or Linear team. +type ForgeMirrorSetting struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The provider and host that identify the forge coordinate. + Forge *ForgeRef `protobuf:"bytes,1,opt,name=forge,proto3" json:"forge,omitempty"` + // The repository name or Linear team key on that coordinate. + Repo string `protobuf:"bytes,2,opt,name=repo,proto3" json:"repo,omitempty"` + // The selected mirror policy for the coordinate. + Mode ForgeMirrorMode `protobuf:"varint,3,opt,name=mode,proto3,enum=compass.v1.ForgeMirrorMode" json:"mode,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ForgeMirrorSetting) Reset() { + *x = ForgeMirrorSetting{} + mi := &file_compass_v1_compass_proto_msgTypes[96] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ForgeMirrorSetting) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ForgeMirrorSetting) ProtoMessage() {} + +func (x *ForgeMirrorSetting) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_compass_proto_msgTypes[96] + 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 ForgeMirrorSetting.ProtoReflect.Descriptor instead. +func (*ForgeMirrorSetting) Descriptor() ([]byte, []int) { + return file_compass_v1_compass_proto_rawDescGZIP(), []int{96} +} + +func (x *ForgeMirrorSetting) GetForge() *ForgeRef { + if x != nil { + return x.Forge + } + return nil +} + +func (x *ForgeMirrorSetting) GetRepo() string { + if x != nil { + return x.Repo + } + return "" +} + +func (x *ForgeMirrorSetting) GetMode() ForgeMirrorMode { + if x != nil { + return x.Mode + } + return ForgeMirrorMode_FORGE_MIRROR_MODE_UNSPECIFIED +} + +// Request to list enabled forge-repository mirror settings. +type ListForgeMirrorSettingsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListForgeMirrorSettingsRequest) Reset() { + *x = ListForgeMirrorSettingsRequest{} + mi := &file_compass_v1_compass_proto_msgTypes[97] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListForgeMirrorSettingsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListForgeMirrorSettingsRequest) ProtoMessage() {} + +func (x *ListForgeMirrorSettingsRequest) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_compass_proto_msgTypes[97] + 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 ListForgeMirrorSettingsRequest.ProtoReflect.Descriptor instead. +func (*ListForgeMirrorSettingsRequest) Descriptor() ([]byte, []int) { + return file_compass_v1_compass_proto_rawDescGZIP(), []int{97} +} + +// The caller's settings for enabled forge repositories. +type ListForgeMirrorSettingsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Each enabled repository and its selected mirror mode. + Settings []*ForgeMirrorSetting `protobuf:"bytes,1,rep,name=settings,proto3" json:"settings,omitempty"` + // Whether the authenticated caller can change these settings. + CanEdit bool `protobuf:"varint,2,opt,name=can_edit,json=canEdit,proto3" json:"can_edit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListForgeMirrorSettingsResponse) Reset() { + *x = ListForgeMirrorSettingsResponse{} + mi := &file_compass_v1_compass_proto_msgTypes[98] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListForgeMirrorSettingsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListForgeMirrorSettingsResponse) ProtoMessage() {} + +func (x *ListForgeMirrorSettingsResponse) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_compass_proto_msgTypes[98] + 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 ListForgeMirrorSettingsResponse.ProtoReflect.Descriptor instead. +func (*ListForgeMirrorSettingsResponse) Descriptor() ([]byte, []int) { + return file_compass_v1_compass_proto_rawDescGZIP(), []int{98} +} + +func (x *ListForgeMirrorSettingsResponse) GetSettings() []*ForgeMirrorSetting { + if x != nil { + return x.Settings + } + return nil +} + +func (x *ListForgeMirrorSettingsResponse) GetCanEdit() bool { + if x != nil { + return x.CanEdit + } + return false +} + +// Request to set a mirror mode for one enabled forge repository. +type SetForgeMirrorSettingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The coordinate and mode to set. UNSPECIFIED is invalid. + Setting *ForgeMirrorSetting `protobuf:"bytes,1,opt,name=setting,proto3" json:"setting,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetForgeMirrorSettingRequest) Reset() { + *x = SetForgeMirrorSettingRequest{} + mi := &file_compass_v1_compass_proto_msgTypes[99] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetForgeMirrorSettingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetForgeMirrorSettingRequest) ProtoMessage() {} + +func (x *SetForgeMirrorSettingRequest) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_compass_proto_msgTypes[99] + 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 SetForgeMirrorSettingRequest.ProtoReflect.Descriptor instead. +func (*SetForgeMirrorSettingRequest) Descriptor() ([]byte, []int) { + return file_compass_v1_compass_proto_rawDescGZIP(), []int{99} +} + +func (x *SetForgeMirrorSettingRequest) GetSetting() *ForgeMirrorSetting { + if x != nil { + return x.Setting + } + return nil +} + +// The stored setting after a successful update. +type SetForgeMirrorSettingResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The coordinate and mode that were stored. + Setting *ForgeMirrorSetting `protobuf:"bytes,1,opt,name=setting,proto3" json:"setting,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetForgeMirrorSettingResponse) Reset() { + *x = SetForgeMirrorSettingResponse{} + mi := &file_compass_v1_compass_proto_msgTypes[100] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetForgeMirrorSettingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetForgeMirrorSettingResponse) ProtoMessage() {} + +func (x *SetForgeMirrorSettingResponse) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_compass_proto_msgTypes[100] + 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 SetForgeMirrorSettingResponse.ProtoReflect.Descriptor instead. +func (*SetForgeMirrorSettingResponse) Descriptor() ([]byte, []int) { + return file_compass_v1_compass_proto_rawDescGZIP(), []int{100} +} + +func (x *SetForgeMirrorSettingResponse) GetSetting() *ForgeMirrorSetting { + if x != nil { + return x.Setting + } + return nil +} + // The board unit: a Compass Issue — the forge issue's fields PLUS the Compass // agent attribution PLUS the Compass machinery, translated from raw forge data // at server ingestion. @@ -6258,7 +6563,7 @@ type Issue struct { func (x *Issue) Reset() { *x = Issue{} - mi := &file_compass_v1_compass_proto_msgTypes[96] + mi := &file_compass_v1_compass_proto_msgTypes[101] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6270,7 +6575,7 @@ func (x *Issue) String() string { func (*Issue) ProtoMessage() {} func (x *Issue) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[96] + mi := &file_compass_v1_compass_proto_msgTypes[101] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6283,7 +6588,7 @@ func (x *Issue) ProtoReflect() protoreflect.Message { // Deprecated: Use Issue.ProtoReflect.Descriptor instead. func (*Issue) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{96} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{101} } func (x *Issue) GetId() string { @@ -6451,7 +6756,7 @@ type PullRequest struct { func (x *PullRequest) Reset() { *x = PullRequest{} - mi := &file_compass_v1_compass_proto_msgTypes[97] + mi := &file_compass_v1_compass_proto_msgTypes[102] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6463,7 +6768,7 @@ func (x *PullRequest) String() string { func (*PullRequest) ProtoMessage() {} func (x *PullRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[97] + mi := &file_compass_v1_compass_proto_msgTypes[102] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6476,7 +6781,7 @@ func (x *PullRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PullRequest.ProtoReflect.Descriptor instead. func (*PullRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{97} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{102} } func (x *PullRequest) GetForge() *ForgeRef { @@ -6618,7 +6923,7 @@ type ChecksSummary struct { func (x *ChecksSummary) Reset() { *x = ChecksSummary{} - mi := &file_compass_v1_compass_proto_msgTypes[98] + mi := &file_compass_v1_compass_proto_msgTypes[103] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6630,7 +6935,7 @@ func (x *ChecksSummary) String() string { func (*ChecksSummary) ProtoMessage() {} func (x *ChecksSummary) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[98] + mi := &file_compass_v1_compass_proto_msgTypes[103] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6643,7 +6948,7 @@ func (x *ChecksSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use ChecksSummary.ProtoReflect.Descriptor instead. func (*ChecksSummary) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{98} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{103} } func (x *ChecksSummary) GetHeadSha() string { @@ -6679,7 +6984,7 @@ type Check struct { func (x *Check) Reset() { *x = Check{} - mi := &file_compass_v1_compass_proto_msgTypes[99] + mi := &file_compass_v1_compass_proto_msgTypes[104] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6691,7 +6996,7 @@ func (x *Check) String() string { func (*Check) ProtoMessage() {} func (x *Check) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[99] + mi := &file_compass_v1_compass_proto_msgTypes[104] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6704,7 +7009,7 @@ func (x *Check) ProtoReflect() protoreflect.Message { // Deprecated: Use Check.ProtoReflect.Descriptor instead. func (*Check) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{99} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{104} } func (x *Check) GetName() string { @@ -6748,7 +7053,7 @@ type ChangedStats struct { func (x *ChangedStats) Reset() { *x = ChangedStats{} - mi := &file_compass_v1_compass_proto_msgTypes[100] + mi := &file_compass_v1_compass_proto_msgTypes[105] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6760,7 +7065,7 @@ func (x *ChangedStats) String() string { func (*ChangedStats) ProtoMessage() {} func (x *ChangedStats) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[100] + mi := &file_compass_v1_compass_proto_msgTypes[105] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6773,7 +7078,7 @@ func (x *ChangedStats) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangedStats.ProtoReflect.Descriptor instead. func (*ChangedStats) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{100} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{105} } func (x *ChangedStats) GetFiles() uint32 { @@ -6813,7 +7118,7 @@ type TrackerRef struct { func (x *TrackerRef) Reset() { *x = TrackerRef{} - mi := &file_compass_v1_compass_proto_msgTypes[101] + mi := &file_compass_v1_compass_proto_msgTypes[106] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6825,7 +7130,7 @@ func (x *TrackerRef) String() string { func (*TrackerRef) ProtoMessage() {} func (x *TrackerRef) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[101] + mi := &file_compass_v1_compass_proto_msgTypes[106] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6838,7 +7143,7 @@ func (x *TrackerRef) ProtoReflect() protoreflect.Message { // Deprecated: Use TrackerRef.ProtoReflect.Descriptor instead. func (*TrackerRef) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{101} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{106} } func (x *TrackerRef) GetKind() string { @@ -6903,7 +7208,7 @@ type Review struct { func (x *Review) Reset() { *x = Review{} - mi := &file_compass_v1_compass_proto_msgTypes[102] + mi := &file_compass_v1_compass_proto_msgTypes[107] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6915,7 +7220,7 @@ func (x *Review) String() string { func (*Review) ProtoMessage() {} func (x *Review) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[102] + mi := &file_compass_v1_compass_proto_msgTypes[107] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6928,7 +7233,7 @@ func (x *Review) ProtoReflect() protoreflect.Message { // Deprecated: Use Review.ProtoReflect.Descriptor instead. func (*Review) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{102} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{107} } func (x *Review) GetAuthor() string { @@ -6977,7 +7282,7 @@ type ReviewThread struct { func (x *ReviewThread) Reset() { *x = ReviewThread{} - mi := &file_compass_v1_compass_proto_msgTypes[103] + mi := &file_compass_v1_compass_proto_msgTypes[108] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6989,7 +7294,7 @@ func (x *ReviewThread) String() string { func (*ReviewThread) ProtoMessage() {} func (x *ReviewThread) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[103] + mi := &file_compass_v1_compass_proto_msgTypes[108] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7002,7 +7307,7 @@ func (x *ReviewThread) ProtoReflect() protoreflect.Message { // Deprecated: Use ReviewThread.ProtoReflect.Descriptor instead. func (*ReviewThread) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{103} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{108} } func (x *ReviewThread) GetPath() string { @@ -7037,7 +7342,7 @@ type Comment struct { func (x *Comment) Reset() { *x = Comment{} - mi := &file_compass_v1_compass_proto_msgTypes[104] + mi := &file_compass_v1_compass_proto_msgTypes[109] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7049,7 +7354,7 @@ func (x *Comment) String() string { func (*Comment) ProtoMessage() {} func (x *Comment) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[104] + mi := &file_compass_v1_compass_proto_msgTypes[109] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7062,7 +7367,7 @@ func (x *Comment) ProtoReflect() protoreflect.Message { // Deprecated: Use Comment.ProtoReflect.Descriptor instead. func (*Comment) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{104} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{109} } func (x *Comment) GetAuthor() string { @@ -7101,7 +7406,7 @@ type MergeQueueEntry struct { func (x *MergeQueueEntry) Reset() { *x = MergeQueueEntry{} - mi := &file_compass_v1_compass_proto_msgTypes[105] + mi := &file_compass_v1_compass_proto_msgTypes[110] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7113,7 +7418,7 @@ func (x *MergeQueueEntry) String() string { func (*MergeQueueEntry) ProtoMessage() {} func (x *MergeQueueEntry) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[105] + mi := &file_compass_v1_compass_proto_msgTypes[110] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7126,7 +7431,7 @@ func (x *MergeQueueEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use MergeQueueEntry.ProtoReflect.Descriptor instead. func (*MergeQueueEntry) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{105} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{110} } func (x *MergeQueueEntry) GetProvider() string { @@ -7484,7 +7789,19 @@ const file_compass_v1_compass_proto_rawDesc = "" + "\fowner_handle\x18\x02 \x01(\tR\vownerHandle\"U\n" + "\bForgeRef\x125\n" + "\bprovider\x18\x01 \x01(\x0e2\x19.compass.v1.ForgeProviderR\bprovider\x12\x12\n" + - "\x04host\x18\x02 \x01(\tR\x04host\"\xed\x04\n" + + "\x04host\x18\x02 \x01(\tR\x04host\"\x85\x01\n" + + "\x12ForgeMirrorSetting\x12*\n" + + "\x05forge\x18\x01 \x01(\v2\x14.compass.v1.ForgeRefR\x05forge\x12\x12\n" + + "\x04repo\x18\x02 \x01(\tR\x04repo\x12/\n" + + "\x04mode\x18\x03 \x01(\x0e2\x1b.compass.v1.ForgeMirrorModeR\x04mode\" \n" + + "\x1eListForgeMirrorSettingsRequest\"x\n" + + "\x1fListForgeMirrorSettingsResponse\x12:\n" + + "\bsettings\x18\x01 \x03(\v2\x1e.compass.v1.ForgeMirrorSettingR\bsettings\x12\x19\n" + + "\bcan_edit\x18\x02 \x01(\bR\acanEdit\"X\n" + + "\x1cSetForgeMirrorSettingRequest\x128\n" + + "\asetting\x18\x01 \x01(\v2\x1e.compass.v1.ForgeMirrorSettingR\asetting\"Y\n" + + "\x1dSetForgeMirrorSettingResponse\x128\n" + + "\asetting\x18\x01 \x01(\v2\x1e.compass.v1.ForgeMirrorSettingR\asetting\"\xed\x04\n" + "\x05Issue\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12*\n" + "\x05forge\x18\x02 \x01(\v2\x14.compass.v1.ForgeRefR\x05forge\x12\x12\n" + @@ -7646,7 +7963,12 @@ const file_compass_v1_compass_proto_rawDesc = "" + "\x15FORGE_PROVIDER_GITHUB\x10\x01\x12\x19\n" + "\x15FORGE_PROVIDER_GITLAB\x10\x02\x12\x1a\n" + "\x16FORGE_PROVIDER_FORGEJO\x10\x03\x12\x19\n" + - "\x15FORGE_PROVIDER_LINEAR\x10\x042\xb3\x13\n" + + "\x15FORGE_PROVIDER_LINEAR\x10\x04*\x89\x01\n" + + "\x0fForgeMirrorMode\x12!\n" + + "\x1dFORGE_MIRROR_MODE_UNSPECIFIED\x10\x00\x12\x19\n" + + "\x15FORGE_MIRROR_MODE_OFF\x10\x01\x12\x1c\n" + + "\x18FORGE_MIRROR_MODE_MANUAL\x10\x02\x12\x1a\n" + + "\x16FORGE_MIRROR_MODE_AUTO\x10\x032\x95\x15\n" + "\x0eCompassService\x12T\n" + "\rGetServerInfo\x12 .compass.v1.GetServerInfoRequest\x1a!.compass.v1.GetServerInfoResponse\x12?\n" + "\x06WhoAmI\x12\x19.compass.v1.WhoAmIRequest\x1a\x1a.compass.v1.WhoAmIResponse\x12Z\n" + @@ -7676,7 +7998,9 @@ const file_compass_v1_compass_proto_rawDesc = "" + "\x11DeleteAgentConfig\x12$.compass.v1.DeleteAgentConfigRequest\x1a%.compass.v1.DeleteAgentConfigResponse\x12]\n" + "\x10PutModelRegistry\x12#.compass.v1.PutModelRegistryRequest\x1a$.compass.v1.PutModelRegistryResponse\x12]\n" + "\x10GetModelRegistry\x12#.compass.v1.GetModelRegistryRequest\x1a$.compass.v1.GetModelRegistryResponse\x12f\n" + - "\x13DeleteModelRegistry\x12&.compass.v1.DeleteModelRegistryRequest\x1a'.compass.v1.DeleteModelRegistryResponse2\xdf\x02\n" + + "\x13DeleteModelRegistry\x12&.compass.v1.DeleteModelRegistryRequest\x1a'.compass.v1.DeleteModelRegistryResponse\x12r\n" + + "\x17ListForgeMirrorSettings\x12*.compass.v1.ListForgeMirrorSettingsRequest\x1a+.compass.v1.ListForgeMirrorSettingsResponse\x12l\n" + + "\x15SetForgeMirrorSetting\x12(.compass.v1.SetForgeMirrorSettingRequest\x1a).compass.v1.SetForgeMirrorSettingResponse2\xdf\x02\n" + "\x0eSecretsService\x12H\n" + "\tSetSecret\x12\x1c.compass.v1.SetSecretRequest\x1a\x1d.compass.v1.SetSecretResponse\x12N\n" + "\vListSecrets\x12\x1e.compass.v1.ListSecretsRequest\x1a\x1f.compass.v1.ListSecretsResponse\x12Q\n" + @@ -7699,8 +8023,8 @@ func file_compass_v1_compass_proto_rawDescGZIP() []byte { return file_compass_v1_compass_proto_rawDescData } -var file_compass_v1_compass_proto_enumTypes = make([]protoimpl.EnumInfo, 14) -var file_compass_v1_compass_proto_msgTypes = make([]protoimpl.MessageInfo, 107) +var file_compass_v1_compass_proto_enumTypes = make([]protoimpl.EnumInfo, 15) +var file_compass_v1_compass_proto_msgTypes = make([]protoimpl.MessageInfo, 112) var file_compass_v1_compass_proto_goTypes = []any{ (SecretDelivery)(0), // 0: compass.v1.SecretDelivery (SecretKind)(0), // 1: compass.v1.SecretKind @@ -7716,258 +8040,273 @@ var file_compass_v1_compass_proto_goTypes = []any{ (SessionErrorKind)(0), // 11: compass.v1.SessionErrorKind (IssueState)(0), // 12: compass.v1.IssueState (ForgeProvider)(0), // 13: compass.v1.ForgeProvider - (*GrantAgentRepositoryRequest)(nil), // 14: compass.v1.GrantAgentRepositoryRequest - (*GrantAgentRepositoryResponse)(nil), // 15: compass.v1.GrantAgentRepositoryResponse - (*RevokeAgentRepositoryRequest)(nil), // 16: compass.v1.RevokeAgentRepositoryRequest - (*RevokeAgentRepositoryResponse)(nil), // 17: compass.v1.RevokeAgentRepositoryResponse - (*ListAgentRepositoriesRequest)(nil), // 18: compass.v1.ListAgentRepositoriesRequest - (*ListAgentRepositoriesResponse)(nil), // 19: compass.v1.ListAgentRepositoriesResponse - (*SetSecretRequest)(nil), // 20: compass.v1.SetSecretRequest - (*SetSecretResponse)(nil), // 21: compass.v1.SetSecretResponse - (*ListSecretsRequest)(nil), // 22: compass.v1.ListSecretsRequest - (*ListSecretsResponse)(nil), // 23: compass.v1.ListSecretsResponse - (*SecretStatus)(nil), // 24: compass.v1.SecretStatus - (*DeleteSecretRequest)(nil), // 25: compass.v1.DeleteSecretRequest - (*DeleteSecretResponse)(nil), // 26: compass.v1.DeleteSecretResponse - (*ListServerSecretsRequest)(nil), // 27: compass.v1.ListServerSecretsRequest - (*ListServerSecretsResponse)(nil), // 28: compass.v1.ListServerSecretsResponse - (*ServerSecretStatus)(nil), // 29: compass.v1.ServerSecretStatus - (*GetRefLinkRulesRequest)(nil), // 30: compass.v1.GetRefLinkRulesRequest - (*RefLinkRule)(nil), // 31: compass.v1.RefLinkRule - (*GetRefLinkRulesResponse)(nil), // 32: compass.v1.GetRefLinkRulesResponse - (*GetServerInfoRequest)(nil), // 33: compass.v1.GetServerInfoRequest - (*GetServerInfoResponse)(nil), // 34: compass.v1.GetServerInfoResponse - (*EnrolledRunner)(nil), // 35: compass.v1.EnrolledRunner - (*WhoAmIRequest)(nil), // 36: compass.v1.WhoAmIRequest - (*WhoAmIResponse)(nil), // 37: compass.v1.WhoAmIResponse - (*GetTourStateRequest)(nil), // 38: compass.v1.GetTourStateRequest - (*GetTourStateResponse)(nil), // 39: compass.v1.GetTourStateResponse - (*ClaimTourStartRequest)(nil), // 40: compass.v1.ClaimTourStartRequest - (*ClaimTourStartResponse)(nil), // 41: compass.v1.ClaimTourStartResponse - (*SetTourStateRequest)(nil), // 42: compass.v1.SetTourStateRequest - (*SetTourStateResponse)(nil), // 43: compass.v1.SetTourStateResponse - (*SubscribeEventsRequest)(nil), // 44: compass.v1.SubscribeEventsRequest - (*SubscribeEventsResponse)(nil), // 45: compass.v1.SubscribeEventsResponse - (*ListBoardIssuesRequest)(nil), // 46: compass.v1.ListBoardIssuesRequest - (*ListBoardIssuesResponse)(nil), // 47: compass.v1.ListBoardIssuesResponse - (*SearchIssuesRequest)(nil), // 48: compass.v1.SearchIssuesRequest - (*SearchIssuesResponse)(nil), // 49: compass.v1.SearchIssuesResponse - (*ServerStatus)(nil), // 50: compass.v1.ServerStatus - (*ResyncRequired)(nil), // 51: compass.v1.ResyncRequired - (*AgentSessionStatus)(nil), // 52: compass.v1.AgentSessionStatus - (*AgentMessageChunk)(nil), // 53: compass.v1.AgentMessageChunk - (*AgentToolCall)(nil), // 54: compass.v1.AgentToolCall - (*AgentPlan)(nil), // 55: compass.v1.AgentPlan - (*AgentPlanEntry)(nil), // 56: compass.v1.AgentPlanEntry - (*SessionEvent)(nil), // 57: compass.v1.SessionEvent - (*SessionAssistantText)(nil), // 58: compass.v1.SessionAssistantText - (*SessionThinking)(nil), // 59: compass.v1.SessionThinking - (*SessionToolCall)(nil), // 60: compass.v1.SessionToolCall - (*SessionToolCallUpdate)(nil), // 61: compass.v1.SessionToolCallUpdate - (*SessionFileDiff)(nil), // 62: compass.v1.SessionFileDiff - (*SessionPlan)(nil), // 63: compass.v1.SessionPlan - (*SessionNotice)(nil), // 64: compass.v1.SessionNotice - (*SessionInjection)(nil), // 65: compass.v1.SessionInjection - (*SessionError)(nil), // 66: compass.v1.SessionError - (*SessionBatchPending)(nil), // 67: compass.v1.SessionBatchPending - (*SubscribeAgentSessionRequest)(nil), // 68: compass.v1.SubscribeAgentSessionRequest - (*AgentSessionFrame)(nil), // 69: compass.v1.AgentSessionFrame - (*ProvisionAgentWorkspaceRequest)(nil), // 70: compass.v1.ProvisionAgentWorkspaceRequest - (*ProvisionAgentWorkspaceResponse)(nil), // 71: compass.v1.ProvisionAgentWorkspaceResponse - (*RemoveAgentWorkspaceRequest)(nil), // 72: compass.v1.RemoveAgentWorkspaceRequest - (*RemoveAgentWorkspaceResponse)(nil), // 73: compass.v1.RemoveAgentWorkspaceResponse - (*StartAgentSessionRequest)(nil), // 74: compass.v1.StartAgentSessionRequest - (*StartAgentSessionResponse)(nil), // 75: compass.v1.StartAgentSessionResponse - (*SpawnAgentRequest)(nil), // 76: compass.v1.SpawnAgentRequest - (*SpawnAgentResponse)(nil), // 77: compass.v1.SpawnAgentResponse - (*StopAgentSessionRequest)(nil), // 78: compass.v1.StopAgentSessionRequest - (*StopAgentSessionResponse)(nil), // 79: compass.v1.StopAgentSessionResponse - (*SkipBatchWindowRequest)(nil), // 80: compass.v1.SkipBatchWindowRequest - (*SkipBatchWindowResponse)(nil), // 81: compass.v1.SkipBatchWindowResponse - (*ReloadAgentSessionRequest)(nil), // 82: compass.v1.ReloadAgentSessionRequest - (*ReloadAgentSessionResponse)(nil), // 83: compass.v1.ReloadAgentSessionResponse - (*GetAgentStatusRequest)(nil), // 84: compass.v1.GetAgentStatusRequest - (*GetAgentStatusResponse)(nil), // 85: compass.v1.GetAgentStatusResponse - (*IssueTokenRequest)(nil), // 86: compass.v1.IssueTokenRequest - (*IssueTokenResponse)(nil), // 87: compass.v1.IssueTokenResponse - (*RenameAccountRequest)(nil), // 88: compass.v1.RenameAccountRequest - (*RenameAccountResponse)(nil), // 89: compass.v1.RenameAccountResponse - (*RevokeTokenRequest)(nil), // 90: compass.v1.RevokeTokenRequest - (*RevokeTokenResponse)(nil), // 91: compass.v1.RevokeTokenResponse - (*PutAgentConfigRequest)(nil), // 92: compass.v1.PutAgentConfigRequest - (*PutAgentConfigResponse)(nil), // 93: compass.v1.PutAgentConfigResponse - (*GetAgentConfigInfoRequest)(nil), // 94: compass.v1.GetAgentConfigInfoRequest - (*GetAgentConfigInfoResponse)(nil), // 95: compass.v1.GetAgentConfigInfoResponse - (*DeleteAgentConfigRequest)(nil), // 96: compass.v1.DeleteAgentConfigRequest - (*DeleteAgentConfigResponse)(nil), // 97: compass.v1.DeleteAgentConfigResponse - (*ModelCandidate)(nil), // 98: compass.v1.ModelCandidate - (*ModelMetadata)(nil), // 99: compass.v1.ModelMetadata - (*ModelRegistryEntry)(nil), // 100: compass.v1.ModelRegistryEntry - (*ModelRegistry)(nil), // 101: compass.v1.ModelRegistry - (*PutModelRegistryRequest)(nil), // 102: compass.v1.PutModelRegistryRequest - (*PutModelRegistryResponse)(nil), // 103: compass.v1.PutModelRegistryResponse - (*GetModelRegistryRequest)(nil), // 104: compass.v1.GetModelRegistryRequest - (*GetModelRegistryResponse)(nil), // 105: compass.v1.GetModelRegistryResponse - (*DeleteModelRegistryRequest)(nil), // 106: compass.v1.DeleteModelRegistryRequest - (*DeleteModelRegistryResponse)(nil), // 107: compass.v1.DeleteModelRegistryResponse - (*AgentAttribution)(nil), // 108: compass.v1.AgentAttribution - (*ForgeRef)(nil), // 109: compass.v1.ForgeRef - (*Issue)(nil), // 110: compass.v1.Issue - (*PullRequest)(nil), // 111: compass.v1.PullRequest - (*ChecksSummary)(nil), // 112: compass.v1.ChecksSummary - (*Check)(nil), // 113: compass.v1.Check - (*ChangedStats)(nil), // 114: compass.v1.ChangedStats - (*TrackerRef)(nil), // 115: compass.v1.TrackerRef - (*Review)(nil), // 116: compass.v1.Review - (*ReviewThread)(nil), // 117: compass.v1.ReviewThread - (*Comment)(nil), // 118: compass.v1.Comment - (*MergeQueueEntry)(nil), // 119: compass.v1.MergeQueueEntry - nil, // 120: compass.v1.ModelRegistry.EntriesEntry - (*timestamppb.Timestamp)(nil), // 121: google.protobuf.Timestamp + (ForgeMirrorMode)(0), // 14: compass.v1.ForgeMirrorMode + (*GrantAgentRepositoryRequest)(nil), // 15: compass.v1.GrantAgentRepositoryRequest + (*GrantAgentRepositoryResponse)(nil), // 16: compass.v1.GrantAgentRepositoryResponse + (*RevokeAgentRepositoryRequest)(nil), // 17: compass.v1.RevokeAgentRepositoryRequest + (*RevokeAgentRepositoryResponse)(nil), // 18: compass.v1.RevokeAgentRepositoryResponse + (*ListAgentRepositoriesRequest)(nil), // 19: compass.v1.ListAgentRepositoriesRequest + (*ListAgentRepositoriesResponse)(nil), // 20: compass.v1.ListAgentRepositoriesResponse + (*SetSecretRequest)(nil), // 21: compass.v1.SetSecretRequest + (*SetSecretResponse)(nil), // 22: compass.v1.SetSecretResponse + (*ListSecretsRequest)(nil), // 23: compass.v1.ListSecretsRequest + (*ListSecretsResponse)(nil), // 24: compass.v1.ListSecretsResponse + (*SecretStatus)(nil), // 25: compass.v1.SecretStatus + (*DeleteSecretRequest)(nil), // 26: compass.v1.DeleteSecretRequest + (*DeleteSecretResponse)(nil), // 27: compass.v1.DeleteSecretResponse + (*ListServerSecretsRequest)(nil), // 28: compass.v1.ListServerSecretsRequest + (*ListServerSecretsResponse)(nil), // 29: compass.v1.ListServerSecretsResponse + (*ServerSecretStatus)(nil), // 30: compass.v1.ServerSecretStatus + (*GetRefLinkRulesRequest)(nil), // 31: compass.v1.GetRefLinkRulesRequest + (*RefLinkRule)(nil), // 32: compass.v1.RefLinkRule + (*GetRefLinkRulesResponse)(nil), // 33: compass.v1.GetRefLinkRulesResponse + (*GetServerInfoRequest)(nil), // 34: compass.v1.GetServerInfoRequest + (*GetServerInfoResponse)(nil), // 35: compass.v1.GetServerInfoResponse + (*EnrolledRunner)(nil), // 36: compass.v1.EnrolledRunner + (*WhoAmIRequest)(nil), // 37: compass.v1.WhoAmIRequest + (*WhoAmIResponse)(nil), // 38: compass.v1.WhoAmIResponse + (*GetTourStateRequest)(nil), // 39: compass.v1.GetTourStateRequest + (*GetTourStateResponse)(nil), // 40: compass.v1.GetTourStateResponse + (*ClaimTourStartRequest)(nil), // 41: compass.v1.ClaimTourStartRequest + (*ClaimTourStartResponse)(nil), // 42: compass.v1.ClaimTourStartResponse + (*SetTourStateRequest)(nil), // 43: compass.v1.SetTourStateRequest + (*SetTourStateResponse)(nil), // 44: compass.v1.SetTourStateResponse + (*SubscribeEventsRequest)(nil), // 45: compass.v1.SubscribeEventsRequest + (*SubscribeEventsResponse)(nil), // 46: compass.v1.SubscribeEventsResponse + (*ListBoardIssuesRequest)(nil), // 47: compass.v1.ListBoardIssuesRequest + (*ListBoardIssuesResponse)(nil), // 48: compass.v1.ListBoardIssuesResponse + (*SearchIssuesRequest)(nil), // 49: compass.v1.SearchIssuesRequest + (*SearchIssuesResponse)(nil), // 50: compass.v1.SearchIssuesResponse + (*ServerStatus)(nil), // 51: compass.v1.ServerStatus + (*ResyncRequired)(nil), // 52: compass.v1.ResyncRequired + (*AgentSessionStatus)(nil), // 53: compass.v1.AgentSessionStatus + (*AgentMessageChunk)(nil), // 54: compass.v1.AgentMessageChunk + (*AgentToolCall)(nil), // 55: compass.v1.AgentToolCall + (*AgentPlan)(nil), // 56: compass.v1.AgentPlan + (*AgentPlanEntry)(nil), // 57: compass.v1.AgentPlanEntry + (*SessionEvent)(nil), // 58: compass.v1.SessionEvent + (*SessionAssistantText)(nil), // 59: compass.v1.SessionAssistantText + (*SessionThinking)(nil), // 60: compass.v1.SessionThinking + (*SessionToolCall)(nil), // 61: compass.v1.SessionToolCall + (*SessionToolCallUpdate)(nil), // 62: compass.v1.SessionToolCallUpdate + (*SessionFileDiff)(nil), // 63: compass.v1.SessionFileDiff + (*SessionPlan)(nil), // 64: compass.v1.SessionPlan + (*SessionNotice)(nil), // 65: compass.v1.SessionNotice + (*SessionInjection)(nil), // 66: compass.v1.SessionInjection + (*SessionError)(nil), // 67: compass.v1.SessionError + (*SessionBatchPending)(nil), // 68: compass.v1.SessionBatchPending + (*SubscribeAgentSessionRequest)(nil), // 69: compass.v1.SubscribeAgentSessionRequest + (*AgentSessionFrame)(nil), // 70: compass.v1.AgentSessionFrame + (*ProvisionAgentWorkspaceRequest)(nil), // 71: compass.v1.ProvisionAgentWorkspaceRequest + (*ProvisionAgentWorkspaceResponse)(nil), // 72: compass.v1.ProvisionAgentWorkspaceResponse + (*RemoveAgentWorkspaceRequest)(nil), // 73: compass.v1.RemoveAgentWorkspaceRequest + (*RemoveAgentWorkspaceResponse)(nil), // 74: compass.v1.RemoveAgentWorkspaceResponse + (*StartAgentSessionRequest)(nil), // 75: compass.v1.StartAgentSessionRequest + (*StartAgentSessionResponse)(nil), // 76: compass.v1.StartAgentSessionResponse + (*SpawnAgentRequest)(nil), // 77: compass.v1.SpawnAgentRequest + (*SpawnAgentResponse)(nil), // 78: compass.v1.SpawnAgentResponse + (*StopAgentSessionRequest)(nil), // 79: compass.v1.StopAgentSessionRequest + (*StopAgentSessionResponse)(nil), // 80: compass.v1.StopAgentSessionResponse + (*SkipBatchWindowRequest)(nil), // 81: compass.v1.SkipBatchWindowRequest + (*SkipBatchWindowResponse)(nil), // 82: compass.v1.SkipBatchWindowResponse + (*ReloadAgentSessionRequest)(nil), // 83: compass.v1.ReloadAgentSessionRequest + (*ReloadAgentSessionResponse)(nil), // 84: compass.v1.ReloadAgentSessionResponse + (*GetAgentStatusRequest)(nil), // 85: compass.v1.GetAgentStatusRequest + (*GetAgentStatusResponse)(nil), // 86: compass.v1.GetAgentStatusResponse + (*IssueTokenRequest)(nil), // 87: compass.v1.IssueTokenRequest + (*IssueTokenResponse)(nil), // 88: compass.v1.IssueTokenResponse + (*RenameAccountRequest)(nil), // 89: compass.v1.RenameAccountRequest + (*RenameAccountResponse)(nil), // 90: compass.v1.RenameAccountResponse + (*RevokeTokenRequest)(nil), // 91: compass.v1.RevokeTokenRequest + (*RevokeTokenResponse)(nil), // 92: compass.v1.RevokeTokenResponse + (*PutAgentConfigRequest)(nil), // 93: compass.v1.PutAgentConfigRequest + (*PutAgentConfigResponse)(nil), // 94: compass.v1.PutAgentConfigResponse + (*GetAgentConfigInfoRequest)(nil), // 95: compass.v1.GetAgentConfigInfoRequest + (*GetAgentConfigInfoResponse)(nil), // 96: compass.v1.GetAgentConfigInfoResponse + (*DeleteAgentConfigRequest)(nil), // 97: compass.v1.DeleteAgentConfigRequest + (*DeleteAgentConfigResponse)(nil), // 98: compass.v1.DeleteAgentConfigResponse + (*ModelCandidate)(nil), // 99: compass.v1.ModelCandidate + (*ModelMetadata)(nil), // 100: compass.v1.ModelMetadata + (*ModelRegistryEntry)(nil), // 101: compass.v1.ModelRegistryEntry + (*ModelRegistry)(nil), // 102: compass.v1.ModelRegistry + (*PutModelRegistryRequest)(nil), // 103: compass.v1.PutModelRegistryRequest + (*PutModelRegistryResponse)(nil), // 104: compass.v1.PutModelRegistryResponse + (*GetModelRegistryRequest)(nil), // 105: compass.v1.GetModelRegistryRequest + (*GetModelRegistryResponse)(nil), // 106: compass.v1.GetModelRegistryResponse + (*DeleteModelRegistryRequest)(nil), // 107: compass.v1.DeleteModelRegistryRequest + (*DeleteModelRegistryResponse)(nil), // 108: compass.v1.DeleteModelRegistryResponse + (*AgentAttribution)(nil), // 109: compass.v1.AgentAttribution + (*ForgeRef)(nil), // 110: compass.v1.ForgeRef + (*ForgeMirrorSetting)(nil), // 111: compass.v1.ForgeMirrorSetting + (*ListForgeMirrorSettingsRequest)(nil), // 112: compass.v1.ListForgeMirrorSettingsRequest + (*ListForgeMirrorSettingsResponse)(nil), // 113: compass.v1.ListForgeMirrorSettingsResponse + (*SetForgeMirrorSettingRequest)(nil), // 114: compass.v1.SetForgeMirrorSettingRequest + (*SetForgeMirrorSettingResponse)(nil), // 115: compass.v1.SetForgeMirrorSettingResponse + (*Issue)(nil), // 116: compass.v1.Issue + (*PullRequest)(nil), // 117: compass.v1.PullRequest + (*ChecksSummary)(nil), // 118: compass.v1.ChecksSummary + (*Check)(nil), // 119: compass.v1.Check + (*ChangedStats)(nil), // 120: compass.v1.ChangedStats + (*TrackerRef)(nil), // 121: compass.v1.TrackerRef + (*Review)(nil), // 122: compass.v1.Review + (*ReviewThread)(nil), // 123: compass.v1.ReviewThread + (*Comment)(nil), // 124: compass.v1.Comment + (*MergeQueueEntry)(nil), // 125: compass.v1.MergeQueueEntry + nil, // 126: compass.v1.ModelRegistry.EntriesEntry + (*timestamppb.Timestamp)(nil), // 127: google.protobuf.Timestamp } var file_compass_v1_compass_proto_depIdxs = []int32{ 0, // 0: compass.v1.SetSecretRequest.delivery:type_name -> compass.v1.SecretDelivery 1, // 1: compass.v1.SetSecretRequest.kind:type_name -> compass.v1.SecretKind 2, // 2: compass.v1.SetSecretRequest.scope:type_name -> compass.v1.SecretScope - 24, // 3: compass.v1.ListSecretsResponse.secrets:type_name -> compass.v1.SecretStatus + 25, // 3: compass.v1.ListSecretsResponse.secrets:type_name -> compass.v1.SecretStatus 0, // 4: compass.v1.SecretStatus.delivery:type_name -> compass.v1.SecretDelivery 1, // 5: compass.v1.SecretStatus.kind:type_name -> compass.v1.SecretKind 2, // 6: compass.v1.DeleteSecretRequest.scope:type_name -> compass.v1.SecretScope - 29, // 7: compass.v1.ListServerSecretsResponse.server_secrets:type_name -> compass.v1.ServerSecretStatus - 31, // 8: compass.v1.GetRefLinkRulesResponse.rules:type_name -> compass.v1.RefLinkRule - 35, // 9: compass.v1.GetServerInfoResponse.enrolled_runners:type_name -> compass.v1.EnrolledRunner + 30, // 7: compass.v1.ListServerSecretsResponse.server_secrets:type_name -> compass.v1.ServerSecretStatus + 32, // 8: compass.v1.GetRefLinkRulesResponse.rules:type_name -> compass.v1.RefLinkRule + 36, // 9: compass.v1.GetServerInfoResponse.enrolled_runners:type_name -> compass.v1.EnrolledRunner 3, // 10: compass.v1.GetTourStateResponse.outcome:type_name -> compass.v1.TourOutcome 3, // 11: compass.v1.SetTourStateRequest.outcome:type_name -> compass.v1.TourOutcome - 50, // 12: compass.v1.SubscribeEventsResponse.server_status:type_name -> compass.v1.ServerStatus - 51, // 13: compass.v1.SubscribeEventsResponse.resync_required:type_name -> compass.v1.ResyncRequired - 52, // 14: compass.v1.SubscribeEventsResponse.agent_session_status:type_name -> compass.v1.AgentSessionStatus - 53, // 15: compass.v1.SubscribeEventsResponse.agent_message_chunk:type_name -> compass.v1.AgentMessageChunk - 54, // 16: compass.v1.SubscribeEventsResponse.agent_tool_call:type_name -> compass.v1.AgentToolCall - 55, // 17: compass.v1.SubscribeEventsResponse.agent_plan:type_name -> compass.v1.AgentPlan - 110, // 18: compass.v1.SubscribeEventsResponse.issue:type_name -> compass.v1.Issue - 110, // 19: compass.v1.ListBoardIssuesResponse.issues:type_name -> compass.v1.Issue - 110, // 20: compass.v1.SearchIssuesResponse.issues:type_name -> compass.v1.Issue + 51, // 12: compass.v1.SubscribeEventsResponse.server_status:type_name -> compass.v1.ServerStatus + 52, // 13: compass.v1.SubscribeEventsResponse.resync_required:type_name -> compass.v1.ResyncRequired + 53, // 14: compass.v1.SubscribeEventsResponse.agent_session_status:type_name -> compass.v1.AgentSessionStatus + 54, // 15: compass.v1.SubscribeEventsResponse.agent_message_chunk:type_name -> compass.v1.AgentMessageChunk + 55, // 16: compass.v1.SubscribeEventsResponse.agent_tool_call:type_name -> compass.v1.AgentToolCall + 56, // 17: compass.v1.SubscribeEventsResponse.agent_plan:type_name -> compass.v1.AgentPlan + 116, // 18: compass.v1.SubscribeEventsResponse.issue:type_name -> compass.v1.Issue + 116, // 19: compass.v1.ListBoardIssuesResponse.issues:type_name -> compass.v1.Issue + 116, // 20: compass.v1.SearchIssuesResponse.issues:type_name -> compass.v1.Issue 4, // 21: compass.v1.ServerStatus.state:type_name -> compass.v1.ServerState 7, // 22: compass.v1.AgentSessionStatus.state:type_name -> compass.v1.AgentSessionState 5, // 23: compass.v1.AgentSessionStatus.runtime_tier:type_name -> compass.v1.RuntimeTier 6, // 24: compass.v1.AgentSessionStatus.egress_posture:type_name -> compass.v1.EgressPosture 8, // 25: compass.v1.AgentToolCall.status:type_name -> compass.v1.AgentToolCallStatus - 56, // 26: compass.v1.AgentPlan.entries:type_name -> compass.v1.AgentPlanEntry + 57, // 26: compass.v1.AgentPlan.entries:type_name -> compass.v1.AgentPlanEntry 9, // 27: compass.v1.AgentPlanEntry.status:type_name -> compass.v1.AgentPlanEntryStatus - 58, // 28: compass.v1.SessionEvent.assistant_text:type_name -> compass.v1.SessionAssistantText - 59, // 29: compass.v1.SessionEvent.thinking:type_name -> compass.v1.SessionThinking - 60, // 30: compass.v1.SessionEvent.tool_call:type_name -> compass.v1.SessionToolCall - 61, // 31: compass.v1.SessionEvent.tool_call_update:type_name -> compass.v1.SessionToolCallUpdate - 63, // 32: compass.v1.SessionEvent.plan:type_name -> compass.v1.SessionPlan - 64, // 33: compass.v1.SessionEvent.notice:type_name -> compass.v1.SessionNotice - 65, // 34: compass.v1.SessionEvent.session_injection:type_name -> compass.v1.SessionInjection - 66, // 35: compass.v1.SessionEvent.session_error:type_name -> compass.v1.SessionError - 67, // 36: compass.v1.SessionEvent.batch_pending:type_name -> compass.v1.SessionBatchPending + 59, // 28: compass.v1.SessionEvent.assistant_text:type_name -> compass.v1.SessionAssistantText + 60, // 29: compass.v1.SessionEvent.thinking:type_name -> compass.v1.SessionThinking + 61, // 30: compass.v1.SessionEvent.tool_call:type_name -> compass.v1.SessionToolCall + 62, // 31: compass.v1.SessionEvent.tool_call_update:type_name -> compass.v1.SessionToolCallUpdate + 64, // 32: compass.v1.SessionEvent.plan:type_name -> compass.v1.SessionPlan + 65, // 33: compass.v1.SessionEvent.notice:type_name -> compass.v1.SessionNotice + 66, // 34: compass.v1.SessionEvent.session_injection:type_name -> compass.v1.SessionInjection + 67, // 35: compass.v1.SessionEvent.session_error:type_name -> compass.v1.SessionError + 68, // 36: compass.v1.SessionEvent.batch_pending:type_name -> compass.v1.SessionBatchPending 8, // 37: compass.v1.SessionToolCall.status:type_name -> compass.v1.AgentToolCallStatus 8, // 38: compass.v1.SessionToolCallUpdate.status:type_name -> compass.v1.AgentToolCallStatus - 62, // 39: compass.v1.SessionToolCallUpdate.diffs:type_name -> compass.v1.SessionFileDiff - 56, // 40: compass.v1.SessionPlan.entries:type_name -> compass.v1.AgentPlanEntry + 63, // 39: compass.v1.SessionToolCallUpdate.diffs:type_name -> compass.v1.SessionFileDiff + 57, // 40: compass.v1.SessionPlan.entries:type_name -> compass.v1.AgentPlanEntry 10, // 41: compass.v1.SessionInjection.op_kind:type_name -> compass.v1.SessionInjectionKind 11, // 42: compass.v1.SessionError.kind:type_name -> compass.v1.SessionErrorKind - 57, // 43: compass.v1.AgentSessionFrame.event:type_name -> compass.v1.SessionEvent + 58, // 43: compass.v1.AgentSessionFrame.event:type_name -> compass.v1.SessionEvent 7, // 44: compass.v1.AgentSessionFrame.state:type_name -> compass.v1.AgentSessionState - 52, // 45: compass.v1.GetAgentStatusResponse.statuses:type_name -> compass.v1.AgentSessionStatus - 98, // 46: compass.v1.ModelRegistryEntry.candidates:type_name -> compass.v1.ModelCandidate - 99, // 47: compass.v1.ModelRegistryEntry.metadata:type_name -> compass.v1.ModelMetadata - 120, // 48: compass.v1.ModelRegistry.entries:type_name -> compass.v1.ModelRegistry.EntriesEntry - 101, // 49: compass.v1.PutModelRegistryRequest.registry:type_name -> compass.v1.ModelRegistry - 101, // 50: compass.v1.GetModelRegistryResponse.registry:type_name -> compass.v1.ModelRegistry + 53, // 45: compass.v1.GetAgentStatusResponse.statuses:type_name -> compass.v1.AgentSessionStatus + 99, // 46: compass.v1.ModelRegistryEntry.candidates:type_name -> compass.v1.ModelCandidate + 100, // 47: compass.v1.ModelRegistryEntry.metadata:type_name -> compass.v1.ModelMetadata + 126, // 48: compass.v1.ModelRegistry.entries:type_name -> compass.v1.ModelRegistry.EntriesEntry + 102, // 49: compass.v1.PutModelRegistryRequest.registry:type_name -> compass.v1.ModelRegistry + 102, // 50: compass.v1.GetModelRegistryResponse.registry:type_name -> compass.v1.ModelRegistry 13, // 51: compass.v1.ForgeRef.provider:type_name -> compass.v1.ForgeProvider - 109, // 52: compass.v1.Issue.forge:type_name -> compass.v1.ForgeRef - 108, // 53: compass.v1.Issue.agent:type_name -> compass.v1.AgentAttribution - 121, // 54: compass.v1.Issue.updated_at:type_name -> google.protobuf.Timestamp - 12, // 55: compass.v1.Issue.state:type_name -> compass.v1.IssueState - 111, // 56: compass.v1.Issue.prs:type_name -> compass.v1.PullRequest - 115, // 57: compass.v1.Issue.tracker:type_name -> compass.v1.TrackerRef - 109, // 58: compass.v1.PullRequest.forge:type_name -> compass.v1.ForgeRef - 108, // 59: compass.v1.PullRequest.agent:type_name -> compass.v1.AgentAttribution - 114, // 60: compass.v1.PullRequest.changed:type_name -> compass.v1.ChangedStats - 112, // 61: compass.v1.PullRequest.checks:type_name -> compass.v1.ChecksSummary - 116, // 62: compass.v1.PullRequest.reviews:type_name -> compass.v1.Review - 117, // 63: compass.v1.PullRequest.threads:type_name -> compass.v1.ReviewThread - 119, // 64: compass.v1.PullRequest.merge_queue:type_name -> compass.v1.MergeQueueEntry - 113, // 65: compass.v1.ChecksSummary.checks:type_name -> compass.v1.Check - 118, // 66: compass.v1.ReviewThread.comments:type_name -> compass.v1.Comment - 121, // 67: compass.v1.MergeQueueEntry.changed_at:type_name -> google.protobuf.Timestamp - 100, // 68: compass.v1.ModelRegistry.EntriesEntry.value:type_name -> compass.v1.ModelRegistryEntry - 33, // 69: compass.v1.CompassService.GetServerInfo:input_type -> compass.v1.GetServerInfoRequest - 36, // 70: compass.v1.CompassService.WhoAmI:input_type -> compass.v1.WhoAmIRequest - 30, // 71: compass.v1.CompassService.GetRefLinkRules:input_type -> compass.v1.GetRefLinkRulesRequest - 38, // 72: compass.v1.CompassService.GetTourState:input_type -> compass.v1.GetTourStateRequest - 40, // 73: compass.v1.CompassService.ClaimTourStart:input_type -> compass.v1.ClaimTourStartRequest - 42, // 74: compass.v1.CompassService.SetTourState:input_type -> compass.v1.SetTourStateRequest - 44, // 75: compass.v1.CompassService.SubscribeEvents:input_type -> compass.v1.SubscribeEventsRequest - 46, // 76: compass.v1.CompassService.ListBoardIssues:input_type -> compass.v1.ListBoardIssuesRequest - 48, // 77: compass.v1.CompassService.SearchIssues:input_type -> compass.v1.SearchIssuesRequest - 70, // 78: compass.v1.CompassService.ProvisionAgentWorkspace:input_type -> compass.v1.ProvisionAgentWorkspaceRequest - 74, // 79: compass.v1.CompassService.StartAgentSession:input_type -> compass.v1.StartAgentSessionRequest - 76, // 80: compass.v1.CompassService.SpawnAgent:input_type -> compass.v1.SpawnAgentRequest - 78, // 81: compass.v1.CompassService.StopAgentSession:input_type -> compass.v1.StopAgentSessionRequest - 80, // 82: compass.v1.CompassService.SkipBatchWindow:input_type -> compass.v1.SkipBatchWindowRequest - 72, // 83: compass.v1.CompassService.RemoveAgentWorkspace:input_type -> compass.v1.RemoveAgentWorkspaceRequest - 82, // 84: compass.v1.CompassService.ReloadAgentSession:input_type -> compass.v1.ReloadAgentSessionRequest - 84, // 85: compass.v1.CompassService.GetAgentStatus:input_type -> compass.v1.GetAgentStatusRequest - 68, // 86: compass.v1.CompassService.SubscribeAgentSession:input_type -> compass.v1.SubscribeAgentSessionRequest - 86, // 87: compass.v1.CompassService.IssueToken:input_type -> compass.v1.IssueTokenRequest - 88, // 88: compass.v1.CompassService.RenameAccount:input_type -> compass.v1.RenameAccountRequest - 90, // 89: compass.v1.CompassService.RevokeToken:input_type -> compass.v1.RevokeTokenRequest - 92, // 90: compass.v1.CompassService.PutAgentConfig:input_type -> compass.v1.PutAgentConfigRequest - 94, // 91: compass.v1.CompassService.GetAgentConfigInfo:input_type -> compass.v1.GetAgentConfigInfoRequest - 96, // 92: compass.v1.CompassService.DeleteAgentConfig:input_type -> compass.v1.DeleteAgentConfigRequest - 102, // 93: compass.v1.CompassService.PutModelRegistry:input_type -> compass.v1.PutModelRegistryRequest - 104, // 94: compass.v1.CompassService.GetModelRegistry:input_type -> compass.v1.GetModelRegistryRequest - 106, // 95: compass.v1.CompassService.DeleteModelRegistry:input_type -> compass.v1.DeleteModelRegistryRequest - 20, // 96: compass.v1.SecretsService.SetSecret:input_type -> compass.v1.SetSecretRequest - 22, // 97: compass.v1.SecretsService.ListSecrets:input_type -> compass.v1.ListSecretsRequest - 25, // 98: compass.v1.SecretsService.DeleteSecret:input_type -> compass.v1.DeleteSecretRequest - 27, // 99: compass.v1.SecretsService.ListServerSecrets:input_type -> compass.v1.ListServerSecretsRequest - 14, // 100: compass.v1.AgentRepositoryService.GrantAgentRepository:input_type -> compass.v1.GrantAgentRepositoryRequest - 16, // 101: compass.v1.AgentRepositoryService.RevokeAgentRepository:input_type -> compass.v1.RevokeAgentRepositoryRequest - 18, // 102: compass.v1.AgentRepositoryService.ListAgentRepositories:input_type -> compass.v1.ListAgentRepositoriesRequest - 34, // 103: compass.v1.CompassService.GetServerInfo:output_type -> compass.v1.GetServerInfoResponse - 37, // 104: compass.v1.CompassService.WhoAmI:output_type -> compass.v1.WhoAmIResponse - 32, // 105: compass.v1.CompassService.GetRefLinkRules:output_type -> compass.v1.GetRefLinkRulesResponse - 39, // 106: compass.v1.CompassService.GetTourState:output_type -> compass.v1.GetTourStateResponse - 41, // 107: compass.v1.CompassService.ClaimTourStart:output_type -> compass.v1.ClaimTourStartResponse - 43, // 108: compass.v1.CompassService.SetTourState:output_type -> compass.v1.SetTourStateResponse - 45, // 109: compass.v1.CompassService.SubscribeEvents:output_type -> compass.v1.SubscribeEventsResponse - 47, // 110: compass.v1.CompassService.ListBoardIssues:output_type -> compass.v1.ListBoardIssuesResponse - 49, // 111: compass.v1.CompassService.SearchIssues:output_type -> compass.v1.SearchIssuesResponse - 71, // 112: compass.v1.CompassService.ProvisionAgentWorkspace:output_type -> compass.v1.ProvisionAgentWorkspaceResponse - 75, // 113: compass.v1.CompassService.StartAgentSession:output_type -> compass.v1.StartAgentSessionResponse - 77, // 114: compass.v1.CompassService.SpawnAgent:output_type -> compass.v1.SpawnAgentResponse - 79, // 115: compass.v1.CompassService.StopAgentSession:output_type -> compass.v1.StopAgentSessionResponse - 81, // 116: compass.v1.CompassService.SkipBatchWindow:output_type -> compass.v1.SkipBatchWindowResponse - 73, // 117: compass.v1.CompassService.RemoveAgentWorkspace:output_type -> compass.v1.RemoveAgentWorkspaceResponse - 83, // 118: compass.v1.CompassService.ReloadAgentSession:output_type -> compass.v1.ReloadAgentSessionResponse - 85, // 119: compass.v1.CompassService.GetAgentStatus:output_type -> compass.v1.GetAgentStatusResponse - 69, // 120: compass.v1.CompassService.SubscribeAgentSession:output_type -> compass.v1.AgentSessionFrame - 87, // 121: compass.v1.CompassService.IssueToken:output_type -> compass.v1.IssueTokenResponse - 89, // 122: compass.v1.CompassService.RenameAccount:output_type -> compass.v1.RenameAccountResponse - 91, // 123: compass.v1.CompassService.RevokeToken:output_type -> compass.v1.RevokeTokenResponse - 93, // 124: compass.v1.CompassService.PutAgentConfig:output_type -> compass.v1.PutAgentConfigResponse - 95, // 125: compass.v1.CompassService.GetAgentConfigInfo:output_type -> compass.v1.GetAgentConfigInfoResponse - 97, // 126: compass.v1.CompassService.DeleteAgentConfig:output_type -> compass.v1.DeleteAgentConfigResponse - 103, // 127: compass.v1.CompassService.PutModelRegistry:output_type -> compass.v1.PutModelRegistryResponse - 105, // 128: compass.v1.CompassService.GetModelRegistry:output_type -> compass.v1.GetModelRegistryResponse - 107, // 129: compass.v1.CompassService.DeleteModelRegistry:output_type -> compass.v1.DeleteModelRegistryResponse - 21, // 130: compass.v1.SecretsService.SetSecret:output_type -> compass.v1.SetSecretResponse - 23, // 131: compass.v1.SecretsService.ListSecrets:output_type -> compass.v1.ListSecretsResponse - 26, // 132: compass.v1.SecretsService.DeleteSecret:output_type -> compass.v1.DeleteSecretResponse - 28, // 133: compass.v1.SecretsService.ListServerSecrets:output_type -> compass.v1.ListServerSecretsResponse - 15, // 134: compass.v1.AgentRepositoryService.GrantAgentRepository:output_type -> compass.v1.GrantAgentRepositoryResponse - 17, // 135: compass.v1.AgentRepositoryService.RevokeAgentRepository:output_type -> compass.v1.RevokeAgentRepositoryResponse - 19, // 136: compass.v1.AgentRepositoryService.ListAgentRepositories:output_type -> compass.v1.ListAgentRepositoriesResponse - 103, // [103:137] is the sub-list for method output_type - 69, // [69:103] is the sub-list for method input_type - 69, // [69:69] is the sub-list for extension type_name - 69, // [69:69] is the sub-list for extension extendee - 0, // [0:69] is the sub-list for field type_name + 110, // 52: compass.v1.ForgeMirrorSetting.forge:type_name -> compass.v1.ForgeRef + 14, // 53: compass.v1.ForgeMirrorSetting.mode:type_name -> compass.v1.ForgeMirrorMode + 111, // 54: compass.v1.ListForgeMirrorSettingsResponse.settings:type_name -> compass.v1.ForgeMirrorSetting + 111, // 55: compass.v1.SetForgeMirrorSettingRequest.setting:type_name -> compass.v1.ForgeMirrorSetting + 111, // 56: compass.v1.SetForgeMirrorSettingResponse.setting:type_name -> compass.v1.ForgeMirrorSetting + 110, // 57: compass.v1.Issue.forge:type_name -> compass.v1.ForgeRef + 109, // 58: compass.v1.Issue.agent:type_name -> compass.v1.AgentAttribution + 127, // 59: compass.v1.Issue.updated_at:type_name -> google.protobuf.Timestamp + 12, // 60: compass.v1.Issue.state:type_name -> compass.v1.IssueState + 117, // 61: compass.v1.Issue.prs:type_name -> compass.v1.PullRequest + 121, // 62: compass.v1.Issue.tracker:type_name -> compass.v1.TrackerRef + 110, // 63: compass.v1.PullRequest.forge:type_name -> compass.v1.ForgeRef + 109, // 64: compass.v1.PullRequest.agent:type_name -> compass.v1.AgentAttribution + 120, // 65: compass.v1.PullRequest.changed:type_name -> compass.v1.ChangedStats + 118, // 66: compass.v1.PullRequest.checks:type_name -> compass.v1.ChecksSummary + 122, // 67: compass.v1.PullRequest.reviews:type_name -> compass.v1.Review + 123, // 68: compass.v1.PullRequest.threads:type_name -> compass.v1.ReviewThread + 125, // 69: compass.v1.PullRequest.merge_queue:type_name -> compass.v1.MergeQueueEntry + 119, // 70: compass.v1.ChecksSummary.checks:type_name -> compass.v1.Check + 124, // 71: compass.v1.ReviewThread.comments:type_name -> compass.v1.Comment + 127, // 72: compass.v1.MergeQueueEntry.changed_at:type_name -> google.protobuf.Timestamp + 101, // 73: compass.v1.ModelRegistry.EntriesEntry.value:type_name -> compass.v1.ModelRegistryEntry + 34, // 74: compass.v1.CompassService.GetServerInfo:input_type -> compass.v1.GetServerInfoRequest + 37, // 75: compass.v1.CompassService.WhoAmI:input_type -> compass.v1.WhoAmIRequest + 31, // 76: compass.v1.CompassService.GetRefLinkRules:input_type -> compass.v1.GetRefLinkRulesRequest + 39, // 77: compass.v1.CompassService.GetTourState:input_type -> compass.v1.GetTourStateRequest + 41, // 78: compass.v1.CompassService.ClaimTourStart:input_type -> compass.v1.ClaimTourStartRequest + 43, // 79: compass.v1.CompassService.SetTourState:input_type -> compass.v1.SetTourStateRequest + 45, // 80: compass.v1.CompassService.SubscribeEvents:input_type -> compass.v1.SubscribeEventsRequest + 47, // 81: compass.v1.CompassService.ListBoardIssues:input_type -> compass.v1.ListBoardIssuesRequest + 49, // 82: compass.v1.CompassService.SearchIssues:input_type -> compass.v1.SearchIssuesRequest + 71, // 83: compass.v1.CompassService.ProvisionAgentWorkspace:input_type -> compass.v1.ProvisionAgentWorkspaceRequest + 75, // 84: compass.v1.CompassService.StartAgentSession:input_type -> compass.v1.StartAgentSessionRequest + 77, // 85: compass.v1.CompassService.SpawnAgent:input_type -> compass.v1.SpawnAgentRequest + 79, // 86: compass.v1.CompassService.StopAgentSession:input_type -> compass.v1.StopAgentSessionRequest + 81, // 87: compass.v1.CompassService.SkipBatchWindow:input_type -> compass.v1.SkipBatchWindowRequest + 73, // 88: compass.v1.CompassService.RemoveAgentWorkspace:input_type -> compass.v1.RemoveAgentWorkspaceRequest + 83, // 89: compass.v1.CompassService.ReloadAgentSession:input_type -> compass.v1.ReloadAgentSessionRequest + 85, // 90: compass.v1.CompassService.GetAgentStatus:input_type -> compass.v1.GetAgentStatusRequest + 69, // 91: compass.v1.CompassService.SubscribeAgentSession:input_type -> compass.v1.SubscribeAgentSessionRequest + 87, // 92: compass.v1.CompassService.IssueToken:input_type -> compass.v1.IssueTokenRequest + 89, // 93: compass.v1.CompassService.RenameAccount:input_type -> compass.v1.RenameAccountRequest + 91, // 94: compass.v1.CompassService.RevokeToken:input_type -> compass.v1.RevokeTokenRequest + 93, // 95: compass.v1.CompassService.PutAgentConfig:input_type -> compass.v1.PutAgentConfigRequest + 95, // 96: compass.v1.CompassService.GetAgentConfigInfo:input_type -> compass.v1.GetAgentConfigInfoRequest + 97, // 97: compass.v1.CompassService.DeleteAgentConfig:input_type -> compass.v1.DeleteAgentConfigRequest + 103, // 98: compass.v1.CompassService.PutModelRegistry:input_type -> compass.v1.PutModelRegistryRequest + 105, // 99: compass.v1.CompassService.GetModelRegistry:input_type -> compass.v1.GetModelRegistryRequest + 107, // 100: compass.v1.CompassService.DeleteModelRegistry:input_type -> compass.v1.DeleteModelRegistryRequest + 112, // 101: compass.v1.CompassService.ListForgeMirrorSettings:input_type -> compass.v1.ListForgeMirrorSettingsRequest + 114, // 102: compass.v1.CompassService.SetForgeMirrorSetting:input_type -> compass.v1.SetForgeMirrorSettingRequest + 21, // 103: compass.v1.SecretsService.SetSecret:input_type -> compass.v1.SetSecretRequest + 23, // 104: compass.v1.SecretsService.ListSecrets:input_type -> compass.v1.ListSecretsRequest + 26, // 105: compass.v1.SecretsService.DeleteSecret:input_type -> compass.v1.DeleteSecretRequest + 28, // 106: compass.v1.SecretsService.ListServerSecrets:input_type -> compass.v1.ListServerSecretsRequest + 15, // 107: compass.v1.AgentRepositoryService.GrantAgentRepository:input_type -> compass.v1.GrantAgentRepositoryRequest + 17, // 108: compass.v1.AgentRepositoryService.RevokeAgentRepository:input_type -> compass.v1.RevokeAgentRepositoryRequest + 19, // 109: compass.v1.AgentRepositoryService.ListAgentRepositories:input_type -> compass.v1.ListAgentRepositoriesRequest + 35, // 110: compass.v1.CompassService.GetServerInfo:output_type -> compass.v1.GetServerInfoResponse + 38, // 111: compass.v1.CompassService.WhoAmI:output_type -> compass.v1.WhoAmIResponse + 33, // 112: compass.v1.CompassService.GetRefLinkRules:output_type -> compass.v1.GetRefLinkRulesResponse + 40, // 113: compass.v1.CompassService.GetTourState:output_type -> compass.v1.GetTourStateResponse + 42, // 114: compass.v1.CompassService.ClaimTourStart:output_type -> compass.v1.ClaimTourStartResponse + 44, // 115: compass.v1.CompassService.SetTourState:output_type -> compass.v1.SetTourStateResponse + 46, // 116: compass.v1.CompassService.SubscribeEvents:output_type -> compass.v1.SubscribeEventsResponse + 48, // 117: compass.v1.CompassService.ListBoardIssues:output_type -> compass.v1.ListBoardIssuesResponse + 50, // 118: compass.v1.CompassService.SearchIssues:output_type -> compass.v1.SearchIssuesResponse + 72, // 119: compass.v1.CompassService.ProvisionAgentWorkspace:output_type -> compass.v1.ProvisionAgentWorkspaceResponse + 76, // 120: compass.v1.CompassService.StartAgentSession:output_type -> compass.v1.StartAgentSessionResponse + 78, // 121: compass.v1.CompassService.SpawnAgent:output_type -> compass.v1.SpawnAgentResponse + 80, // 122: compass.v1.CompassService.StopAgentSession:output_type -> compass.v1.StopAgentSessionResponse + 82, // 123: compass.v1.CompassService.SkipBatchWindow:output_type -> compass.v1.SkipBatchWindowResponse + 74, // 124: compass.v1.CompassService.RemoveAgentWorkspace:output_type -> compass.v1.RemoveAgentWorkspaceResponse + 84, // 125: compass.v1.CompassService.ReloadAgentSession:output_type -> compass.v1.ReloadAgentSessionResponse + 86, // 126: compass.v1.CompassService.GetAgentStatus:output_type -> compass.v1.GetAgentStatusResponse + 70, // 127: compass.v1.CompassService.SubscribeAgentSession:output_type -> compass.v1.AgentSessionFrame + 88, // 128: compass.v1.CompassService.IssueToken:output_type -> compass.v1.IssueTokenResponse + 90, // 129: compass.v1.CompassService.RenameAccount:output_type -> compass.v1.RenameAccountResponse + 92, // 130: compass.v1.CompassService.RevokeToken:output_type -> compass.v1.RevokeTokenResponse + 94, // 131: compass.v1.CompassService.PutAgentConfig:output_type -> compass.v1.PutAgentConfigResponse + 96, // 132: compass.v1.CompassService.GetAgentConfigInfo:output_type -> compass.v1.GetAgentConfigInfoResponse + 98, // 133: compass.v1.CompassService.DeleteAgentConfig:output_type -> compass.v1.DeleteAgentConfigResponse + 104, // 134: compass.v1.CompassService.PutModelRegistry:output_type -> compass.v1.PutModelRegistryResponse + 106, // 135: compass.v1.CompassService.GetModelRegistry:output_type -> compass.v1.GetModelRegistryResponse + 108, // 136: compass.v1.CompassService.DeleteModelRegistry:output_type -> compass.v1.DeleteModelRegistryResponse + 113, // 137: compass.v1.CompassService.ListForgeMirrorSettings:output_type -> compass.v1.ListForgeMirrorSettingsResponse + 115, // 138: compass.v1.CompassService.SetForgeMirrorSetting:output_type -> compass.v1.SetForgeMirrorSettingResponse + 22, // 139: compass.v1.SecretsService.SetSecret:output_type -> compass.v1.SetSecretResponse + 24, // 140: compass.v1.SecretsService.ListSecrets:output_type -> compass.v1.ListSecretsResponse + 27, // 141: compass.v1.SecretsService.DeleteSecret:output_type -> compass.v1.DeleteSecretResponse + 29, // 142: compass.v1.SecretsService.ListServerSecrets:output_type -> compass.v1.ListServerSecretsResponse + 16, // 143: compass.v1.AgentRepositoryService.GrantAgentRepository:output_type -> compass.v1.GrantAgentRepositoryResponse + 18, // 144: compass.v1.AgentRepositoryService.RevokeAgentRepository:output_type -> compass.v1.RevokeAgentRepositoryResponse + 20, // 145: compass.v1.AgentRepositoryService.ListAgentRepositories:output_type -> compass.v1.ListAgentRepositoriesResponse + 110, // [110:146] is the sub-list for method output_type + 74, // [74:110] is the sub-list for method input_type + 74, // [74:74] is the sub-list for extension type_name + 74, // [74:74] is the sub-list for extension extendee + 0, // [0:74] is the sub-list for field type_name } func init() { file_compass_v1_compass_proto_init() } @@ -8004,8 +8343,8 @@ func file_compass_v1_compass_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_compass_v1_compass_proto_rawDesc), len(file_compass_v1_compass_proto_rawDesc)), - NumEnums: 14, - NumMessages: 107, + NumEnums: 15, + NumMessages: 112, NumExtensions: 0, NumServices: 3, }, diff --git a/go/gen/compass/v1/compassv1connect/comms.connect.go b/go/gen/compass/v1/compassv1connect/comms.connect.go index ce9d4aeb7..c84b3edaa 100644 --- a/go/gen/compass/v1/compassv1connect/comms.connect.go +++ b/go/gen/compass/v1/compassv1connect/comms.connect.go @@ -98,6 +98,9 @@ const ( // CommsServiceRespondToAskProcedure is the fully-qualified name of the CommsService's RespondToAsk // RPC. CommsServiceRespondToAskProcedure = "/compass.v1.CommsService/RespondToAsk" + // CommsServiceMirrorToForgeProcedure is the fully-qualified name of the CommsService's + // MirrorToForge RPC. + CommsServiceMirrorToForgeProcedure = "/compass.v1.CommsService/MirrorToForge" // CommsServiceSearchMessagesProcedure is the fully-qualified name of the CommsService's // SearchMessages RPC. CommsServiceSearchMessagesProcedure = "/compass.v1.CommsService/SearchMessages" @@ -184,6 +187,9 @@ type CommsServiceClient interface { // Answer a pending structured ask (D5). Authorized: the caller must be a // member of the channel the ask belongs to. RespondToAsk(context.Context, *connect.Request[v1.RespondToAskRequest]) (*connect.Response[v1.RespondToAskResponse], error) + // Mirror a visible ask, topic, or message set to an allowed forge ref on the + // calling user's behalf. + MirrorToForge(context.Context, *connect.Request[v1.MirrorToForgeRequest]) (*connect.Response[v1.MirrorToForgeResponse], error) // Full-text search across the messages the caller may see — the audit/search // property served from the store of record (D1, D12). Results are scoped to // the caller's visible channels regardless of the scope field. @@ -332,6 +338,12 @@ func NewCommsServiceClient(httpClient connect.HTTPClient, baseURL string, opts . connect.WithSchema(commsServiceMethods.ByName("RespondToAsk")), connect.WithClientOptions(opts...), ), + mirrorToForge: connect.NewClient[v1.MirrorToForgeRequest, v1.MirrorToForgeResponse]( + httpClient, + baseURL+CommsServiceMirrorToForgeProcedure, + connect.WithSchema(commsServiceMethods.ByName("MirrorToForge")), + connect.WithClientOptions(opts...), + ), searchMessages: connect.NewClient[v1.SearchMessagesRequest, v1.SearchMessagesResponse]( httpClient, baseURL+CommsServiceSearchMessagesProcedure, @@ -402,6 +414,7 @@ type commsServiceClient struct { listTopics *connect.Client[v1.ListTopicsRequest, v1.ListTopicsResponse] updateTopic *connect.Client[v1.UpdateTopicRequest, v1.UpdateTopicResponse] respondToAsk *connect.Client[v1.RespondToAskRequest, v1.RespondToAskResponse] + mirrorToForge *connect.Client[v1.MirrorToForgeRequest, v1.MirrorToForgeResponse] searchMessages *connect.Client[v1.SearchMessagesRequest, v1.SearchMessagesResponse] setChannelPolicy *connect.Client[v1.SetChannelPolicyRequest, v1.SetChannelPolicyResponse] getRoster *connect.Client[v1.GetRosterRequest, v1.GetRosterResponse] @@ -497,6 +510,11 @@ func (c *commsServiceClient) RespondToAsk(ctx context.Context, req *connect.Requ return c.respondToAsk.CallUnary(ctx, req) } +// MirrorToForge calls compass.v1.CommsService.MirrorToForge. +func (c *commsServiceClient) MirrorToForge(ctx context.Context, req *connect.Request[v1.MirrorToForgeRequest]) (*connect.Response[v1.MirrorToForgeResponse], error) { + return c.mirrorToForge.CallUnary(ctx, req) +} + // SearchMessages calls compass.v1.CommsService.SearchMessages. func (c *commsServiceClient) SearchMessages(ctx context.Context, req *connect.Request[v1.SearchMessagesRequest]) (*connect.Response[v1.SearchMessagesResponse], error) { return c.searchMessages.CallUnary(ctx, req) @@ -600,6 +618,9 @@ type CommsServiceHandler interface { // Answer a pending structured ask (D5). Authorized: the caller must be a // member of the channel the ask belongs to. RespondToAsk(context.Context, *connect.Request[v1.RespondToAskRequest]) (*connect.Response[v1.RespondToAskResponse], error) + // Mirror a visible ask, topic, or message set to an allowed forge ref on the + // calling user's behalf. + MirrorToForge(context.Context, *connect.Request[v1.MirrorToForgeRequest]) (*connect.Response[v1.MirrorToForgeResponse], error) // Full-text search across the messages the caller may see — the audit/search // property served from the store of record (D1, D12). Results are scoped to // the caller's visible channels regardless of the scope field. @@ -744,6 +765,12 @@ func NewCommsServiceHandler(svc CommsServiceHandler, opts ...connect.HandlerOpti connect.WithSchema(commsServiceMethods.ByName("RespondToAsk")), connect.WithHandlerOptions(opts...), ) + commsServiceMirrorToForgeHandler := connect.NewUnaryHandler( + CommsServiceMirrorToForgeProcedure, + svc.MirrorToForge, + connect.WithSchema(commsServiceMethods.ByName("MirrorToForge")), + connect.WithHandlerOptions(opts...), + ) commsServiceSearchMessagesHandler := connect.NewUnaryHandler( CommsServiceSearchMessagesProcedure, svc.SearchMessages, @@ -828,6 +855,8 @@ func NewCommsServiceHandler(svc CommsServiceHandler, opts ...connect.HandlerOpti commsServiceUpdateTopicHandler.ServeHTTP(w, r) case CommsServiceRespondToAskProcedure: commsServiceRespondToAskHandler.ServeHTTP(w, r) + case CommsServiceMirrorToForgeProcedure: + commsServiceMirrorToForgeHandler.ServeHTTP(w, r) case CommsServiceSearchMessagesProcedure: commsServiceSearchMessagesHandler.ServeHTTP(w, r) case CommsServiceSetChannelPolicyProcedure: @@ -921,6 +950,10 @@ func (UnimplementedCommsServiceHandler) RespondToAsk(context.Context, *connect.R return nil, connect.NewError(connect.CodeUnimplemented, errors.New("compass.v1.CommsService.RespondToAsk is not implemented")) } +func (UnimplementedCommsServiceHandler) MirrorToForge(context.Context, *connect.Request[v1.MirrorToForgeRequest]) (*connect.Response[v1.MirrorToForgeResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("compass.v1.CommsService.MirrorToForge is not implemented")) +} + func (UnimplementedCommsServiceHandler) SearchMessages(context.Context, *connect.Request[v1.SearchMessagesRequest]) (*connect.Response[v1.SearchMessagesResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("compass.v1.CommsService.SearchMessages is not implemented")) } diff --git a/go/gen/compass/v1/compassv1connect/compass.connect.go b/go/gen/compass/v1/compassv1connect/compass.connect.go index 7ede139b0..311495fe1 100644 --- a/go/gen/compass/v1/compassv1connect/compass.connect.go +++ b/go/gen/compass/v1/compassv1connect/compass.connect.go @@ -125,6 +125,12 @@ const ( // CompassServiceDeleteModelRegistryProcedure is the fully-qualified name of the CompassService's // DeleteModelRegistry RPC. CompassServiceDeleteModelRegistryProcedure = "/compass.v1.CompassService/DeleteModelRegistry" + // CompassServiceListForgeMirrorSettingsProcedure is the fully-qualified name of the + // CompassService's ListForgeMirrorSettings RPC. + CompassServiceListForgeMirrorSettingsProcedure = "/compass.v1.CompassService/ListForgeMirrorSettings" + // CompassServiceSetForgeMirrorSettingProcedure is the fully-qualified name of the CompassService's + // SetForgeMirrorSetting RPC. + CompassServiceSetForgeMirrorSettingProcedure = "/compass.v1.CompassService/SetForgeMirrorSetting" // SecretsServiceSetSecretProcedure is the fully-qualified name of the SecretsService's SetSecret // RPC. SecretsServiceSetSecretProcedure = "/compass.v1.SecretsService/SetSecret" @@ -311,6 +317,12 @@ type CompassServiceClient interface { // like PutModelRegistry. Fails closed if the registry being cleared holds any // stable name still referenced by a published profile's models.* map. DeleteModelRegistry(context.Context, *connect.Request[v1.DeleteModelRegistryRequest]) (*connect.Response[v1.DeleteModelRegistryResponse], error) + // List the tenant's enabled forge-repository mirror settings. Any authenticated + // account may read them; can_edit reports whether the caller may change them. + ListForgeMirrorSettings(context.Context, *connect.Request[v1.ListForgeMirrorSettingsRequest]) (*connect.Response[v1.ListForgeMirrorSettingsResponse], error) + // Set one tenant's mirror mode for an enabled forge repository. Requires a + // tenant admin; OFF removes the setting row. + SetForgeMirrorSetting(context.Context, *connect.Request[v1.SetForgeMirrorSettingRequest]) (*connect.Response[v1.SetForgeMirrorSettingResponse], error) } // NewCompassServiceClient constructs a client for the compass.v1.CompassService service. By @@ -486,6 +498,18 @@ func NewCompassServiceClient(httpClient connect.HTTPClient, baseURL string, opts connect.WithSchema(compassServiceMethods.ByName("DeleteModelRegistry")), connect.WithClientOptions(opts...), ), + listForgeMirrorSettings: connect.NewClient[v1.ListForgeMirrorSettingsRequest, v1.ListForgeMirrorSettingsResponse]( + httpClient, + baseURL+CompassServiceListForgeMirrorSettingsProcedure, + connect.WithSchema(compassServiceMethods.ByName("ListForgeMirrorSettings")), + connect.WithClientOptions(opts...), + ), + setForgeMirrorSetting: connect.NewClient[v1.SetForgeMirrorSettingRequest, v1.SetForgeMirrorSettingResponse]( + httpClient, + baseURL+CompassServiceSetForgeMirrorSettingProcedure, + connect.WithSchema(compassServiceMethods.ByName("SetForgeMirrorSetting")), + connect.WithClientOptions(opts...), + ), } } @@ -518,6 +542,8 @@ type compassServiceClient struct { putModelRegistry *connect.Client[v1.PutModelRegistryRequest, v1.PutModelRegistryResponse] getModelRegistry *connect.Client[v1.GetModelRegistryRequest, v1.GetModelRegistryResponse] deleteModelRegistry *connect.Client[v1.DeleteModelRegistryRequest, v1.DeleteModelRegistryResponse] + listForgeMirrorSettings *connect.Client[v1.ListForgeMirrorSettingsRequest, v1.ListForgeMirrorSettingsResponse] + setForgeMirrorSetting *connect.Client[v1.SetForgeMirrorSettingRequest, v1.SetForgeMirrorSettingResponse] } // GetServerInfo calls compass.v1.CompassService.GetServerInfo. @@ -655,6 +681,16 @@ func (c *compassServiceClient) DeleteModelRegistry(ctx context.Context, req *con return c.deleteModelRegistry.CallUnary(ctx, req) } +// ListForgeMirrorSettings calls compass.v1.CompassService.ListForgeMirrorSettings. +func (c *compassServiceClient) ListForgeMirrorSettings(ctx context.Context, req *connect.Request[v1.ListForgeMirrorSettingsRequest]) (*connect.Response[v1.ListForgeMirrorSettingsResponse], error) { + return c.listForgeMirrorSettings.CallUnary(ctx, req) +} + +// SetForgeMirrorSetting calls compass.v1.CompassService.SetForgeMirrorSetting. +func (c *compassServiceClient) SetForgeMirrorSetting(ctx context.Context, req *connect.Request[v1.SetForgeMirrorSettingRequest]) (*connect.Response[v1.SetForgeMirrorSettingResponse], error) { + return c.setForgeMirrorSetting.CallUnary(ctx, req) +} + // CompassServiceHandler is an implementation of the compass.v1.CompassService service. type CompassServiceHandler interface { // Liveness probe plus the server's build + contract version. The first @@ -818,6 +854,12 @@ type CompassServiceHandler interface { // like PutModelRegistry. Fails closed if the registry being cleared holds any // stable name still referenced by a published profile's models.* map. DeleteModelRegistry(context.Context, *connect.Request[v1.DeleteModelRegistryRequest]) (*connect.Response[v1.DeleteModelRegistryResponse], error) + // List the tenant's enabled forge-repository mirror settings. Any authenticated + // account may read them; can_edit reports whether the caller may change them. + ListForgeMirrorSettings(context.Context, *connect.Request[v1.ListForgeMirrorSettingsRequest]) (*connect.Response[v1.ListForgeMirrorSettingsResponse], error) + // Set one tenant's mirror mode for an enabled forge repository. Requires a + // tenant admin; OFF removes the setting row. + SetForgeMirrorSetting(context.Context, *connect.Request[v1.SetForgeMirrorSettingRequest]) (*connect.Response[v1.SetForgeMirrorSettingResponse], error) } // NewCompassServiceHandler builds an HTTP handler from the service implementation. It returns the @@ -989,6 +1031,18 @@ func NewCompassServiceHandler(svc CompassServiceHandler, opts ...connect.Handler connect.WithSchema(compassServiceMethods.ByName("DeleteModelRegistry")), connect.WithHandlerOptions(opts...), ) + compassServiceListForgeMirrorSettingsHandler := connect.NewUnaryHandler( + CompassServiceListForgeMirrorSettingsProcedure, + svc.ListForgeMirrorSettings, + connect.WithSchema(compassServiceMethods.ByName("ListForgeMirrorSettings")), + connect.WithHandlerOptions(opts...), + ) + compassServiceSetForgeMirrorSettingHandler := connect.NewUnaryHandler( + CompassServiceSetForgeMirrorSettingProcedure, + svc.SetForgeMirrorSetting, + connect.WithSchema(compassServiceMethods.ByName("SetForgeMirrorSetting")), + connect.WithHandlerOptions(opts...), + ) return "/compass.v1.CompassService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case CompassServiceGetServerInfoProcedure: @@ -1045,6 +1099,10 @@ func NewCompassServiceHandler(svc CompassServiceHandler, opts ...connect.Handler compassServiceGetModelRegistryHandler.ServeHTTP(w, r) case CompassServiceDeleteModelRegistryProcedure: compassServiceDeleteModelRegistryHandler.ServeHTTP(w, r) + case CompassServiceListForgeMirrorSettingsProcedure: + compassServiceListForgeMirrorSettingsHandler.ServeHTTP(w, r) + case CompassServiceSetForgeMirrorSettingProcedure: + compassServiceSetForgeMirrorSettingHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -1162,6 +1220,14 @@ func (UnimplementedCompassServiceHandler) DeleteModelRegistry(context.Context, * return nil, connect.NewError(connect.CodeUnimplemented, errors.New("compass.v1.CompassService.DeleteModelRegistry is not implemented")) } +func (UnimplementedCompassServiceHandler) ListForgeMirrorSettings(context.Context, *connect.Request[v1.ListForgeMirrorSettingsRequest]) (*connect.Response[v1.ListForgeMirrorSettingsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("compass.v1.CompassService.ListForgeMirrorSettings is not implemented")) +} + +func (UnimplementedCompassServiceHandler) SetForgeMirrorSetting(context.Context, *connect.Request[v1.SetForgeMirrorSettingRequest]) (*connect.Response[v1.SetForgeMirrorSettingResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("compass.v1.CompassService.SetForgeMirrorSetting is not implemented")) +} + // SecretsServiceClient is a client for the compass.v1.SecretsService service. type SecretsServiceClient interface { // Declare a secret's registry row (name/delivery/kind/routing) and write its diff --git a/go/internal/auth/admin_gate.go b/go/internal/auth/admin_gate.go index 8e8728524..e6598203c 100644 --- a/go/internal/auth/admin_gate.go +++ b/go/internal/auth/admin_gate.go @@ -83,7 +83,11 @@ func classifyProcedure(procedure string) (privilege, bool) { compassv1connect.CompassServiceGetAgentConfigInfoProcedure, compassv1connect.CompassServiceSubscribeAgentSessionProcedure, compassv1connect.CompassServiceGetRefLinkRulesProcedure, - compassv1connect.CompassServiceGetModelRegistryProcedure: + compassv1connect.CompassServiceGetModelRegistryProcedure, + // Forge mirror settings: Set checks the tenant's UserRoleAdmin in the + // handler, which differs from this gate's single bootstrap admin. + compassv1connect.CompassServiceListForgeMirrorSettingsProcedure, + compassv1connect.CompassServiceSetForgeMirrorSettingProcedure: return authenticatedOpen{}, true // Every CommsService method: open to any authenticated account (the service @@ -102,6 +106,7 @@ func classifyProcedure(procedure string) (privilege, bool) { compassv1connect.CommsServiceListMessagesProcedure, compassv1connect.CommsServicePostMessageProcedure, compassv1connect.CommsServiceRespondToAskProcedure, + compassv1connect.CommsServiceMirrorToForgeProcedure, compassv1connect.CommsServiceSearchMessagesProcedure, compassv1connect.CommsServiceListTopicsProcedure, compassv1connect.CommsServiceUpdateTopicProcedure, diff --git a/go/internal/comms/agent_update_helper_pgtest_test.go b/go/internal/comms/agent_update_helper_pgtest_test.go index 77e41c07e..47eb7e7b4 100644 --- a/go/internal/comms/agent_update_helper_pgtest_test.go +++ b/go/internal/comms/agent_update_helper_pgtest_test.go @@ -98,7 +98,7 @@ func updateBlocksFromWire(blocks []*compassv1.MessageBlock) ([]store.MessageBloc if body == nil { return nil, connect.NewError(connect.CodeInvalidArgument, errEmptyBlock) } - ask, err := askFromWire(body.Ask) + ask, err := askFromWire(body.Ask, "") if err != nil { return nil, err } diff --git a/go/internal/comms/ask_mapping_test.go b/go/internal/comms/ask_mapping_test.go index 2b18fecc0..3df2c562b 100644 --- a/go/internal/comms/ask_mapping_test.go +++ b/go/internal/comms/ask_mapping_test.go @@ -81,7 +81,7 @@ func TestAskFromWireDropsEveryServerOwnedField(t *testing.T) { // one so the content assertion also checks canonicalization. wire.Ref = "RigelBuild/Compass#12" - got, err := askFromWire(wire) + got, err := askFromWire(wire, "") if err != nil { t.Fatalf("askFromWire: %v", err) } @@ -295,7 +295,7 @@ func TestAskFromWireRef(t *testing.T) { {name: "url", in: "https://github.com/o/r/pull/1", wantErr: true}, } { t.Run(tc.name, func(t *testing.T) { - got, err := askFromWire(&compassv1.Ask{Ref: tc.in}) + got, err := askFromWire(&compassv1.Ask{Ref: tc.in}, "") if tc.wantErr { if connect.CodeOf(err) != connect.CodeInvalidArgument { t.Fatalf("askFromWire(%q) err = %v, want invalid_argument", tc.in, err) @@ -311,3 +311,14 @@ func TestAskFromWireRef(t *testing.T) { }) } } + +func TestAskFromWireUsesConfiguredGitHubHost(t *testing.T) { + got, err := askFromWire(&compassv1.Ask{Ref: "owner/repo#7"}, "ghe.example") + if err != nil { + t.Fatalf("askFromWire: %v", err) + } + want := store.ForgeCoord{Provider: store.ForgeProviderGitHub, Host: "ghe.example", Repo: "owner/repo", Number: 7} + if got.RefCoord == nil || *got.RefCoord != want { + t.Fatalf("RefCoord = %+v, want %+v", got.RefCoord, want) + } +} diff --git a/go/internal/comms/comms.go b/go/internal/comms/comms.go index f84beb564..ef58fc266 100644 --- a/go/internal/comms/comms.go +++ b/go/internal/comms/comms.go @@ -64,7 +64,19 @@ type Comms struct { // exactly like hub.SetSettleSink). Set once at server assembly BEFORE any RPC // is served, so it needs no lock. Nil-safe: a Comms with no presence source // (a unit test, or an un-wired path) reports every agent OFFLINE. - presence PresenceSource + presence PresenceSource + mirrorer Mirrorer + driver MirrorDriver + githubHost string +} + +type Mirrorer interface { + MirrorAsUser(ctx context.Context, user store.Account, req *compassv1.MirrorToForgeRequest) (string, error) +} + +// MirrorDriver delivers ask rows after their source messages commit. +type MirrorDriver interface { + DriveAsks(ctx context.Context, askIDs []string) } // NewComms constructs the CommsService handler over store and bus. adminID is the @@ -111,6 +123,17 @@ func (c *Comms) SetPresenceSource(src PresenceSource) { c.presence = src } +// SetMirrorer wires the user-click forge path and post-commit ask driver before serving. +func (c *Comms) SetMirrorer(m Mirrorer, d MirrorDriver) { + c.mirrorer = m + c.driver = d +} + +// SetForgeHost configures the GitHub host used to derive ask coordinates. +func (c *Comms) SetForgeHost(host string) { + c.githubHost = host +} + // Ensure Comms satisfies the generated handler interface at compile time. var _ compassv1connect.CommsServiceHandler = (*Comms)(nil) @@ -451,7 +474,7 @@ func (c *Comms) PostMessage( if req.Msg.GetTurnSequence() > math.MaxInt64 { return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("turn_sequence %d exceeds signed BIGINT range", req.Msg.GetTurnSequence())) } - blocks, err := blocksFromWire(req.Msg.GetBlocks()) + blocks, err := blocksFromWire(req.Msg.GetBlocks(), c.githubHost) if err != nil { return nil, err } @@ -476,6 +499,7 @@ func (c *Comms) PostMessage( // spurious live state-change for a row that did not change. if inserted { c.publishMessagePosted(ctx, msg) + c.driveCommittedAsks(ctx, msg) } return connect.NewResponse(&compassv1.PostMessageResponse{Message: MessageToWire(msg)}), nil } @@ -518,9 +542,33 @@ func (c *Comms) RespondToAsk( // the ack-gated cursor + resweep (RIG-2257: no bespoke ask wake). c.publishMessageUpdated(askMsg) c.publishMessagePosted(ctx, answerMsg) + c.driveCommittedAsks(ctx, askMsg) return connect.NewResponse(&compassv1.RespondToAskResponse{}), nil } +// MirrorToForge accepts a user click after confirming that the caller is a user. +func (c *Comms) MirrorToForge( + ctx context.Context, + req *connect.Request[compassv1.MirrorToForgeRequest], +) (*connect.Response[compassv1.MirrorToForgeResponse], error) { + user := c.actorFromContext(ctx) + account, err := c.store.GetAccount(ctx, user) + if err != nil { + return nil, edgeError(err) + } + if account.User == nil { + return nil, connect.NewError(connect.CodePermissionDenied, errors.New("comms: forge mirroring is user-only")) + } + if c.mirrorer == nil { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("comms: forge mirroring is not configured")) + } + url, err := c.mirrorer.MirrorAsUser(ctx, account, req.Msg) + if err != nil { + return nil, err + } + return connect.NewResponse(&compassv1.MirrorToForgeResponse{CommentUrl: url}), nil +} + // SearchMessages runs a visibility-scoped full-text search; the store scopes // results to the caller's visible set regardless of the scope field. func (c *Comms) SearchMessages( @@ -769,6 +817,23 @@ func (c *Comms) applyBoardOp( return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("comms: UpdatePinnedBoard request has no pin/unpin op set")) } } +func (c *Comms) driveCommittedAsks(ctx context.Context, msg store.Message) { + if c.driver == nil { + return + } + ids := make([]string, 0) + for _, block := range msg.Blocks { + if block.Ask != nil { + ids = append(ids, block.Ask.AskID) + } + if block.AskAnswer != nil { + ids = append(ids, block.AskAnswer.Ask.AskID) + } + } + if len(ids) != 0 { + go c.driver.DriveAsks(context.WithoutCancel(ctx), ids) + } +} // SubscribeComms is implemented in subscribe.go. diff --git a/go/internal/comms/comms_mirror_test.go b/go/internal/comms/comms_mirror_test.go new file mode 100644 index 000000000..393f82920 --- /dev/null +++ b/go/internal/comms/comms_mirror_test.go @@ -0,0 +1,108 @@ +//go:build pgtest + +package comms + +import ( + "context" + "testing" + "time" + + "connectrpc.com/connect" + + compassv1 "github.com/RigelBuild/compass/go/gen/compass/v1" + "github.com/RigelBuild/compass/go/internal/store" +) + +type mirrorRecorder struct { + user store.AccountID + req *compassv1.MirrorToForgeRequest +} + +func (m *mirrorRecorder) MirrorAsUser(_ context.Context, user store.Account, req *compassv1.MirrorToForgeRequest) (string, error) { + m.user = user.ID + m.req = req + return "https://forge.example/comment", nil +} + +type mirrorDriverRecorder struct { + calls chan []string +} + +func (m mirrorDriverRecorder) DriveAsks(_ context.Context, askIDs []string) { + m.calls <- append([]string(nil), askIDs...) +} + +func TestCommittedAskMessagesDriveMirrors(t *testing.T) { + svc, st := newHandler(t) + owner := mustUser(t, st, "mirror-driver-owner") + agent := mustAgent(t, st, owner.ID, "mirror-driver-agent") + channel, err := st.CreateChannel(context.Background(), owner.ID, store.NewChannel{ + Name: "mirror-driver-room", Kind: store.ChannelKindChannel, + MemberAccountIDs: []store.AccountID{owner.ID, agent.ID}, + }) + if err != nil { + t.Fatalf("CreateChannel: %v", err) + } + driver := mirrorDriverRecorder{calls: make(chan []string, 2)} + svc.SetMirrorer(nil, driver) + posted, err := svc.PostMessage(WithActor(context.Background(), agent.ID), connect.NewRequest(&compassv1.PostMessageRequest{ + Container: &compassv1.PostMessageRequest_ChannelId{ChannelId: string(channel.ID)}, + Topic: &compassv1.PostMessageRequest_TopicName{TopicName: "mirror-asks"}, CreateTopic: true, + Blocks: []*compassv1.MessageBlock{{Block: &compassv1.MessageBlock_Ask{Ask: &compassv1.Ask{ + Ref: "RIG-12", Questions: []*compassv1.AskQuestion{{QuestionId: "q1", Question: "Ship?", Options: []*compassv1.AskOption{{Id: "yes", Label: "Yes"}}}}, + }}}}, + })) + if err != nil { + t.Fatalf("PostMessage: %v", err) + } + askID := posted.Msg.GetMessage().GetBlocks()[0].GetAsk().GetAskId() + assertDrivenAskID(t, driver.calls, askID) + if _, err := svc.RespondToAsk(WithActor(context.Background(), owner.ID), connect.NewRequest(&compassv1.RespondToAskRequest{ + AskId: askID, Answers: []*compassv1.AskQuestionAnswer{{QuestionId: "q1", ChosenOptionIds: []string{"yes"}}}, + })); err != nil { + t.Fatalf("RespondToAsk: %v", err) + } + assertDrivenAskID(t, driver.calls, askID) +} + +func assertDrivenAskID(t *testing.T, calls <-chan []string, want string) { + t.Helper() + select { + case got := <-calls: + if len(got) != 1 || got[0] != want { + t.Fatalf("DriveAsks ids = %v, want [%s]", got, want) + } + case <-time.After(5 * time.Second): + t.Fatalf("DriveAsks did not receive ask %q", want) + } +} + +func TestMirrorToForgeIsUserOnlyAndDelegates(t *testing.T) { + svc, st := newHandler(t) + owner := mustUser(t, st, "mirror-edge-owner") + agent := mustAgent(t, st, owner.ID, "mirror-edge-agent") + mirrorer := &mirrorRecorder{} + svc.SetMirrorer(mirrorer, nil) + request := &compassv1.MirrorToForgeRequest{Ref: "RIG-12", TopicId: "topic", ChannelId: "channel"} + + _, err := svc.MirrorToForge(WithActor(context.Background(), agent.ID), connect.NewRequest(request)) + connectCodeIs(t, err, connect.CodePermissionDenied, "agent MirrorToForge") + if mirrorer.req != nil { + t.Fatal("MirrorAsUser was called for an agent") + } + + response, err := svc.MirrorToForge(WithActor(context.Background(), owner.ID), connect.NewRequest(request)) + if err != nil { + t.Fatalf("user MirrorToForge: %v", err) + } + if mirrorer.user != owner.ID || mirrorer.req != request || response.Msg.GetCommentUrl() != "https://forge.example/comment" { + t.Fatalf("delegated mirror user=%q request=%p response=%+v", mirrorer.user, mirrorer.req, response.Msg) + } +} + +func TestMirrorToForgeWithoutMirrorerIsUnimplemented(t *testing.T) { + svc, st := newHandler(t) + owner := mustUser(t, st, "mirror-edge-unwired") + _, err := svc.MirrorToForge(WithActor(context.Background(), owner.ID), connect.NewRequest(&compassv1.MirrorToForgeRequest{})) + connectCodeIs(t, err, connect.CodeUnimplemented, "MirrorToForge without a mirrorer") +} diff --git a/go/internal/comms/mapping.go b/go/internal/comms/mapping.go index 340cb6e5c..2d08bdf95 100644 --- a/go/internal/comms/mapping.go +++ b/go/internal/comms/mapping.go @@ -10,6 +10,7 @@ import ( compassv1 "github.com/RigelBuild/compass/go/gen/compass/v1" "github.com/RigelBuild/compass/go/internal/fabric" + "github.com/RigelBuild/compass/go/internal/forge" "github.com/RigelBuild/compass/go/internal/refs" "github.com/RigelBuild/compass/go/internal/store" ) @@ -405,7 +406,7 @@ func (c *Comms) memberUpdatesFromWire(ctx context.Context, caller store.AccountI // blocksFromWire maps wire message blocks onto store blocks, rejecting an empty // or unset block oneof as an invalid argument (the store also rejects a // malformed oneof, but catching it at the edge gives a precise connect error). -func blocksFromWire(blocks []*compassv1.MessageBlock) ([]store.MessageBlock, error) { +func blocksFromWire(blocks []*compassv1.MessageBlock, githubHost string) ([]store.MessageBlock, error) { out := make([]store.MessageBlock, 0, len(blocks)) for _, b := range blocks { switch body := b.GetBlock().(type) { @@ -419,7 +420,7 @@ func blocksFromWire(blocks []*compassv1.MessageBlock) ([]store.MessageBlock, err if body == nil { return nil, connect.NewError(connect.CodeInvalidArgument, errEmptyBlock) } - ask, err := askFromWire(body.Ask) + ask, err := askFromWire(body.Ask, githubHost) if err != nil { return nil, err } @@ -437,7 +438,7 @@ func blocksFromWire(blocks []*compassv1.MessageBlock) ([]store.MessageBlock, err return out, nil } -func askFromWire(a *compassv1.Ask) (*store.Ask, error) { +func askFromWire(a *compassv1.Ask, githubHost string) (*store.Ask, error) { ref, err := askRefFromWire(a.GetRef()) if err != nil { return nil, err @@ -446,7 +447,22 @@ func askFromWire(a *compassv1.Ask) (*store.Ask, error) { // the store's mintAskIDs assigns a fresh, globally-unique id on append. Honoring // a wire value would let two posts share an ask_id, making RespondToAsk's // containment SELECT match multiple rows and answer a nondeterministic one. - return &store.Ask{AskID: "", Ref: ref, Questions: askQuestionsFromWire(a)}, nil + var coord *store.ForgeCoord + if ref != "" { + r, parseErr := refs.Parse(ref) + if parseErr != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("comms: parse canonical ask ref: %w", parseErr)) + } + if r.Kind == refs.KindForge { + if githubHost == "" { + githubHost = "github.com" + } + coord = &store.ForgeCoord{Provider: store.ForgeProviderGitHub, Host: githubHost, Repo: r.Owner + "/" + r.Repo, Number: r.Number} + } else { + coord = &store.ForgeCoord{Provider: store.ForgeProviderLinear, Host: forge.LinearHost, Repo: r.Key, Number: r.Number} + } + } + return &store.Ask{AskID: "", Ref: ref, RefCoord: coord, Questions: askQuestionsFromWire(a)}, nil } // askRefFromWire canonicalizes an ask's optional ref; "" means no ref. diff --git a/go/internal/forge/github.go b/go/internal/forge/github.go index 6f07f1993..3a1db343c 100644 --- a/go/internal/forge/github.go +++ b/go/internal/forge/github.go @@ -34,6 +34,11 @@ type TokenSource interface { Invalidate() } +// AppIdentitySource resolves the login a GitHub App authors comments as. +type AppIdentitySource interface { + AppLogin(ctx context.Context) (string, error) +} + // GitHubConfig configures a GitHub read client. type GitHubConfig struct { Host string // "github.com" or a GHES host; API base derives from it @@ -94,6 +99,9 @@ type GitHub struct { // now is the clock seam (defaults to time.Now in NewGitHub); tests override // it to drive the reset-time gate deterministically without real sleeps. now func() time.Time + + // identity resolves the App's comment login; nil when the token is not an App. + identity AppIdentitySource } // rateResource is a GitHub rate-limit bucket, named by the x-ratelimit-resource @@ -140,7 +148,20 @@ func NewGitHub(cfg GitHubConfig) *GitHub { if client == nil { client = &http.Client{Timeout: 30 * time.Second} } - return &GitHub{host: cfg.Host, token: cfg.Token, client: client, now: time.Now} + return &GitHub{host: cfg.Host, token: cfg.Token, client: client, now: time.Now, identity: identitySource(cfg.Token)} +} + +func identitySource(token TokenSource) AppIdentitySource { + identity, _ := token.(AppIdentitySource) + return identity +} + +// AppLogin returns the GitHub App login that authors marker comments. +func (g *GitHub) AppLogin(ctx context.Context) (string, error) { + if g.identity == nil { + return "", errors.New("forge: GitHub App identity is unavailable") + } + return g.identity.AppLogin(ctx) } // Compile-time proof that GitHub satisfies the Provider interface (the read diff --git a/go/internal/forge/githubapp.go b/go/internal/forge/githubapp.go index ed1335b11..357431d85 100644 --- a/go/internal/forge/githubapp.go +++ b/go/internal/forge/githubapp.go @@ -16,6 +16,7 @@ import ( "io" "net/http" "strconv" + "strings" "sync" "time" @@ -63,6 +64,9 @@ type appTokenSource struct { token string refresh time.Time // when we proactively re-mint (token expiry - tokenRefreshLead) hasToken bool + + identityMu sync.Mutex + appLogin string } // NewAppTokenSource returns a TokenSource minting installation access tokens: @@ -171,6 +175,62 @@ func appAPIBase(host string) string { return "https://" + host + "/api/v3" } +// AppLogin resolves the installation owner's native comment login once. +func (s *appTokenSource) AppLogin(ctx context.Context) (string, error) { + s.identityMu.Lock() + if s.appLogin != "" { + login := s.appLogin + s.identityMu.Unlock() + return login, nil + } + s.identityMu.Unlock() + + pemBytes, err := s.cfg.PrivateKey(ctx) + if err != nil { + return "", fmt.Errorf("forge: resolve app private key for identity: %w", err) + } + key, err := parseRSAPrivateKey(pemBytes) + if err != nil { + return "", fmt.Errorf("forge: parse app private key for identity: %w", err) + } + jwt, err := s.buildAppJWT(key) + if err != nil { + return "", fmt.Errorf("forge: build app identity jwt: %w", err) + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.apiBase+"/app", nil) + if err != nil { + return "", fmt.Errorf("forge: build app identity request: %w", err) + } + req.Header.Set("Authorization", "Bearer "+jwt) + req.Header.Set("Accept", "application/vnd.github+json") + resp, err := s.client.Do(req) + if err != nil { + return "", fmt.Errorf("forge: resolve app login: %w", err) + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + _, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4096)) + return "", fmt.Errorf("forge: app identity returned status %d", resp.StatusCode) + } + var out struct { + Slug string `json:"slug"` + } + if err := json.NewDecoder(io.LimitReader(resp.Body, 4096)).Decode(&out); err != nil { + return "", fmt.Errorf("forge: decode app identity: %w", err) + } + if strings.TrimSpace(out.Slug) == "" { + return "", errors.New("forge: app identity returned no slug") + } + login := out.Slug + "[bot]" + s.identityMu.Lock() + if s.appLogin == "" { + s.appLogin = login + } + login = s.appLogin + s.identityMu.Unlock() + return login, nil +} + // Token returns the cached installation token when it is still comfortably // inside its lifetime, else mints a fresh one. The mint is singleflighted: // concurrent callers observing a stale cache share one HTTP round-trip. diff --git a/go/internal/forge/linear.go b/go/internal/forge/linear.go index 327674578..0fcb38d69 100644 --- a/go/internal/forge/linear.go +++ b/go/internal/forge/linear.go @@ -150,6 +150,9 @@ type Linear struct { // createdOrder is createdIDs' keys oldest-first, so the cap evicts the oldest. createdOrder []string + identityMu sync.Mutex + appLogin string + identityDone chan struct{} // probeDone/actorCapable cache the one-time actor-capability probe (A4). // Once probeDone, actorCapable governs whether writes set createAsUser. probeDone bool @@ -184,6 +187,53 @@ func NewLinear(cfg LinearConfig) *Linear { } } +// AppLogin resolves the Compass app identity used in Linear comment records. +func (l *Linear) AppLogin(ctx context.Context) (string, error) { + for { + l.identityMu.Lock() + if l.appLogin != "" { + login := l.appLogin + l.identityMu.Unlock() + return login, nil + } + if done := l.identityDone; done != nil { + l.identityMu.Unlock() + select { + case <-ctx.Done(): + return "", ctx.Err() + case <-done: + } + continue + } + done := make(chan struct{}) + l.identityDone = done + l.identityMu.Unlock() + + const query = `query CompassMirrorIdentity { viewer { id displayName } }` + var out struct { + Viewer struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + } `json:"viewer"` + } + err := l.doGraphQL(ctx, query, nil, &out) + if err == nil && (out.Viewer.ID == "" || strings.TrimSpace(out.Viewer.DisplayName) == "") { + err = errors.New("forge: Linear viewer identity was empty") + } + l.identityMu.Lock() + if err == nil { + l.appLogin = out.Viewer.DisplayName + } + l.identityDone = nil + close(done) + l.identityMu.Unlock() + if err != nil { + return "", fmt.Errorf("forge: resolve Linear app identity: %w", err) + } + return out.Viewer.DisplayName, nil + } +} + // Compile-time proof that Linear satisfies the Provider interface. var _ Provider = (*Linear)(nil) diff --git a/go/internal/forge/linear_test.go b/go/internal/forge/linear_test.go index 510356bb3..239a8b054 100644 --- a/go/internal/forge/linear_test.go +++ b/go/internal/forge/linear_test.go @@ -328,6 +328,24 @@ func TestLinearDeleteCommentUnsupported(t *testing.T) { t.Fatalf("DeleteComment issued %d requests, want zero", rt.calls) } } +func TestLinearAppLoginUsesDisplayName(t *testing.T) { + rt := &scriptedRoundTripper{responses: []scriptedResponse{{ + status: http.StatusOK, + body: `{"data":{"viewer":{"id":"viewer-id","name":"API name","displayName":"Compass App"}}}`, + }}} + l := newTestLinear(rt, &fakeTokenSource{token: "t"}, slog.New(&capturingHandler{})) + login, err := l.AppLogin(context.Background()) + if err != nil { + t.Fatalf("AppLogin: %v", err) + } + if login != "Compass App" { + t.Fatalf("AppLogin = %q, want comment display name", login) + } + query, _ := decodeGraphQLReq(t, readReqBody(t, rt.requests[0])) + if !strings.Contains(query, "viewer { id displayName }") { + t.Fatalf("identity query = %q, want displayName", query) + } +} func TestLinearListCommentsSinceWalksPages(t *testing.T) { rt := &scriptedRoundTripper{responses: []scriptedResponse{ diff --git a/go/internal/forge/owner.go b/go/internal/forge/owner.go index b923152a0..42bee15a9 100644 --- a/go/internal/forge/owner.go +++ b/go/internal/forge/owner.go @@ -14,6 +14,7 @@ import ( "errors" "fmt" "regexp" + "strings" ) // Author is the attribution a stamped body carries: the Compass agent handle, @@ -71,6 +72,13 @@ func header(author Author) string { "** (Compass agent, owned by **@" + author.OwnerHandle + "**)\n\n---\n\n" } +// MaxOwnerHeaderLen is the longest header StampOwner can prepend for session, so a +// caller can budget body bytes before stamping with handles it has not resolved yet. +func MaxOwnerHeaderLen(session string) int { + longest := strings.Repeat("a", 39) + return len(header(Author{AgentHandle: longest, OwnerHandle: longest, SessionID: session})) +} + // validateField reports whether a header field is legal, naming the field in the // error so a refusal is diagnosable. func validateField(name, value string) error { diff --git a/go/internal/mirror/drive.go b/go/internal/mirror/drive.go new file mode 100644 index 000000000..28d518b6a --- /dev/null +++ b/go/internal/mirror/drive.go @@ -0,0 +1,456 @@ +package mirror + +import ( + "context" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "log/slog" + "sort" + "sync/atomic" + "time" + + "github.com/RigelBuild/compass/go/internal/forge" + "github.com/RigelBuild/compass/go/internal/refs" + "github.com/RigelBuild/compass/go/internal/store" +) + +const ( + leaseTTL = 2 * time.Minute + renewInterval = 30 * time.Second + commentWait = 2 * time.Minute + pendingState = int16(1) + doneState = int16(2) + failedState = int16(3) + maxAttempts = 3 +) + +var ( + // ErrRefused is returned by Poster for a policy or scope refusal. + ErrRefused = errors.New("mirror: post refused") + sleep = sleepContext +) + +// Poster writes and reconciles comments for one actor and forge coordinate. +// ListMirrorComments returns only comments by the configured Compass App login; +// implementations fail closed when they cannot resolve that account. +// BodyLimit excludes the owner header; Drive reserves the row marker itself. +type Poster interface { + PostMirror(ctx context.Context, actor store.AccountID, need store.ForgeMirrorMode, ref refs.Ref, commentKey, clientID, body string) (forge.Comment, error) + ListMirrorComments(ctx context.Context, actor store.AccountID, ref refs.Ref, since time.Time) ([]forge.Comment, error) + DeleteMirrorComment(ctx context.Context, actor store.AccountID, ref refs.Ref, commentKey string) error + BodyLimit(ref refs.Ref) int +} + +// LinkFunc builds a source link from the stored channel and topic IDs. +type LinkFunc func(channelID, topicID string) Link + +// BodyFunc renders a source-2 row within its net body limit. Drive appends the row marker. +type BodyFunc func(context.Context, store.ForgeMirrorRow, int) (string, error) + +// Drive leases and delivers each mirror row in order. +func Drive(ctx context.Context, st *store.Store, p Poster, rows []store.ForgeMirrorRow, link LinkFunc, bodyFn BodyFunc) { + log := slog.Default() + for _, candidate := range rows { + if ctx.Err() != nil { + return + } + if _, err := DriveOne(ctx, st, p, candidate, link, bodyFn); err != nil { + log.ErrorContext(ctx, "drive forge mirror", "mirror_id", candidate.ID, "error", err) + } + } +} + +// DriveOne leases and processes one row, then returns the tenant-scoped stored row. +func DriveOne(ctx context.Context, st *store.Store, p Poster, candidate store.ForgeMirrorRow, link LinkFunc, bodyFn BodyFunc) (store.ForgeMirrorRow, error) { + log := slog.Default() + for { + if ctx.Err() != nil { + return store.ForgeMirrorRow{}, ctx.Err() + } + token, err := newLeaseToken() + if err != nil { + return store.ForgeMirrorRow{}, fmt.Errorf("mirror: create lease token: %w", err) + } + row, ok, err := st.LeaseForgeMirror(ctx, candidate.ID, token, leaseTTL) + if err != nil { + return store.ForgeMirrorRow{}, fmt.Errorf("mirror: lease row %q: %w", candidate.ID, err) + } + if !ok { + return st.ForgeMirrorByID(ctx, candidate.ID) + } + processErr := processLeasedRow(ctx, st, p, row, token, link, bodyFn, log) + finalRow, readErr := st.ForgeMirrorByID(ctx, candidate.ID) + if readErr != nil { + return finalRow, errors.Join(processErr, readErr) + } + repointedComment := finalRow.State == pendingState && finalRow.CommentKey != row.CommentKey + refusedCurrentRevision := errors.Is(processErr, ErrRefused) && finalRow.BodyRev <= row.BodyRev + if refusedCurrentRevision || finalRow.BodyRev <= row.BodyRev && !repointedComment { + return finalRow, processErr + } + candidate = finalRow + } +} + +func processLeasedRow(ctx context.Context, st *store.Store, p Poster, row store.ForgeMirrorRow, token string, link LinkFunc, bodyFn BodyFunc, log *slog.Logger) error { + stopRenew, leaseLost, renewDone := renewLease(ctx, st, row.ID, token, log) + defer func() { + close(stopRenew) + <-renewDone + }() + + if row.State == doneState { + if row.RecheckAt != nil { + if err := recheckComments(ctx, st, p, row, token, leaseLost); err != nil { + return fmt.Errorf("mirror: recheck row %q: %w", row.ID, err) + } + } + return nil + } + + ref, err := refs.Parse(row.TargetRef) + if err != nil { + cause := fmt.Errorf("mirror: parse target ref %q: %w", row.TargetRef, err) + if markErr := markFailed(ctx, st, row, token, cause); markErr != nil { + return errors.Join(cause, fmt.Errorf("mirror: mark invalid target failed: %w", markErr)) + } + return cause + } + body, err := renderBody(ctx, st, p, row, link, bodyFn, ref) + if err != nil { + if markErr := markFailed(ctx, st, row, token, err); markErr != nil { + return errors.Join(err, fmt.Errorf("mirror: mark render failure: %w", markErr)) + } + return err + } + need := store.ForgeMirrorAuto + if row.Agent == "" { + need = store.ForgeMirrorManual + } + if err := deliverRow(ctx, st, p, row, token, ref, need, body, leaseLost, row.BodyRev); err != nil { + return fmt.Errorf("mirror: deliver row %q: %w", row.ID, err) + } + return nil +} + +func deliverRow(ctx context.Context, st *store.Store, p Poster, row store.ForgeMirrorRow, token string, ref refs.Ref, need store.ForgeMirrorMode, body string, leaseLost *atomic.Bool, renderedRev int16) error { + if row.CommentKey != "" && row.PostedRev == row.BodyRev { + return markDelivered(ctx, st, row, token, row.CommentID, row.CommentKey, row.CommentURL, false, renderedRev) + } + + state := deliveryState{ + row: row, + isGitHub: ref.Kind == refs.KindForge, + uncertain: ref.Kind == refs.KindForge && row.CommentKey == "" && row.FirstAttemptAt != nil, + } + for attempt := range maxAttempts { + if lostOrCanceled(ctx, leaseLost) { + return nil + } + retry, err := deliverAttempt(ctx, st, p, token, ref, need, body, leaseLost, &state, renderedRev) + if err != nil { + state.lastErr = err + } + if !retry { + return err + } + if attempt+1 < maxAttempts { + if err := sleep(ctx, retryBackoff[attempt]); err != nil { + return err + } + } + } + if state.lastErr != nil { + return markFailed(ctx, st, state.row, token, state.lastErr) + } + return nil +} + +type deliveryState struct { + row store.ForgeMirrorRow + isGitHub bool + uncertain bool + lastErr error +} + +func deliverAttempt(ctx context.Context, st *store.Store, p Poster, token string, ref refs.Ref, need store.ForgeMirrorMode, body string, leaseLost *atomic.Bool, state *deliveryState, renderedRev int16) (bool, error) { + commentKey, comment, adopted, err := recoverPriorComment(ctx, p, state.row, ref, state.isGitHub, leaseLost) + if err != nil { + return true, err + } + if adopted { + commentKey = comment.Key + state.uncertain = true + // An adopted comment's body revision is unknown, so always edit it. + } + + if commentKey == "" { + priorAttempt := state.row.FirstAttemptAt != nil + updated, ok, err := st.BeginForgeMirrorAttempt(ctx, state.row.ID, token) + if err != nil { + return true, err + } + if !ok { + return false, nil + } + state.row = updated + state.uncertain = state.uncertain || state.isGitHub && priorAttempt + } + + if lostOrCanceled(ctx, leaseLost) { + return false, nil + } + comment, err = p.PostMirror(ctx, state.row.Actor, need, ref, commentKey, state.row.ClientCommentID, body) + if err == nil { + return false, markDelivered(ctx, st, state.row, token, comment.ID, comment.Key, comment.URL, state.uncertain, renderedRev) + } + if errors.Is(err, ErrRefused) { + return false, errors.Join(ErrRefused, markFailed(ctx, st, state.row, token, err)) + } + if state.isGitHub { + state.uncertain = true + } + return true, err +} + +func recoverPriorComment(ctx context.Context, p Poster, row store.ForgeMirrorRow, ref refs.Ref, isGitHub bool, leaseLost *atomic.Bool) (string, forge.Comment, bool, error) { + if row.CommentKey != "" || !isGitHub || row.FirstAttemptAt == nil { + return row.CommentKey, forge.Comment{}, false, nil + } + adopted, comment, err := recoverComment(ctx, p, row, ref, leaseLost) + return row.CommentKey, comment, adopted, err +} + +var retryBackoff = [...]time.Duration{time.Second, 4 * time.Second, 16 * time.Second} + +func renderBody(ctx context.Context, st *store.Store, p Poster, row store.ForgeMirrorRow, link LinkFunc, bodyFn BodyFunc, ref refs.Ref) (string, error) { + providerLimit := p.BodyLimit(ref) + limit := providerLimit + + markerLength := len(WithMarker("", row.ID)) + if providerLimit > 0 { + if providerLimit < markerLength { + return "", errors.New("mirror: provider body limit cannot fit row marker") + } + + limit = providerLimit - markerLength + } + if row.Source == 2 { + if bodyFn == nil { + return "", errors.New("mirror: source-2 body supplier is required") + } + body, err := bodyFn(ctx, row, limit) + if err != nil { + return "", fmt.Errorf("mirror: render source-2 row: %w", err) + } + if providerLimit > 0 { + body = truncateUTF8(body, limit) + } + return WithMarker(body, row.ID), nil + } + if row.Source != 1 { + return "", fmt.Errorf("mirror: unsupported source %d", row.Source) + } + ask, err := st.AskForForgeMirror(ctx, row.Actor, row.AskID) + if err != nil { + return "", err + } + var askContent *store.Ask + for _, block := range ask.Message.Blocks { + if block.Ask != nil { + askContent = block.Ask + break + } + } + if askContent == nil { + return "", errors.New("mirror: ask row contains no ask block") + } + linkValue := Link{Channel: ask.Channel, Topic: ask.Topic} + if link != nil { + linkValue.URL = link(string(ask.ChannelID), ask.Message.TopicID).URL + } + return WithMarker(renderAskWithinLimit(*askContent, ask.AnsweredBy, linkValue, limit), row.ID), nil +} + +func recoverComment(ctx context.Context, p Poster, row store.ForgeMirrorRow, ref refs.Ref, leaseLost *atomic.Bool) (bool, forge.Comment, error) { + lastAttempt := row.LastAttemptAt + if lastAttempt == nil { + lastAttempt = row.FirstAttemptAt + } + if lastAttempt != nil { + if wait := time.Until(lastAttempt.Add(commentWait)); wait > 0 { + if err := sleep(ctx, wait); err != nil { + return false, forge.Comment{}, err + } + } + } + if lostOrCanceled(ctx, leaseLost) { + return false, forge.Comment{}, context.Canceled + } + since := time.Time{} + if row.FirstAttemptAt != nil { + since = row.FirstAttemptAt.Add(-time.Minute) + } + comments, err := p.ListMirrorComments(ctx, row.Actor, ref, since) + if err != nil { + return false, forge.Comment{}, fmt.Errorf("mirror: list comments for recovery: %w", err) + } + marked := make([]forge.Comment, 0, len(comments)) + for _, comment := range comments { + if HasMarker(comment.Body, row.ID) { + marked = append(marked, comment) + } + } + if len(marked) == 0 { + return false, forge.Comment{}, nil + } + marked = oldestComments(marked) + for _, duplicate := range marked[1:] { + if lostOrCanceled(ctx, leaseLost) { + return false, forge.Comment{}, context.Canceled + } + if err := p.DeleteMirrorComment(ctx, row.Actor, ref, duplicate.Key); err != nil { + return false, forge.Comment{}, fmt.Errorf("mirror: delete duplicate comment %q: %w", duplicate.Key, err) + } + } + return true, marked[0], nil +} + +func recheckComments(ctx context.Context, st *store.Store, p Poster, row store.ForgeMirrorRow, token string, leaseLost *atomic.Bool) error { + ref, err := refs.Parse(row.TargetRef) + if err != nil { + return markFailed(ctx, st, row, token, fmt.Errorf("mirror: parse target ref %q: %w", row.TargetRef, err)) + } + if lostOrCanceled(ctx, leaseLost) { + return nil + } + since := time.Time{} + if row.FirstAttemptAt != nil { + since = row.FirstAttemptAt.Add(-time.Minute) + } + comments, err := p.ListMirrorComments(ctx, row.Actor, ref, since) + if err != nil { + return fmt.Errorf("mirror: list comments for recheck: %w", err) + } + marked := make([]forge.Comment, 0, len(comments)) + for _, comment := range comments { + if HasMarker(comment.Body, row.ID) { + marked = append(marked, comment) + } + } + marked = oldestComments(marked) + for _, duplicate := range marked[1:] { + if lostOrCanceled(ctx, leaseLost) { + return nil + } + if err := p.DeleteMirrorComment(ctx, row.Actor, ref, duplicate.Key); err != nil { + return fmt.Errorf("mirror: delete duplicate comment %q: %w", duplicate.Key, err) + } + } + mark := store.ForgeMirrorMark{PostedRev: row.PostedRev, State: doneState, ClearRecheck: true} + if len(marked) > 0 { + mark.CommentID = marked[0].ID + mark.CommentKey = marked[0].Key + mark.URL = marked[0].URL + if marked[0].Key != row.CommentKey { + mark.PostedRev = 0 + mark.State = pendingState + } + } else { + mark.CommentID = row.CommentID + mark.CommentKey = row.CommentKey + mark.URL = row.CommentURL + } + if err := st.MarkForgeMirror(ctx, row.ID, token, mark); err != nil { + return fmt.Errorf("mirror: clear recheck state: %w", err) + } + return nil +} + +func markDelivered(ctx context.Context, st *store.Store, row store.ForgeMirrorRow, token string, id uint64, key, url string, recheck bool, renderedRev int16) error { + if err := st.MarkForgeMirror(ctx, row.ID, token, store.ForgeMirrorMark{ + PostedRev: renderedRev, State: doneState, CommentID: id, + CommentKey: key, URL: url, Recheck: recheck, + }); err != nil { + return fmt.Errorf("mirror: mark delivered: %w", err) + } + return nil +} + +func markFailed(ctx context.Context, st *store.Store, row store.ForgeMirrorRow, token string, cause error) error { + if err := st.MarkForgeMirror(ctx, row.ID, token, store.ForgeMirrorMark{ + PostedRev: row.PostedRev, State: failedState, Err: cause.Error(), + }); err != nil { + return fmt.Errorf("mirror: mark failed: %w", err) + } + return nil +} + +func renewLease(ctx context.Context, st *store.Store, id, token string, log *slog.Logger) (chan struct{}, *atomic.Bool, chan struct{}) { + stop := make(chan struct{}) + done := make(chan struct{}) + lost := &atomic.Bool{} + go func() { + defer close(done) + ticker := time.NewTicker(renewInterval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-stop: + return + case <-ticker.C: + ok, err := st.RenewForgeMirror(ctx, id, token, leaseTTL) + if err != nil { + lost.Store(true) + log.ErrorContext(ctx, "renew forge mirror lease", "mirror_id", id, "error", err) + return + } + if !ok { + lost.Store(true) + return + } + } + } + }() + return stop, lost, done +} + +func lostOrCanceled(ctx context.Context, leaseLost *atomic.Bool) bool { + return leaseLost.Load() || ctx.Err() != nil +} + +func newLeaseToken() (string, error) { + var token [16]byte + if _, err := rand.Read(token[:]); err != nil { + return "", fmt.Errorf("mirror: read lease token: %w", err) + } + return hex.EncodeToString(token[:]), nil +} + +func sleepContext(ctx context.Context, duration time.Duration) error { + if duration <= 0 { + return ctx.Err() + } + timer := time.NewTimer(duration) + defer timer.Stop() + select { + case <-ctx.Done(): + return ctx.Err() + case <-timer.C: + return nil + } +} + +func oldestComments(comments []forge.Comment) []forge.Comment { + sort.Slice(comments, func(i, j int) bool { + if comments[i].ID != 0 && comments[j].ID != 0 { + return comments[i].ID < comments[j].ID + } + return comments[i].Key < comments[j].Key + }) + return comments +} diff --git a/go/internal/mirror/drive_pgtest_test.go b/go/internal/mirror/drive_pgtest_test.go new file mode 100644 index 000000000..f9c9cd7a0 --- /dev/null +++ b/go/internal/mirror/drive_pgtest_test.go @@ -0,0 +1,915 @@ +//go:build pgtest + +package mirror + +import ( + "context" + "errors" + "fmt" + "log/slog" + "sort" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/jackc/pgx/v5/pgtype" + "github.com/jackc/pgx/v5/pgxpool" + + "github.com/RigelBuild/compass/go/internal/forge" + "github.com/RigelBuild/compass/go/internal/pgtest" + "github.com/RigelBuild/compass/go/internal/refs" + "github.com/RigelBuild/compass/go/internal/store" +) + +type postResult struct { + comment forge.Comment + err error + lose bool +} + +type recordedPost struct { + need store.ForgeMirrorMode + key string + clientID string + body string +} + +type fakePoster struct { + mu sync.Mutex + release sync.Once + posts []recordedPost + comments []forge.Comment + deletedKeys []string + results []postResult + listErr error + nextID uint64 + entered chan struct{} + proceed chan struct{} + onBodyLimit func() + onPost func(int) +} + +func (p *fakePoster) BodyLimit(refs.Ref) int { + if p.onBodyLimit != nil { + p.onBodyLimit() + } + return 4096 +} + +func (p *fakePoster) PostMirror(_ context.Context, _ store.AccountID, need store.ForgeMirrorMode, ref refs.Ref, key, clientID, body string) (forge.Comment, error) { + p.mu.Lock() + p.posts = append(p.posts, recordedPost{need: need, key: key, clientID: clientID, body: body}) + if p.entered != nil { + select { + case p.entered <- struct{}{}: + default: + } + } + var result postResult + if len(p.results) > 0 { + result = p.results[0] + p.results = p.results[1:] + } + + var existing *forge.Comment + for i := range p.comments { + if p.comments[i].Key == key || (key == "" && ref.Kind == refs.KindTracker && p.comments[i].Key == clientID) { + existing = &p.comments[i] + break + } + } + if existing != nil { + existing.Body = body + existing.ForgeAccount = "compass-app" + result.comment = *existing + } else { + if result.comment.ID == 0 { + p.nextID++ + commentKey := strconv.FormatUint(p.nextID, 10) + result.comment = forge.Comment{ID: p.nextID, Key: commentKey, URL: "https://forge.test/comment/" + commentKey} + } + if ref.Kind == refs.KindTracker { + result.comment.Key = clientID + } + result.comment.ForgeAccount = "compass-app" + if key == "" && (result.err == nil || result.lose) { + comment := result.comment + comment.Body = body + p.comments = append(p.comments, comment) + } + } + call := len(p.posts) + p.mu.Unlock() + + if p.onPost != nil { + p.onPost(call) + } + if p.proceed != nil { + <-p.proceed + } + return result.comment, result.err +} + +func (p *fakePoster) ListMirrorComments(_ context.Context, _ store.AccountID, _ refs.Ref, _ time.Time) ([]forge.Comment, error) { + p.mu.Lock() + defer p.mu.Unlock() + if p.listErr != nil { + return nil, p.listErr + } + comments := make([]forge.Comment, 0, len(p.comments)) + for _, comment := range p.comments { + if comment.ForgeAccount == "compass-app" { + comments = append(comments, comment) + } + } + sort.Slice(comments, func(i, j int) bool { return comments[i].ID < comments[j].ID }) + return comments, nil +} + +func (p *fakePoster) DeleteMirrorComment(_ context.Context, _ store.AccountID, _ refs.Ref, key string) error { + p.mu.Lock() + defer p.mu.Unlock() + p.deletedKeys = append(p.deletedKeys, key) + for i := range p.comments { + if p.comments[i].Key == key { + p.comments = append(p.comments[:i], p.comments[i+1:]...) + break + } + } + return nil +} + +func (p *fakePoster) snapshot() ([]recordedPost, []forge.Comment, []string) { + p.mu.Lock() + defer p.mu.Unlock() + return append([]recordedPost(nil), p.posts...), append([]forge.Comment(nil), p.comments...), append([]string(nil), p.deletedKeys...) +} + +func (p *fakePoster) unblock() { + p.release.Do(func() { close(p.proceed) }) +} + +func openMirrorStore(t *testing.T) (*store.Store, context.Context, string) { + t.Helper() + ctx := context.Background() + dsn := pgtest.RequireDSN(t) + st, err := store.Open(ctx, dsn) + if err != nil { + t.Fatalf("store.Open: %v", err) + } + t.Cleanup(st.Close) + return st, ctx, dsn +} + +func mirrorOwner(t *testing.T, ctx context.Context, st *store.Store) store.Account { + t.Helper() + owner, err := st.CreateUser(ctx, store.NewUser{Handle: "mirror-owner", DisplayName: "Mirror Owner"}) + if err != nil { + t.Fatalf("CreateUser(owner): %v", err) + } + return owner +} + +func claimClickRow(t *testing.T, ctx context.Context, st *store.Store, owner store.Account, target string) store.ForgeMirrorRow { + t.Helper() + if err := st.SetForgeMirrorSetting(ctx, owner.ID, store.ForgeMirrorSetting{Provider: store.ForgeProviderGitHub, Host: "github.com", Repo: "owner/repo", Mode: store.ForgeMirrorManual}); err != nil { + t.Fatalf("SetForgeMirrorSetting: %v", err) + } + row, created, err := st.ClaimForgeMirror(ctx, store.ForgeMirrorRow{Source: 2, SourceKey: target, Actor: owner.ID, TargetRef: target, BodyRev: 1, State: 1}) + if err != nil || !created { + t.Fatalf("ClaimForgeMirror: created=%v err=%v", created, err) + } + return row +} + +func mirrorAsk(t *testing.T, ctx context.Context, st *store.Store) (store.Account, store.ForgeMirrorRow) { + t.Helper() + owner := mirrorOwner(t, ctx, st) + agent, err := st.CreateAgent(ctx, owner.ID, store.NewAgent{Handle: "mirror-agent", DisplayName: "Mirror Agent"}) + if err != nil { + t.Fatalf("CreateAgent: %v", err) + } + channel, err := st.CreateChannel(ctx, owner.ID, store.NewChannel{Kind: store.ChannelKindChannel, Name: "mirror-asks", MemberAccountIDs: []store.AccountID{owner.ID, agent.ID}}) + if err != nil { + t.Fatalf("CreateChannel: %v", err) + } + coord := store.ForgeCoord{Provider: store.ForgeProviderGitHub, Host: "github.com", Repo: "owner/repo", Number: 17} + if err := st.SetForgeMirrorSetting(ctx, owner.ID, store.ForgeMirrorSetting{Provider: coord.Provider, Host: coord.Host, Repo: coord.Repo, Mode: store.ForgeMirrorAuto}); err != nil { + t.Fatalf("SetForgeMirrorSetting: %v", err) + } + ask := &store.Ask{Ref: "owner/repo#17", RefCoord: &coord, Questions: []store.AskQuestion{{QuestionID: "q1", Question: "Ship this?", Options: []store.AskOption{{ID: "yes", Label: "Yes"}}}}} + msg, _, err := st.AppendMessage(ctx, store.Message{AuthorAccountID: agent.ID, Blocks: []store.MessageBlock{{Ask: ask}}}, string(channel.ID), store.TopicRef{Name: "decisions", Create: true}, "") + if err != nil { + t.Fatalf("AppendMessage(ask): %v", err) + } + rows, err := st.AskForgeMirrors(ctx, []string{msg.Blocks[0].Ask.AskID}) + if err != nil || len(rows) != 1 { + t.Fatalf("AskForgeMirrors: rows=%d err=%v", len(rows), err) + } + return owner, rows[0] +} + +func clickBody(context.Context, store.ForgeMirrorRow, int) (string, error) { + return "manual body", nil +} + +func openMirrorPool(t *testing.T, ctx context.Context, dsn string) *pgxpool.Pool { + t.Helper() + pool, err := pgxpool.New(ctx, dsn) + if err != nil { + t.Fatalf("pgxpool.New: %v", err) + } + t.Cleanup(pool.Close) + return pool +} + +func assertMirrorState(t *testing.T, ctx context.Context, pool *pgxpool.Pool, id string, state int16) store.ForgeMirrorRow { + t.Helper() + var actualState, bodyRev, postedRev int16 + var commentID pgtype.Int8 + var commentKey, commentURL, lastError string + var recheckAt pgtype.Timestamptz + if err := pool.QueryRow(ctx, `SELECT state, body_rev, posted_rev, comment_id, comment_key, comment_url, last_error, recheck_at FROM forge_mirrors WHERE id = $1`, id).Scan(&actualState, &bodyRev, &postedRev, &commentID, &commentKey, &commentURL, &lastError, &recheckAt); err != nil { + t.Fatalf("read mirror state: %v", err) + } + if actualState != state { + t.Fatalf("mirror state = %d, want %d", actualState, state) + } + var recheck *time.Time + if recheckAt.Valid { + recheck = &recheckAt.Time + } + row := store.ForgeMirrorRow{ID: id, State: actualState, BodyRev: bodyRev, PostedRev: postedRev, CommentKey: commentKey, CommentURL: commentURL, LastError: lastError, RecheckAt: recheck} + if commentID.Valid { + row.CommentID = uint64(commentID.Int64) + } + return row +} + +func immediateSleep(t *testing.T) { + t.Helper() + old := sleep + sleep = func(context.Context, time.Duration) error { return nil } + t.Cleanup(func() { sleep = old }) +} + +func TestDriveSourceTwoPostsManualAndStoresResult(t *testing.T) { + st, ctx, dsn := openMirrorStore(t) + pool := openMirrorPool(t, ctx, dsn) + owner := mirrorOwner(t, ctx, st) + row := claimClickRow(t, ctx, st, owner, "RIG-12") + poster := &fakePoster{results: []postResult{{comment: forge.Comment{ID: 7, Key: "created-key", URL: "https://linear.app/comment/7"}}}} + Drive(ctx, st, poster, []store.ForgeMirrorRow{row}, nil, clickBody) + + posts, comments, _ := poster.snapshot() + if len(posts) != 1 || posts[0].need != store.ForgeMirrorManual || posts[0].key != "" || posts[0].clientID != row.ClientCommentID { + t.Fatalf("posts = %+v, want one manual create with row client id", posts) + } + if len(comments) != 1 || !HasMarker(comments[0].Body, row.ID) { + t.Fatalf("comments = %+v, want one marked comment", comments) + } + stored := assertMirrorState(t, ctx, pool, row.ID, 2) + if stored.CommentKey != row.ClientCommentID || stored.CommentURL != "https://linear.app/comment/7" || stored.RecheckAt != nil { + t.Fatalf("stored row = %+v, want client comment ID, URL, and no recheck", stored) + } +} + +func TestDriveRetriesThreeFailures(t *testing.T) { + st, ctx, dsn := openMirrorStore(t) + pool := openMirrorPool(t, ctx, dsn) + owner := mirrorOwner(t, ctx, st) + row := claimClickRow(t, ctx, st, owner, "RIG-12") + poster := &fakePoster{results: []postResult{{err: errors.New("first")}, {err: errors.New("second")}, {err: errors.New("third")}}} + immediateSleep(t) + Drive(ctx, st, poster, []store.ForgeMirrorRow{row}, nil, clickBody) + posts, _, _ := poster.snapshot() + if len(posts) != 3 { + t.Fatalf("post calls = %d, want 3", len(posts)) + } + if failed := assertMirrorState(t, ctx, pool, row.ID, 3); failed.LastError != "third" { + t.Fatalf("last error = %q, want third", failed.LastError) + } +} + +func TestDriveRefusalMarksFailedWithoutRetry(t *testing.T) { + st, ctx, dsn := openMirrorStore(t) + pool := openMirrorPool(t, ctx, dsn) + owner := mirrorOwner(t, ctx, st) + row := claimClickRow(t, ctx, st, owner, "RIG-12") + poster := &fakePoster{results: []postResult{{err: fmt.Errorf("policy: %w", ErrRefused)}}} + Drive(ctx, st, poster, []store.ForgeMirrorRow{row}, nil, clickBody) + posts, _, _ := poster.snapshot() + if len(posts) != 1 { + t.Fatalf("post calls = %d, want 1", len(posts)) + } + if failed := assertMirrorState(t, ctx, pool, row.ID, 3); failed.LastError == "" { + t.Fatal("refused row has no failure reason") + } +} + +func TestDriveAdoptsLostGitHubPostWithManyLaterComments(t *testing.T) { + st, ctx, dsn := openMirrorStore(t) + pool := openMirrorPool(t, ctx, dsn) + owner := mirrorOwner(t, ctx, st) + row := claimClickRow(t, ctx, st, owner, "owner/repo#7") + poster := &fakePoster{results: []postResult{ + {comment: forge.Comment{ID: 1, Key: "first-comment", ForgeAccount: "compass-app"}, err: errors.New("response lost"), lose: true}, + {comment: forge.Comment{ID: 1, Key: "first-comment", URL: "https://github.test/comment/1", ForgeAccount: "compass-app"}}, + }} + for i := range 60 { + poster.comments = append(poster.comments, forge.Comment{ID: uint64(i + 2), Key: fmt.Sprintf("later-%d", i), Body: "later comment"}) + } + immediateSleep(t) + Drive(ctx, st, poster, []store.ForgeMirrorRow{row}, nil, clickBody) + posts, comments, _ := poster.snapshot() + if len(posts) != 2 || posts[0].key != "" || posts[1].key != "first-comment" { + t.Fatalf("posts = %+v, want lost create then edit of adopted comment", posts) + } + marked := 0 + for _, comment := range comments { + if HasMarker(comment.Body, row.ID) { + marked++ + } + } + if len(comments) != 61 || marked != 1 { + t.Fatalf("fake forge comments = %d (%d marked), want 61 comments and one marked", len(comments), marked) + } + if stored := assertMirrorState(t, ctx, pool, row.ID, 2); stored.CommentKey != "first-comment" || stored.RecheckAt == nil { + t.Fatalf("stored GitHub row = %+v, want adopted key and recheck", stored) + } +} + +func TestDriveRecoveryScanErrorDoesNotCreateDuplicate(t *testing.T) { + st, ctx, dsn := openMirrorStore(t) + pool := openMirrorPool(t, ctx, dsn) + owner := mirrorOwner(t, ctx, st) + row := claimClickRow(t, ctx, st, owner, "owner/repo#8") + _, ok, err := st.LeaseForgeMirror(ctx, row.ID, "prior-token", time.Minute) + if err != nil || !ok { + t.Fatalf("LeaseForgeMirror: ok=%v err=%v", ok, err) + } + attempt, ok, err := st.BeginForgeMirrorAttempt(ctx, row.ID, "prior-token") + if err != nil || !ok { + t.Fatalf("BeginForgeMirrorAttempt: ok=%v err=%v", ok, err) + } + if err := st.MarkForgeMirror(ctx, row.ID, "prior-token", store.ForgeMirrorMark{PostedRev: 0, State: 1}); err != nil { + t.Fatalf("MarkForgeMirror(pending): %v", err) + } + leased, ok, err := st.LeaseForgeMirror(ctx, row.ID, "recovery-token", time.Minute) + if err != nil || !ok { + t.Fatalf("LeaseForgeMirror(recovery): ok=%v err=%v", ok, err) + } + if attempt.FirstAttemptAt == nil || leased.FirstAttemptAt == nil || leased.LastAttemptAt == nil { + t.Fatalf("attempt timestamps missing: attempt=%+v leased=%+v", attempt, leased) + } + if _, err := pool.Exec(ctx, `UPDATE forge_mirrors SET lease_until = now() - interval '1 second' WHERE id = $1`, row.ID); err != nil { + t.Fatalf("expire mirror lease: %v", err) + } + immediateSleep(t) + poster := &fakePoster{listErr: errors.New("forge unavailable")} + Drive(ctx, st, poster, []store.ForgeMirrorRow{leased}, nil, clickBody) + posts, _, _ := poster.snapshot() + if len(posts) != 0 { + t.Fatalf("post calls = %d, want no create while recovery scan fails", len(posts)) + } + if got := assertMirrorState(t, ctx, pool, row.ID, 3); got.LastError == "" { + t.Fatal("failed recovery scan did not persist its error") + } +} + +func TestDriveLinearRetryReusesClientCommentID(t *testing.T) { + st, ctx, dsn := openMirrorStore(t) + pool := openMirrorPool(t, ctx, dsn) + owner := mirrorOwner(t, ctx, st) + row := claimClickRow(t, ctx, st, owner, "RIG-12") + poster := &fakePoster{results: []postResult{{err: errors.New("lost response"), lose: true}, {comment: forge.Comment{Key: "linear-comment", URL: "https://linear.app/comment/1"}}}} + immediateSleep(t) + Drive(ctx, st, poster, []store.ForgeMirrorRow{row}, nil, clickBody) + posts, comments, _ := poster.snapshot() + if len(posts) != 2 || posts[0].clientID != row.ClientCommentID || posts[1].clientID != row.ClientCommentID { + t.Fatalf("post client IDs = %+v, want same row UUID on both tries", posts) + } + if len(comments) != 1 { + t.Fatalf("fake Linear comments = %d, want one idempotent comment", len(comments)) + } + if got := assertMirrorState(t, ctx, pool, row.ID, 2); got.CommentKey != row.ClientCommentID { + t.Fatalf("stored row = %+v, want Linear client ID", got) + } +} + +func TestDriveEditsSameCommentAfterAnswer(t *testing.T) { + st, ctx, dsn := openMirrorStore(t) + pool := openMirrorPool(t, ctx, dsn) + owner, row := mirrorAsk(t, ctx, st) + poster := &fakePoster{} + link := func(channelID, topicID string) Link { + return Link{Channel: "mirror", Topic: topicID, URL: "https://compass.test/channel/" + channelID + "/topic/" + topicID} + } + Drive(ctx, st, poster, []store.ForgeMirrorRow{row}, link, nil) + posts, _, _ := poster.snapshot() + if len(posts) != 1 || posts[0].key != "" || posts[0].need != store.ForgeMirrorAuto { + t.Fatalf("initial posts = %+v, want one automatic ask post", posts) + } + if !strings.Contains(posts[0].body, "#mirror-asks › decisions") || strings.Contains(posts[0].body, string(row.Agent)) { + t.Fatalf("initial body header = %q, want stored channel and topic names", posts[0].body) + } + if !strings.Contains(posts[0].body, "Ship this?") || strings.Contains(posts[0].body, "Answers:") { + t.Fatalf("initial body = %q, want pending ask only", posts[0].body) + } + if _, _, err := st.AnswerAsk(ctx, owner.ID, row.AskID, []store.AskAnswer{{QuestionID: "q1", ChosenOptionIDs: []string{"yes"}}}); err != nil { + t.Fatalf("AnswerAsk: %v", err) + } + rows, err := st.AskForgeMirrors(ctx, []string{row.AskID}) + if err != nil || len(rows) != 1 { + t.Fatalf("AskForgeMirrors after answer: rows=%+v err=%v", rows, err) + } + Drive(ctx, st, poster, rows, link, nil) + posts, comments, _ := poster.snapshot() + if len(posts) != 2 || posts[0].key != "" || posts[1].key == "" { + t.Fatalf("posts = %+v, want create then edit of same comment", posts) + } + if !strings.Contains(posts[1].body, "Answers:") || !strings.Contains(posts[1].body, "Answered by @mirror-owner") || len(comments) != 1 { + t.Fatalf("posts=%+v comments=%+v, want answered content on the original comment", posts, comments) + } + if stored := assertMirrorState(t, ctx, pool, row.ID, 2); stored.CommentKey == "" || stored.PostedRev != 2 || stored.BodyRev != 2 { + t.Fatalf("stored row = %+v, want updated comment and revision 2", stored) + } +} + +func TestDriveConcurrentLeasesAllowOnePost(t *testing.T) { + st, ctx, _ := openMirrorStore(t) + owner := mirrorOwner(t, ctx, st) + row := claimClickRow(t, ctx, st, owner, "RIG-12") + poster := &fakePoster{entered: make(chan struct{}, 1), proceed: make(chan struct{})} + defer poster.unblock() + done := make(chan struct{}) + go func() { + Drive(ctx, st, poster, []store.ForgeMirrorRow{row}, nil, clickBody) + close(done) + }() + select { + case <-poster.entered: + case <-time.After(10 * time.Second): + t.Fatal("first Drive did not reach poster") + } + Drive(ctx, st, poster, []store.ForgeMirrorRow{row}, nil, clickBody) + poster.unblock() + select { + case <-done: + case <-time.After(10 * time.Second): + t.Fatal("first Drive did not finish") + } + posts, _, _ := poster.snapshot() + if len(posts) != 1 { + t.Fatalf("post calls = %d, want one lease winner", len(posts)) + } +} + +func TestDriveExpiredLeaseAdoptsMarker(t *testing.T) { + st, ctx, dsn := openMirrorStore(t) + pool := openMirrorPool(t, ctx, dsn) + owner := mirrorOwner(t, ctx, st) + row := claimClickRow(t, ctx, st, owner, "owner/repo#9") + _, ok, err := st.LeaseForgeMirror(ctx, row.ID, "expired-token", time.Minute) + if err != nil || !ok { + t.Fatalf("LeaseForgeMirror: ok=%v err=%v", ok, err) + } + if _, ok, err := st.BeginForgeMirrorAttempt(ctx, row.ID, "expired-token"); err != nil || !ok { + t.Fatalf("BeginForgeMirrorAttempt: ok=%v err=%v", ok, err) + } + if _, err := pool.Exec(ctx, `UPDATE forge_mirrors SET lease_until = now() - interval '1 second', first_attempt_at = now() - interval '3 minutes', last_attempt_at = now() - interval '3 minutes' WHERE id = $1`, row.ID); err != nil { + t.Fatalf("expire mirror lease: %v", err) + } + poster := &fakePoster{comments: []forge.Comment{{ID: 9, Key: "adopt-me", ForgeAccount: "compass-app", Body: WithMarker("old body", row.ID)}}} + immediateSleep(t) + Drive(ctx, st, poster, []store.ForgeMirrorRow{row}, nil, clickBody) + posts, _, _ := poster.snapshot() + if len(posts) != 1 || posts[0].key != "adopt-me" { + t.Fatalf("posts = %+v, want edit adopted after lease expiry", posts) + } + if got := assertMirrorState(t, ctx, pool, row.ID, 2); got.CommentKey != "adopt-me" { + t.Fatalf("stored row = %+v, want adopted key", got) + } +} + +func TestDriveStaleTokenMarkWritesNothing(t *testing.T) { + st, ctx, dsn := openMirrorStore(t) + pool := openMirrorPool(t, ctx, dsn) + owner := mirrorOwner(t, ctx, st) + row := claimClickRow(t, ctx, st, owner, "RIG-12") + leased, ok, err := st.LeaseForgeMirror(ctx, row.ID, "current-token", time.Minute) + if err != nil || !ok { + t.Fatalf("LeaseForgeMirror: ok=%v err=%v", ok, err) + } + if _, ok, err := st.BeginForgeMirrorAttempt(ctx, row.ID, "current-token"); err != nil || !ok { + t.Fatalf("BeginForgeMirrorAttempt: ok=%v err=%v", ok, err) + } + if err := st.MarkForgeMirror(ctx, row.ID, "stale-token", store.ForgeMirrorMark{PostedRev: 1, State: 2, CommentKey: "wrong"}); !errors.Is(err, store.ErrLeaseLost) { + t.Fatalf("MarkForgeMirror(stale) = %v, want ErrLeaseLost", err) + } + stored := assertMirrorState(t, ctx, pool, row.ID, 1) + var token string + if err := pool.QueryRow(ctx, `SELECT lease_token FROM forge_mirrors WHERE id = $1`, row.ID).Scan(&token); err != nil { + t.Fatalf("read lease token: %v", err) + } + if stored.CommentKey != "" || stored.PostedRev != 0 || token != leased.LeaseToken { + t.Fatalf("stale mark changed row: row=%+v token=%q want lease=%q", stored, token, leased.LeaseToken) + } +} + +func TestDriveRecheckDeletesNewerDuplicateAndKeepsOldest(t *testing.T) { + st, ctx, dsn := openMirrorStore(t) + pool := openMirrorPool(t, ctx, dsn) + owner := mirrorOwner(t, ctx, st) + row := claimClickRow(t, ctx, st, owner, "owner/repo#10") + leased, ok, err := st.LeaseForgeMirror(ctx, row.ID, "recheck-token", time.Minute) + if err != nil || !ok { + t.Fatalf("LeaseForgeMirror: ok=%v err=%v", ok, err) + } + if err := st.MarkForgeMirror(ctx, row.ID, "recheck-token", store.ForgeMirrorMark{PostedRev: 1, State: 2, CommentID: 10, CommentKey: "newer", URL: "new-url", Recheck: true}); err != nil { + t.Fatalf("MarkForgeMirror(recheck): %v", err) + } + if _, err := pool.Exec(ctx, `UPDATE forge_mirrors SET recheck_at = now() - interval '1 second' WHERE id = $1`, row.ID); err != nil { + t.Fatalf("expire recheck: %v", err) + } + leased.RecheckAt = new(time.Time) + *leased.RecheckAt = time.Now().Add(-time.Second) + poster := &fakePoster{comments: []forge.Comment{ + {ID: 8, Key: "oldest", ForgeAccount: "compass-app", URL: "old-url", Body: WithMarker("old", row.ID)}, + {ID: 10, Key: "newer", ForgeAccount: "compass-app", URL: "new-url", Body: WithMarker("new", row.ID)}, + }} + Drive(ctx, st, poster, []store.ForgeMirrorRow{leased}, nil, clickBody) + _, comments, deleted := poster.snapshot() + if len(deleted) != 1 || deleted[0] != "newer" || len(comments) != 1 || comments[0].Key != "oldest" || comments[0].Body != WithMarker("manual body", row.ID) { + t.Fatalf("deleted=%v comments=%+v, want delete newer and edit retained oldest comment", deleted, comments) + } + stored := assertMirrorState(t, ctx, pool, row.ID, 2) + if stored.PostedRev != 1 || stored.CommentKey != "oldest" || stored.CommentURL != "old-url" || stored.RecheckAt != nil { + t.Fatalf("repointed row = %+v, want oldest comment pointer and original posted revision", stored) + } +} + +func TestDriveCertainGitHubPostDoesNotScheduleRecheck(t *testing.T) { + st, ctx, dsn := openMirrorStore(t) + pool := openMirrorPool(t, ctx, dsn) + owner := mirrorOwner(t, ctx, st) + row := claimClickRow(t, ctx, st, owner, "owner/repo#11") + poster := &fakePoster{} + Drive(ctx, st, poster, []store.ForgeMirrorRow{row}, nil, clickBody) + posts, _, _ := poster.snapshot() + if len(posts) != 1 { + t.Fatalf("post calls = %d, want 1", len(posts)) + } + if stored := assertMirrorState(t, ctx, pool, row.ID, 2); stored.RecheckAt != nil { + t.Fatalf("certain GitHub post scheduled recheck: %+v", stored) + } +} + +func TestDriveForeignMarkerCommentIsNotAdoptedOrDeleted(t *testing.T) { + st, ctx, dsn := openMirrorStore(t) + pool := openMirrorPool(t, ctx, dsn) + owner := mirrorOwner(t, ctx, st) + row := claimClickRow(t, ctx, st, owner, "owner/repo#12") + _, ok, err := st.LeaseForgeMirror(ctx, row.ID, "foreign-token", time.Minute) + if err != nil || !ok { + t.Fatalf("LeaseForgeMirror: ok=%v err=%v", ok, err) + } + if _, ok, err := st.BeginForgeMirrorAttempt(ctx, row.ID, "foreign-token"); err != nil || !ok { + t.Fatalf("BeginForgeMirrorAttempt: ok=%v err=%v", ok, err) + } + if _, err := pool.Exec(ctx, `UPDATE forge_mirrors SET lease_until = now() - interval '1 second', first_attempt_at = now() - interval '3 minutes', last_attempt_at = now() - interval '3 minutes' WHERE id = $1`, row.ID); err != nil { + t.Fatalf("expire mirror lease: %v", err) + } + foreign := forge.Comment{ID: 4, Key: "foreign-marker", ForgeAccount: "person", Body: WithMarker("copied", row.ID)} + poster := &fakePoster{comments: []forge.Comment{foreign}} + immediateSleep(t) + Drive(ctx, st, poster, []store.ForgeMirrorRow{row}, nil, clickBody) + posts, comments, deleted := poster.snapshot() + if len(posts) != 1 || len(deleted) != 0 || len(comments) != 2 { + t.Fatalf("posts=%+v deleted=%v comments=%+v, want foreign comment preserved and a new owned comment", posts, deleted, comments) + } + if stored := assertMirrorState(t, ctx, pool, row.ID, 2); stored.CommentKey == foreign.Key { + t.Fatalf("adopted foreign comment: %+v", stored) + } +} + +func TestDriveRevisionBumpBetweenRenderAndAttemptRemainsPending(t *testing.T) { + st, ctx, dsn := openMirrorStore(t) + pool := openMirrorPool(t, ctx, dsn) + owner := mirrorOwner(t, ctx, st) + row := claimClickRow(t, ctx, st, owner, "owner/repo#14") + leased, ok, err := st.LeaseForgeMirror(ctx, row.ID, "render-race", time.Minute) + if err != nil || !ok { + t.Fatalf("LeaseForgeMirror: ok=%v err=%v", ok, err) + } + ready := make(chan struct{}) + continueRender := make(chan struct{}) + bodyFn := func(ctx context.Context, row store.ForgeMirrorRow, _ int) (string, error) { + body := fmt.Sprintf("body rendered at revision %d", row.BodyRev) + close(ready) + select { + case <-continueRender: + case <-ctx.Done(): + return "", ctx.Err() + } + return body, nil + } + poster := &fakePoster{} + done := make(chan error, 1) + go func() { + done <- processLeasedRow(ctx, st, poster, leased, "render-race", nil, bodyFn, slog.Default()) + }() + select { + case <-ready: + case <-time.After(10 * time.Second): + close(continueRender) + t.Fatal("Drive did not finish rendering the body") + } + if _, _, err := st.ClaimForgeMirror(ctx, store.ForgeMirrorRow{Source: 2, SourceKey: row.SourceKey, Actor: owner.ID, TargetRef: row.TargetRef, BodyRev: 1, State: 1}); err != nil { + close(continueRender) + t.Fatalf("ClaimForgeMirror(revision bump): %v", err) + } + close(continueRender) + select { + case err := <-done: + if err != nil { + t.Fatalf("processLeasedRow: %v", err) + } + case <-time.After(10 * time.Second): + t.Fatal("Drive did not finish after render resumed") + } + stored := assertMirrorState(t, ctx, pool, row.ID, 1) + if stored.BodyRev != 2 || stored.PostedRev != 1 { + t.Fatalf("mirror revisions = (%d, %d), want (2, 1) pending", stored.BodyRev, stored.PostedRev) + } + posts, _, _ := poster.snapshot() + if len(posts) != 1 || !strings.Contains(posts[0].body, "body rendered at revision 1") { + t.Fatalf("posts = %+v, want the original revision-1 body", posts) + } +} + +func TestDriveOneRedrivesAskAnsweredDuringPost(t *testing.T) { + st, ctx, dsn := openMirrorStore(t) + pool := openMirrorPool(t, ctx, dsn) + owner, row := mirrorAsk(t, ctx, st) + poster := &fakePoster{entered: make(chan struct{}, 1), proceed: make(chan struct{})} + done := make(chan struct{}) + var final store.ForgeMirrorRow + go func() { + final, _ = DriveOne(ctx, st, poster, row, nil, nil) + close(done) + }() + select { + case <-poster.entered: + case <-time.After(10 * time.Second): + poster.unblock() + t.Fatal("DriveOne did not reach the initial post") + } + if _, _, err := st.AnswerAsk(ctx, owner.ID, row.AskID, []store.AskAnswer{{QuestionID: "q1", ChosenOptionIDs: []string{"yes"}}}); err != nil { + poster.unblock() + t.Fatalf("AnswerAsk during post: %v", err) + } + poster.unblock() + select { + case <-done: + case <-time.After(10 * time.Second): + t.Fatal("DriveOne did not finish after post release") + } + if final.State != 2 || final.BodyRev != 2 || final.PostedRev != 2 { + t.Fatalf("final row = %+v, want revision 2 delivered", final) + } + posts, comments, _ := poster.snapshot() + if len(posts) != 2 || !strings.Contains(posts[1].body, "Answers:") || len(comments) != 1 { + t.Fatalf("posts=%+v comments=%+v, want answer edit of the same comment", posts, comments) + } + stored := assertMirrorState(t, ctx, pool, row.ID, 2) + if stored.BodyRev != 2 || stored.PostedRev != 2 { + t.Fatalf("stored revisions = (%d, %d), want (2, 2)", stored.BodyRev, stored.PostedRev) + } +} + +func TestDriveRecheckRepointRedeliversCurrentBodyAndDeletesDuplicate(t *testing.T) { + st, ctx, dsn := openMirrorStore(t) + pool := openMirrorPool(t, ctx, dsn) + owner := mirrorOwner(t, ctx, st) + row := claimClickRow(t, ctx, st, owner, "owner/repo#13") + if _, ok, err := st.LeaseForgeMirror(ctx, row.ID, "repoint-token", time.Minute); err != nil || !ok { + t.Fatalf("LeaseForgeMirror: ok=%v err=%v", ok, err) + } + if err := st.MarkForgeMirror(ctx, row.ID, "repoint-token", store.ForgeMirrorMark{PostedRev: 2, State: 2, CommentID: 11, CommentKey: "newer", URL: "new-url", Recheck: true}); err != nil { + t.Fatalf("MarkForgeMirror(recheck): %v", err) + } + if _, err := pool.Exec(ctx, `UPDATE forge_mirrors SET recheck_at = now() - interval '1 second', body_rev = 2 WHERE id = $1`, row.ID); err != nil { + t.Fatalf("expire recheck: %v", err) + } + leased, err := st.ForgeMirrorByID(ctx, row.ID) + if err != nil { + t.Fatalf("ForgeMirrorByID(due recheck): %v", err) + } + poster := &fakePoster{comments: []forge.Comment{ + {ID: 9, Key: "oldest", ForgeAccount: "compass-app", URL: "old-url", Body: WithMarker("old body", row.ID)}, + {ID: 11, Key: "newer", ForgeAccount: "compass-app", URL: "new-url", Body: WithMarker("new body", row.ID)}, + }} + Drive(ctx, st, poster, []store.ForgeMirrorRow{leased}, nil, clickBody) + posts, comments, deleted := poster.snapshot() + if len(posts) != 1 || posts[0].key != "oldest" || len(deleted) != 1 || deleted[0] != "newer" || len(comments) != 1 || comments[0].Body != WithMarker("manual body", row.ID) { + t.Fatalf("posts=%+v deleted=%v comments=%+v, want oldest current body and duplicate removal", posts, deleted, comments) + } + if stored := assertMirrorState(t, ctx, pool, row.ID, 2); stored.CommentKey != "oldest" || stored.PostedRev != 2 || stored.RecheckAt != nil { + t.Fatalf("recheck delivery = %+v, want oldest current body and cleared recheck", stored) + } +} + +func TestDriveRedrivesAfterRevisionBumpDuringPost(t *testing.T) { + st, ctx, dsn := openMirrorStore(t) + pool := openMirrorPool(t, ctx, dsn) + owner := mirrorOwner(t, ctx, st) + row := claimClickRow(t, ctx, st, owner, "owner/repo#14") + poster := &fakePoster{entered: make(chan struct{}, 1), proceed: make(chan struct{})} + done := make(chan struct{}) + go func() { + Drive(ctx, st, poster, []store.ForgeMirrorRow{row}, nil, clickBody) + close(done) + }() + select { + case <-poster.entered: + case <-time.After(10 * time.Second): + poster.unblock() + t.Fatal("Drive did not reach the poster") + } + if _, _, err := st.ClaimForgeMirror(ctx, store.ForgeMirrorRow{Source: 2, SourceKey: row.SourceKey, Actor: owner.ID, TargetRef: row.TargetRef, BodyRev: 1, State: 1}); err != nil { + poster.unblock() + t.Fatalf("ClaimForgeMirror(revision bump): %v", err) + } + poster.unblock() + select { + case <-done: + case <-time.After(10 * time.Second): + t.Fatal("Drive did not finish after poster release") + } + stored := assertMirrorState(t, ctx, pool, row.ID, 2) + if stored.BodyRev != 2 || stored.PostedRev != 2 { + t.Fatalf("mirror revisions = (%d, %d), want (2, 2) after same-call redrive", stored.BodyRev, stored.PostedRev) + } + posts, _, _ := poster.snapshot() + if len(posts) != 2 { + t.Fatalf("post calls = %d, want initial delivery and redrive", len(posts)) + } +} + +func TestDriveOneRedrivesAfterFailedPassWithRevisionBump(t *testing.T) { + st, ctx, dsn := openMirrorStore(t) + pool := openMirrorPool(t, ctx, dsn) + owner := mirrorOwner(t, ctx, st) + row := claimClickRow(t, ctx, st, owner, "owner/repo#20") + poster := &fakePoster{results: []postResult{{err: errors.New("first")}, {err: errors.New("second")}, {err: errors.New("third")}, {comment: forge.Comment{ID: 20, Key: "recovered", URL: "https://forge.test/comment/20"}}}} + poster.onPost = func(call int) { + if call != 1 { + return + } + if _, _, err := st.ClaimForgeMirror(ctx, store.ForgeMirrorRow{Source: 2, SourceKey: row.SourceKey, Actor: owner.ID, TargetRef: row.TargetRef, BodyRev: 1, State: 1}); err != nil { + t.Errorf("ClaimForgeMirror(revision bump during failed post): %v", err) + } + } + immediateSleep(t) + got, err := DriveOne(ctx, st, poster, row, nil, func(_ context.Context, row store.ForgeMirrorRow, _ int) (string, error) { + return fmt.Sprintf("body at rev %d", row.BodyRev), nil + }) + if err != nil { + t.Fatalf("DriveOne: %v", err) + } + posts, _, _ := poster.snapshot() + if got.State != 2 || got.BodyRev != 2 || got.PostedRev != 2 || len(posts) != 4 || !strings.Contains(posts[0].body, "rev 1") || !strings.Contains(posts[3].body, "rev 2") { + t.Fatalf("DriveOne result=%+v posts=%+v; want failed rev-1 pass followed by delivered rev-2 pass", got, posts) + } + stored := assertMirrorState(t, ctx, pool, row.ID, 2) + if stored.BodyRev != 2 || stored.PostedRev != 2 { + t.Fatalf("stored revisions = (%d, %d), want (2, 2)", stored.BodyRev, stored.PostedRev) + } +} + +func TestDriveOneRedrivesAcrossFourRevisionBumps(t *testing.T) { + st, ctx, dsn := openMirrorStore(t) + pool := openMirrorPool(t, ctx, dsn) + owner := mirrorOwner(t, ctx, st) + row := claimClickRow(t, ctx, st, owner, "owner/repo#21") + poster := &fakePoster{} + poster.onPost = func(call int) { + if call > 4 { + return + } + if _, _, err := st.ClaimForgeMirror(ctx, store.ForgeMirrorRow{Source: 2, SourceKey: row.SourceKey, Actor: owner.ID, TargetRef: row.TargetRef, BodyRev: 1, State: 1}); err != nil { + t.Errorf("ClaimForgeMirror(revision bump %d): %v", call, err) + } + } + immediateSleep(t) + got, err := DriveOne(ctx, st, poster, row, nil, func(_ context.Context, row store.ForgeMirrorRow, _ int) (string, error) { + return fmt.Sprintf("body at rev %d", row.BodyRev), nil + }) + if err != nil { + t.Fatalf("DriveOne: %v", err) + } + posts, _, _ := poster.snapshot() + if got.State != 2 || got.BodyRev != 5 || got.PostedRev != 5 || len(posts) != 5 { + t.Fatalf("DriveOne result=%+v post count=%d; want revision 5 delivered after four bumps", got, len(posts)) + } + for i, post := range posts { + if !strings.Contains(post.body, fmt.Sprintf("rev %d", i+1)) { + t.Errorf("post %d body = %q, want revision %d", i+1, post.body, i+1) + } + } + stored := assertMirrorState(t, ctx, pool, row.ID, 2) + if stored.BodyRev != 5 || stored.PostedRev != 5 { + t.Fatalf("stored revisions = (%d, %d), want (5, 5)", stored.BodyRev, stored.PostedRev) + } +} + +func TestDriveOneRedrivesAfterRefusalRacesRevisionBump(t *testing.T) { + st, ctx, dsn := openMirrorStore(t) + pool := openMirrorPool(t, ctx, dsn) + owner := mirrorOwner(t, ctx, st) + row := claimClickRow(t, ctx, st, owner, "owner/repo#22") + poster := &fakePoster{results: []postResult{{err: ErrRefused}}} + poster.onPost = func(call int) { + if call != 1 { + return + } + if _, _, err := st.ClaimForgeMirror(ctx, store.ForgeMirrorRow{Source: 2, SourceKey: row.SourceKey, Actor: owner.ID, TargetRef: row.TargetRef, BodyRev: 1, State: 1}); err != nil { + t.Errorf("ClaimForgeMirror(revision bump during refusal): %v", err) + } + } + immediateSleep(t) + got, err := DriveOne(ctx, st, poster, row, nil, func(_ context.Context, row store.ForgeMirrorRow, _ int) (string, error) { + return fmt.Sprintf("body at rev %d", row.BodyRev), nil + }) + if err != nil || got.BodyRev != 2 || got.PostedRev != 2 || got.State != 2 { + t.Fatalf("DriveOne result=(%+v, %v), want bumped revision 2 delivered after refusing rev 1", got, err) + } + posts, _, _ := poster.snapshot() + if len(posts) != 2 || !strings.Contains(posts[0].body, "rev 1") || !strings.Contains(posts[1].body, "rev 2") { + t.Fatalf("posts = %+v, want one refused rev-1 attempt and one rev-2 delivery", posts) + } + stored := assertMirrorState(t, ctx, pool, row.ID, 2) + if stored.BodyRev != 2 || stored.PostedRev != 2 { + t.Fatalf("stored revisions = (%d, %d), want (2, 2)", stored.BodyRev, stored.PostedRev) + } +} + +func TestDrivePreservesScheduledRecheckAcrossLaterEdit(t *testing.T) { + st, ctx, dsn := openMirrorStore(t) + pool := openMirrorPool(t, ctx, dsn) + owner := mirrorOwner(t, ctx, st) + row := claimClickRow(t, ctx, st, owner, "owner/repo#15") + if _, ok, err := st.LeaseForgeMirror(ctx, row.ID, "schedule-recheck", time.Minute); err != nil || !ok { + t.Fatalf("LeaseForgeMirror = (ok=%v, err=%v)", ok, err) + } + if err := st.MarkForgeMirror(ctx, row.ID, "schedule-recheck", store.ForgeMirrorMark{PostedRev: 1, State: 2, CommentID: 15, CommentKey: "comment", Recheck: true}); err != nil { + t.Fatalf("MarkForgeMirror(schedule): %v", err) + } + updated, created, err := st.ClaimForgeMirror(ctx, store.ForgeMirrorRow{Source: 2, SourceKey: row.SourceKey, Actor: owner.ID, TargetRef: row.TargetRef, BodyRev: 1, State: 1}) + if err != nil || created || updated.BodyRev != 2 || updated.State != 1 { + t.Fatalf("ClaimForgeMirror(revision bump) = (%+v, %v, %v), want existing pending rev 2", updated, created, err) + } + poster := &fakePoster{comments: []forge.Comment{{ID: 15, Key: "comment", ForgeAccount: "compass-app", Body: WithMarker("old", row.ID)}}} + Drive(ctx, st, poster, []store.ForgeMirrorRow{updated}, nil, clickBody) + if stored := assertMirrorState(t, ctx, pool, row.ID, 2); stored.RecheckAt == nil { + t.Fatal("later edit cleared scheduled recheck") + } +} + +func TestDriveRecheckClearsRecheckAndReeditsRepointedComment(t *testing.T) { + st, ctx, dsn := openMirrorStore(t) + pool := openMirrorPool(t, ctx, dsn) + owner := mirrorOwner(t, ctx, st) + row := claimClickRow(t, ctx, st, owner, "owner/repo#16") + if _, ok, err := st.LeaseForgeMirror(ctx, row.ID, "recheck-edit", time.Minute); err != nil || !ok { + t.Fatalf("LeaseForgeMirror = (ok=%v, err=%v)", ok, err) + } + if err := st.MarkForgeMirror(ctx, row.ID, "recheck-edit", store.ForgeMirrorMark{PostedRev: 2, State: 2, CommentID: 11, CommentKey: "newer", URL: "new-url", Recheck: true}); err != nil { + t.Fatalf("MarkForgeMirror(schedule): %v", err) + } + if _, err := pool.Exec(ctx, `UPDATE forge_mirrors SET recheck_at = now() - interval '1 second', body_rev = 2, posted_rev = 2 WHERE id = $1`, row.ID); err != nil { + t.Fatalf("prepare stale-body recheck: %v", err) + } + leased, err := st.ForgeMirrorByID(ctx, row.ID) + if err != nil { + t.Fatalf("ForgeMirrorByID(due recheck): %v", err) + } + poster := &fakePoster{comments: []forge.Comment{ + {ID: 9, Key: "oldest", ForgeAccount: "compass-app", URL: "old-url", Body: WithMarker("old body", row.ID)}, + {ID: 11, Key: "newer", ForgeAccount: "compass-app", URL: "new-url", Body: WithMarker("new body", row.ID)}, + }} + Drive(ctx, st, poster, []store.ForgeMirrorRow{leased}, nil, clickBody) + if got := assertMirrorState(t, ctx, pool, row.ID, 2); got.PostedRev != 2 || got.CommentKey != "oldest" || got.RecheckAt != nil { + t.Fatalf("recheck result = %+v, want oldest comment redelivered and recheck cleared", got) + } + posts, comments, deleted := poster.snapshot() + if len(posts) != 1 || posts[0].key != "oldest" || len(deleted) != 1 || deleted[0] != "newer" || len(comments) != 1 || comments[0].Body != WithMarker("manual body", row.ID) { + t.Fatalf("posts=%+v deleted=%v comments=%+v, want oldest current body and duplicate removal", posts, deleted, comments) + } + if stored := assertMirrorState(t, ctx, pool, row.ID, 2); stored.CommentKey != "oldest" || stored.PostedRev != 2 || stored.RecheckAt != nil { + t.Fatalf("recheck delivery = %+v, want oldest current body and cleared recheck", stored) + } +} diff --git a/go/internal/mirror/render.go b/go/internal/mirror/render.go new file mode 100644 index 000000000..d1a2cd720 --- /dev/null +++ b/go/internal/mirror/render.go @@ -0,0 +1,265 @@ +package mirror + +import ( + "fmt" + "slices" + "strings" + "unicode/utf8" + + "github.com/RigelBuild/compass/go/internal/store" +) + +// Link names the Compass source and its optional public URL. +type Link struct { + Channel string + Topic string + URL string +} + +// RenderAsk formats one ask and its recorded answer, when present. +func RenderAsk(ask store.Ask, answeredBy string, link Link) string { + lines := make([]string, 1, 1+len(ask.Questions)*3) + lines[0] = header(link) + lines = append(lines, askLines(ask, answeredBy)...) + return strings.Join(lines, "\n") +} + +func truncateUTF8(text string, limit int) string { + if limit <= 0 { + return "" + } + if len(text) <= limit { + return text + } + end := limit + for end > 0 && !utf8.RuneStart(text[end]) { + end-- + } + return text[:end] +} + +// Render formats the source header and message history within a byte limit. +// Callers pass the limit net of any owner header; the driver reserves the row marker. +// renderAskWithinLimit keeps the source header and newest ask details in budget. +func renderAskWithinLimit(ask store.Ask, answeredBy string, link Link, limit int) string { + head := header(link) + body := askLines(ask, answeredBy) + if limit <= 0 { + return strings.Join(append([]string{head}, body...), "\n") + } + if len(head) >= limit { + return truncateUTF8(head, limit) + } + used := len(head) + kept := make([]string, 0, len(body)) + for _, line := range slices.Backward(body) { + available := limit - used - 1 + if available <= 0 { + break + } + if len(line) > available { + kept = append(kept, truncateUTF8(line, available)) + break + } + kept = append(kept, line) + used += len(line) + 1 + } + result := []string{head} + for _, line := range slices.Backward(kept) { + result = append(result, line) + } + return strings.Join(result, "\n") +} + +func Render(msgs []store.Message, authors map[store.AccountID]string, link Link, note string, limit int) string { + prefix := []string{header(link)} + if note != "" { + prefix = append(prefix, neutralize(note)) + } + if limit <= 0 { + lines := append([]string(nil), prefix...) + for _, msg := range msgs { + lines = append(lines, renderMessage(msg, authors)) + } + return strings.Join(lines, "\n\n") + } + if renderedLength(prefix) > limit { + if len(prefix) == 2 && limit > len(prefix[0])+2 { + prefix[1] = truncateUTF8(prefix[1], limit-len(prefix[0])-2) + } else { + prefix = []string{truncateUTF8(prefix[0], limit)} + } + if renderedLength(prefix) > limit { + return truncateUTF8(prefix[0], limit) + } + return strings.Join(prefix, "\n\n") + } + if len(msgs) == 0 { + return strings.Join(prefix, "\n\n") + } + + messageBodies := make([]string, len(msgs)) + for i, msg := range msgs { + messageBodies[i] = renderMessage(msg, authors) + } + baseLength := renderedLength(prefix) + suffixLength := 0 + var best []string + for kept := 1; kept <= len(messageBodies); kept++ { + message := messageBodies[len(messageBodies)-kept] + suffixLength += len(message) + if kept > 1 { + suffixLength += 2 + } + dropped := len(messageBodies) - kept + dropLine := "" + dropLength := 0 + if dropped > 0 { + dropLine = fmt.Sprintf("Dropped %d older messages.", dropped) + dropLength = len(dropLine) + 2 + } + if baseLength+dropLength+suffixLength <= limit { + best = make([]string, 0, len(prefix)+1+kept) + best = append(best, prefix...) + if dropLine != "" { + best = append(best, dropLine) + } + best = append(best, messageBodies[len(messageBodies)-kept:]...) + } + } + if best != nil { + return strings.Join(best, "\n\n") + } + + dropped := len(messageBodies) - 1 + lines := append([]string(nil), prefix...) + used := baseLength + if dropped > 0 { + dropLine := fmt.Sprintf("Dropped %d older messages.", dropped) + if used+2+len(dropLine) < limit { + lines = append(lines, dropLine) + used += len(dropLine) + 2 + } + } + if used < limit { + available := limit - used + if len(lines) > 0 { + available -= 2 + } + if available > 0 { + lines = append(lines, truncateUTF8(messageBodies[len(messageBodies)-1], available)) + } + } + return strings.Join(lines, "\n\n") +} + +func renderedLength(lines []string) int { + if len(lines) == 0 { + return 0 + } + length := (len(lines) - 1) * 2 + for _, line := range lines { + length += len(line) + } + return length +} + +// WithMarker appends the row marker after the rendered body. +func WithMarker(body, rowID string) string { + return strings.TrimRight(body, "\n") + "\n\n" +} + +// HasMarker reports whether body contains the row marker. +func HasMarker(body, rowID string) bool { + return strings.Contains(body, "") +} + +func header(link Link) string { + label := "#" + neutralize(link.Channel) + " › " + neutralize(link.Topic) + if link.URL == "" { + return label + } + return "[" + label + "](" + link.URL + ")" +} + +func askLines(ask store.Ask, answeredBy string) []string { + var lines []string + for _, question := range ask.Questions { + if question.Header != "" { + lines = append(lines, "### "+neutralize(question.Header)) + } + lines = append(lines, neutralize(question.Question)) + for index, option := range question.Options { + label := neutralize(option.Label) + if question.Recommended != nil && int(*question.Recommended) == index { + label += " (recommended)" + } + lines = append(lines, "- "+label) + } + } + if !ask.Answered { + return lines + } + + var answers []string + allSkipped := true + for _, question := range ask.Questions { + var selected []string + for _, chosenID := range question.ChosenOptionIDs { + for _, option := range question.Options { + if option.ID == chosenID { + selected = append(selected, neutralize(option.Label)) + allSkipped = false + break + } + } + } + if question.CustomText != "" { + selected = append(selected, neutralize(question.CustomText)) + allSkipped = false + } + if len(selected) > 0 { + answers = append(answers, "- "+neutralize(question.Question)+": "+strings.Join(selected, "; ")) + } + } + if allSkipped { + lines = append(lines, "Skipped.") + } else { + lines = append(lines, "Answers:") + lines = append(lines, answers...) + } + lines = append(lines, "Answered by @"+neutralize(answeredBy)) + return lines +} + +func renderMessage(msg store.Message, authors map[store.AccountID]string) string { + author := authors[msg.AuthorAccountID] + if author == "" { + author = string(msg.AuthorAccountID) + } + lines := []string{fmt.Sprintf("> @%s · %s", neutralize(author), msg.At.UTC().Format("2006-01-02T15:04Z"))} + for _, block := range msg.Blocks { + switch { + case block.Text != nil: + lines = appendQuoted(lines, neutralize(*block.Text)) + case block.Ask != nil: + lines = appendQuoted(lines, strings.Join(askLines(*block.Ask, ""), "\n")) + case block.AskAnswer != nil: + lines = appendQuoted(lines, strings.Join(askLines(block.AskAnswer.Ask, author), "\n")) + } + } + return strings.Join(lines, "\n") +} + +func appendQuoted(lines []string, text string) []string { + for line := range strings.SplitSeq(text, "\n") { + lines = append(lines, "> "+line) + } + return lines +} + +// neutralize escapes quoted mirror markers so a quoted body cannot be adopted as +// another row's comment. Forged owner headers are left to forge.StampOwner's strip. +func neutralize(text string) string { + return strings.ReplaceAll(text, " after" + got := Render([]store.Message{{ + AuthorAccountID: "agent", At: time.Unix(0, 0), Blocks: []store.MessageBlock{{Text: new(text)}}, + }}, map[store.AccountID]string{"agent": "agent"}, Link{Channel: "c", Topic: "t"}, "", 0) + if strings.Contains(got, "") { + t.Fatalf("WithMarker() = %q, want marker after two newlines", body) + } + if !HasMarker(body, "row-123") || HasMarker(body, "other-row") { + t.Fatalf("HasMarker() = %v for matching ID and %v for other ID, want true/false", HasMarker(body, "row-123"), HasMarker(body, "other-row")) + } +} + +func TestRenderRespectsFinalBodyByteBudget(t *testing.T) { + t.Run("header and note exceed budget", func(t *testing.T) { + limit := 6 + body := Render(nil, nil, Link{Channel: "channel", Topic: "topic"}, "a note too long", limit) + if len(body) > limit { + t.Fatalf("Render() length = %d, want <= %d: %q", len(body), limit, body) + } + }) + + t.Run("single oversized newest message is retained in part", func(t *testing.T) { + message := store.Message{ + AuthorAccountID: "agent", + At: time.Unix(0, 0), + Blocks: []store.MessageBlock{{Text: new("oversized newest content")}}, + } + fullMessage := renderMessage(message, map[store.AccountID]string{"agent": "agent"}) + prefix := "#c › t\n\n" + limit := len(prefix) + len(fullMessage) - 4 + body := Render([]store.Message{message}, map[store.AccountID]string{"agent": "agent"}, Link{Channel: "c", Topic: "t"}, "", limit) + if len(body) > limit { + t.Fatalf("Render() length = %d, exceeds limit %d", len(body), limit) + } + if !strings.Contains(body, "> oversi") || strings.Contains(body, "Dropped 1 older messages.") { + t.Fatalf("Render() = %q, want truncated newest message", body) + } + }) + + t.Run("marker fits within supplied net limit", func(t *testing.T) { + marker := "\n\n" + limit := 64 + body := Render(nil, nil, Link{Channel: "a", Topic: "b"}, "a sufficiently long note", limit-len(marker)) + final := WithMarker(body, "row") + if len(final) > limit { + t.Fatalf("marked body length = %d, want <= %d: %q", len(final), limit, final) + } + }) +} diff --git a/go/internal/store/db/forge_mirror_ask.sql.go b/go/internal/store/db/forge_mirror_ask.sql.go new file mode 100644 index 000000000..70fb380ee --- /dev/null +++ b/go/internal/store/db/forge_mirror_ask.sql.go @@ -0,0 +1,102 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: forge_mirror_ask.sql + +package db + +import ( + "context" +) + +const askForForgeMirror = `-- name: AskForForgeMirror :one +WITH RECURSIVE chain AS ( + SELECT aa.account_id, aa.parent_agent_id + FROM agent_accounts aa + WHERE aa.account_id = $2 + UNION + SELECT a.account_id, a.parent_agent_id + FROM agent_accounts a + JOIN chain ch ON a.account_id = ch.parent_agent_id +), participating AS ( + SELECT cm.channel_id FROM channel_members cm WHERE cm.account_id = $2 + UNION + SELECT c.id FROM channels c + WHERE c.membership_mode = 1 + AND ( + c.parent_agent_id IN (SELECT account_id FROM chain) + OR c.parent_agent_id IN ( + SELECT aa.account_id FROM agent_accounts aa WHERE aa.owner_user_id = $2 + ) + ) +) +SELECT m.id, m.topic_id, m.author_account_id, + (CASE WHEN ah.owner_user_id IS NULL THEN COALESCE(ah.handle, '') + WHEN oh.handle IS NULL THEN '' ELSE oh.handle || '/' || ah.handle END)::text AS author_handle, + m.at_unix_ms, m.blocks, m.turn_sequence, t.channel_id, c.name AS channel_name, + t.name AS topic_name, COALESCE(answer.author_handle, '')::text AS answered_by + FROM messages m + LEFT JOIN account_handles ah ON ah.account_id = m.author_account_id + LEFT JOIN account_handles oh ON oh.account_id = ah.owner_user_id + JOIN topics t ON t.id = m.topic_id + JOIN channels c ON c.id = t.channel_id + JOIN participating p ON p.channel_id = t.channel_id + LEFT JOIN LATERAL ( + SELECT (CASE WHEN answer_handle.owner_user_id IS NULL THEN COALESCE(answer_handle.handle, '') + WHEN answer_owner.handle IS NULL THEN '' + ELSE answer_owner.handle || '/' || answer_handle.handle END)::text AS author_handle + FROM messages answer_message + LEFT JOIN account_handles answer_handle ON answer_handle.account_id = answer_message.author_account_id + LEFT JOIN account_handles answer_owner ON answer_owner.account_id = answer_handle.owner_user_id + WHERE answer_message.topic_id = m.topic_id + AND answer_message.blocks @> jsonb_build_array(jsonb_build_object( + 'kind', 'ask_answer', + 'ask_answer', jsonb_build_object('ask', jsonb_build_object('ask_id', $1::text)) + )) + ORDER BY answer_message.seq + LIMIT 1 + ) answer ON TRUE + WHERE m.blocks @> jsonb_build_array(jsonb_build_object( + 'kind', 'ask', 'ask', jsonb_build_object('ask_id', $1::text) + )) + ORDER BY m.seq + LIMIT 1 +` + +type AskForForgeMirrorParams struct { + AskID string + AccountID string +} + +type AskForForgeMirrorRow struct { + ID string + TopicID string + AuthorAccountID string + AuthorHandle string + AtUnixMs int64 + Blocks []byte + TurnSequence int64 + ChannelID string + ChannelName string + TopicName string + AnsweredBy string +} + +func (q *Queries) AskForForgeMirror(ctx context.Context, arg AskForForgeMirrorParams) (AskForForgeMirrorRow, error) { + row := q.db.QueryRow(ctx, askForForgeMirror, arg.AskID, arg.AccountID) + var i AskForForgeMirrorRow + err := row.Scan( + &i.ID, + &i.TopicID, + &i.AuthorAccountID, + &i.AuthorHandle, + &i.AtUnixMs, + &i.Blocks, + &i.TurnSequence, + &i.ChannelID, + &i.ChannelName, + &i.TopicName, + &i.AnsweredBy, + ) + return i, err +} diff --git a/go/internal/store/db/forge_mirrors.sql.go b/go/internal/store/db/forge_mirrors.sql.go new file mode 100644 index 000000000..fa039e219 --- /dev/null +++ b/go/internal/store/db/forge_mirrors.sql.go @@ -0,0 +1,803 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: forge_mirrors.sql + +package db + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const askForgeMirrors = `-- name: AskForgeMirrors :many +SELECT id, source, ask_id, source_key, actor_account_id, + COALESCE(agent_account_id, '')::text AS agent_account_id, + client_request_id, target_ref, body_rev, posted_rev, state, attempts, + comment_id, comment_key, comment_url, last_error, first_attempt_at, + last_attempt_at, client_comment_id::text AS client_comment_id, + recheck_at, lease_until, lease_token, updated_at + FROM forge_mirrors WHERE ask_id = ANY($1::text[]) ORDER BY created_at, id +` + +type AskForgeMirrorsRow struct { + ID string + Source int16 + AskID string + SourceKey string + ActorAccountID string + AgentAccountID string + ClientRequestID pgtype.Text + TargetRef string + BodyRev int16 + PostedRev int16 + State int16 + Attempts int32 + CommentID int64 + CommentKey string + CommentUrl string + LastError string + FirstAttemptAt pgtype.Timestamptz + LastAttemptAt pgtype.Timestamptz + ClientCommentID string + RecheckAt pgtype.Timestamptz + LeaseUntil pgtype.Timestamptz + LeaseToken string + UpdatedAt pgtype.Timestamptz +} + +func (q *Queries) AskForgeMirrors(ctx context.Context, askIds []string) ([]AskForgeMirrorsRow, error) { + rows, err := q.db.Query(ctx, askForgeMirrors, askIds) + if err != nil { + return nil, err + } + defer rows.Close() + var items []AskForgeMirrorsRow + for rows.Next() { + var i AskForgeMirrorsRow + if err := rows.Scan( + &i.ID, + &i.Source, + &i.AskID, + &i.SourceKey, + &i.ActorAccountID, + &i.AgentAccountID, + &i.ClientRequestID, + &i.TargetRef, + &i.BodyRev, + &i.PostedRev, + &i.State, + &i.Attempts, + &i.CommentID, + &i.CommentKey, + &i.CommentUrl, + &i.LastError, + &i.FirstAttemptAt, + &i.LastAttemptAt, + &i.ClientCommentID, + &i.RecheckAt, + &i.LeaseUntil, + &i.LeaseToken, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const beginForgeMirrorAttempt = `-- name: BeginForgeMirrorAttempt :one +UPDATE forge_mirrors + SET first_attempt_at = COALESCE(first_attempt_at, now()), + last_attempt_at = now(), attempts = attempts + 1 + WHERE id = $1 AND lease_token = $2 AND lease_until > now() +RETURNING id, source, ask_id, source_key, actor_account_id, + COALESCE(agent_account_id, '')::text AS agent_account_id, + client_request_id, target_ref, body_rev, posted_rev, state, attempts, + comment_id, comment_key, comment_url, last_error, first_attempt_at, + last_attempt_at, client_comment_id::text AS client_comment_id, + recheck_at, lease_until, lease_token, updated_at +` + +type BeginForgeMirrorAttemptParams struct { + ID string + LeaseToken string +} + +type BeginForgeMirrorAttemptRow struct { + ID string + Source int16 + AskID string + SourceKey string + ActorAccountID string + AgentAccountID string + ClientRequestID pgtype.Text + TargetRef string + BodyRev int16 + PostedRev int16 + State int16 + Attempts int32 + CommentID int64 + CommentKey string + CommentUrl string + LastError string + FirstAttemptAt pgtype.Timestamptz + LastAttemptAt pgtype.Timestamptz + ClientCommentID string + RecheckAt pgtype.Timestamptz + LeaseUntil pgtype.Timestamptz + LeaseToken string + UpdatedAt pgtype.Timestamptz +} + +func (q *Queries) BeginForgeMirrorAttempt(ctx context.Context, arg BeginForgeMirrorAttemptParams) (BeginForgeMirrorAttemptRow, error) { + row := q.db.QueryRow(ctx, beginForgeMirrorAttempt, arg.ID, arg.LeaseToken) + var i BeginForgeMirrorAttemptRow + err := row.Scan( + &i.ID, + &i.Source, + &i.AskID, + &i.SourceKey, + &i.ActorAccountID, + &i.AgentAccountID, + &i.ClientRequestID, + &i.TargetRef, + &i.BodyRev, + &i.PostedRev, + &i.State, + &i.Attempts, + &i.CommentID, + &i.CommentKey, + &i.CommentUrl, + &i.LastError, + &i.FirstAttemptAt, + &i.LastAttemptAt, + &i.ClientCommentID, + &i.RecheckAt, + &i.LeaseUntil, + &i.LeaseToken, + &i.UpdatedAt, + ) + return i, err +} + +const bumpAskForgeMirror = `-- name: BumpAskForgeMirror :exec +UPDATE forge_mirrors SET body_rev = 2, state = 1 WHERE ask_id = $1 +` + +func (q *Queries) BumpAskForgeMirror(ctx context.Context, askID string) error { + _, err := q.db.Exec(ctx, bumpAskForgeMirror, askID) + return err +} + +const bumpForgeMirrorSourceRevision = `-- name: BumpForgeMirrorSourceRevision :execrows +UPDATE forge_mirrors + SET body_rev = LEAST(body_rev::integer + 1, 32767)::smallint, state = 1 + WHERE id = $1 +` + +func (q *Queries) BumpForgeMirrorSourceRevision(ctx context.Context, id string) (int64, error) { + result, err := q.db.Exec(ctx, bumpForgeMirrorSourceRevision, id) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} + +const claimForgeMirror = `-- name: ClaimForgeMirror :one +INSERT INTO forge_mirrors + (id, source, ask_id, source_key, actor_account_id, agent_account_id, + client_request_id, target_ref, body_rev, state) +VALUES ($1, $2, $3, $4, $5, + $6, $7, $8, $9, $10) +ON CONFLICT DO NOTHING +RETURNING id, source, ask_id, source_key, actor_account_id, + COALESCE(agent_account_id, '')::text AS agent_account_id, + client_request_id, target_ref, body_rev, posted_rev, state, attempts, + comment_id, comment_key, comment_url, last_error, first_attempt_at, + last_attempt_at, client_comment_id::text AS client_comment_id, + recheck_at, lease_until, lease_token, updated_at +` + +type ClaimForgeMirrorParams struct { + ID string + Source int16 + AskID string + SourceKey string + ActorAccountID string + AgentAccountID pgtype.Text + ClientRequestID pgtype.Text + TargetRef string + BodyRev int16 + State int16 +} + +type ClaimForgeMirrorRow struct { + ID string + Source int16 + AskID string + SourceKey string + ActorAccountID string + AgentAccountID string + ClientRequestID pgtype.Text + TargetRef string + BodyRev int16 + PostedRev int16 + State int16 + Attempts int32 + CommentID int64 + CommentKey string + CommentUrl string + LastError string + FirstAttemptAt pgtype.Timestamptz + LastAttemptAt pgtype.Timestamptz + ClientCommentID string + RecheckAt pgtype.Timestamptz + LeaseUntil pgtype.Timestamptz + LeaseToken string + UpdatedAt pgtype.Timestamptz +} + +func (q *Queries) ClaimForgeMirror(ctx context.Context, arg ClaimForgeMirrorParams) (ClaimForgeMirrorRow, error) { + row := q.db.QueryRow(ctx, claimForgeMirror, + arg.ID, + arg.Source, + arg.AskID, + arg.SourceKey, + arg.ActorAccountID, + arg.AgentAccountID, + arg.ClientRequestID, + arg.TargetRef, + arg.BodyRev, + arg.State, + ) + var i ClaimForgeMirrorRow + err := row.Scan( + &i.ID, + &i.Source, + &i.AskID, + &i.SourceKey, + &i.ActorAccountID, + &i.AgentAccountID, + &i.ClientRequestID, + &i.TargetRef, + &i.BodyRev, + &i.PostedRev, + &i.State, + &i.Attempts, + &i.CommentID, + &i.CommentKey, + &i.CommentUrl, + &i.LastError, + &i.FirstAttemptAt, + &i.LastAttemptAt, + &i.ClientCommentID, + &i.RecheckAt, + &i.LeaseUntil, + &i.LeaseToken, + &i.UpdatedAt, + ) + return i, err +} + +const deleteForgeMirrorSetting = `-- name: DeleteForgeMirrorSetting :exec +DELETE FROM forge_mirror_settings + WHERE forge_provider = $1 AND forge_host = $2 AND repo = $3 +` + +type DeleteForgeMirrorSettingParams struct { + ForgeProvider int16 + ForgeHost string + Repo string +} + +func (q *Queries) DeleteForgeMirrorSetting(ctx context.Context, arg DeleteForgeMirrorSettingParams) error { + _, err := q.db.Exec(ctx, deleteForgeMirrorSetting, arg.ForgeProvider, arg.ForgeHost, arg.Repo) + return err +} + +const forgeMirrorByConflictKeys = `-- name: ForgeMirrorByConflictKeys :one +SELECT id, source, ask_id, source_key, actor_account_id, + COALESCE(agent_account_id, '')::text AS agent_account_id, + client_request_id, target_ref, body_rev, posted_rev, state, attempts, + comment_id, comment_key, comment_url, last_error, first_attempt_at, + last_attempt_at, client_comment_id::text AS client_comment_id, + recheck_at, lease_until, lease_token, updated_at, + CASE WHEN agent_account_id = NULLIF($1::text, '') AND client_request_id = $2::text + THEN 2::smallint + WHEN ask_id = NULLIF($3::text, '') THEN 1::smallint + ELSE 3::smallint END AS conflict_kind + FROM forge_mirrors + WHERE ($3::text <> '' AND ask_id = $3::text) + OR ($1::text <> '' AND agent_account_id = $1::text AND client_request_id = $2::text) + OR ($4::text <> '' AND source_key = $4::text AND target_ref = $5::text) + ORDER BY CASE WHEN agent_account_id = NULLIF($1::text, '') AND client_request_id = $2::text THEN 1 + WHEN ask_id = NULLIF($3::text, '') THEN 2 + ELSE 3 END + LIMIT 1 +` + +type ForgeMirrorByConflictKeysParams struct { + AgentAccountID string + ClientRequestID pgtype.Text + AskID string + SourceKey string + TargetRef string +} + +type ForgeMirrorByConflictKeysRow struct { + ID string + Source int16 + AskID string + SourceKey string + ActorAccountID string + AgentAccountID string + ClientRequestID pgtype.Text + TargetRef string + BodyRev int16 + PostedRev int16 + State int16 + Attempts int32 + CommentID int64 + CommentKey string + CommentUrl string + LastError string + FirstAttemptAt pgtype.Timestamptz + LastAttemptAt pgtype.Timestamptz + ClientCommentID string + RecheckAt pgtype.Timestamptz + LeaseUntil pgtype.Timestamptz + LeaseToken string + UpdatedAt pgtype.Timestamptz + ConflictKind int16 +} + +func (q *Queries) ForgeMirrorByConflictKeys(ctx context.Context, arg ForgeMirrorByConflictKeysParams) (ForgeMirrorByConflictKeysRow, error) { + row := q.db.QueryRow(ctx, forgeMirrorByConflictKeys, + arg.AgentAccountID, + arg.ClientRequestID, + arg.AskID, + arg.SourceKey, + arg.TargetRef, + ) + var i ForgeMirrorByConflictKeysRow + err := row.Scan( + &i.ID, + &i.Source, + &i.AskID, + &i.SourceKey, + &i.ActorAccountID, + &i.AgentAccountID, + &i.ClientRequestID, + &i.TargetRef, + &i.BodyRev, + &i.PostedRev, + &i.State, + &i.Attempts, + &i.CommentID, + &i.CommentKey, + &i.CommentUrl, + &i.LastError, + &i.FirstAttemptAt, + &i.LastAttemptAt, + &i.ClientCommentID, + &i.RecheckAt, + &i.LeaseUntil, + &i.LeaseToken, + &i.UpdatedAt, + &i.ConflictKind, + ) + return i, err +} + +const forgeMirrorByID = `-- name: ForgeMirrorByID :one +SELECT id, source, ask_id, source_key, actor_account_id, + COALESCE(agent_account_id, '')::text AS agent_account_id, + client_request_id, target_ref, body_rev, posted_rev, state, attempts, + comment_id, comment_key, comment_url, last_error, first_attempt_at, + last_attempt_at, client_comment_id::text AS client_comment_id, + recheck_at, lease_until, lease_token, updated_at + FROM forge_mirrors WHERE id = $1 +` + +type ForgeMirrorByIDRow struct { + ID string + Source int16 + AskID string + SourceKey string + ActorAccountID string + AgentAccountID string + ClientRequestID pgtype.Text + TargetRef string + BodyRev int16 + PostedRev int16 + State int16 + Attempts int32 + CommentID int64 + CommentKey string + CommentUrl string + LastError string + FirstAttemptAt pgtype.Timestamptz + LastAttemptAt pgtype.Timestamptz + ClientCommentID string + RecheckAt pgtype.Timestamptz + LeaseUntil pgtype.Timestamptz + LeaseToken string + UpdatedAt pgtype.Timestamptz +} + +func (q *Queries) ForgeMirrorByID(ctx context.Context, id string) (ForgeMirrorByIDRow, error) { + row := q.db.QueryRow(ctx, forgeMirrorByID, id) + var i ForgeMirrorByIDRow + err := row.Scan( + &i.ID, + &i.Source, + &i.AskID, + &i.SourceKey, + &i.ActorAccountID, + &i.AgentAccountID, + &i.ClientRequestID, + &i.TargetRef, + &i.BodyRev, + &i.PostedRev, + &i.State, + &i.Attempts, + &i.CommentID, + &i.CommentKey, + &i.CommentUrl, + &i.LastError, + &i.FirstAttemptAt, + &i.LastAttemptAt, + &i.ClientCommentID, + &i.RecheckAt, + &i.LeaseUntil, + &i.LeaseToken, + &i.UpdatedAt, + ) + return i, err +} + +const forgeMirrorMode = `-- name: ForgeMirrorMode :one + +SELECT COALESCE(( + SELECT mode FROM forge_mirror_settings + WHERE forge_provider = $1::smallint AND forge_host = $2::text AND repo = $3::text +), 0)::smallint AS mode +` + +type ForgeMirrorModeParams struct { + ForgeProvider int16 + ForgeHost string + Repo string +} + +// Ask mirroring settings and outbox persistence. +func (q *Queries) ForgeMirrorMode(ctx context.Context, arg ForgeMirrorModeParams) (int16, error) { + row := q.db.QueryRow(ctx, forgeMirrorMode, arg.ForgeProvider, arg.ForgeHost, arg.Repo) + var mode int16 + err := row.Scan(&mode) + return mode, err +} + +const insertAskForgeMirror = `-- name: InsertAskForgeMirror :exec +INSERT INTO forge_mirrors + (id, source, ask_id, actor_account_id, agent_account_id, target_ref, body_rev, state) +VALUES ($1, 1, $2, $3, $3, $4, 1, 1) +` + +type InsertAskForgeMirrorParams struct { + ID string + AskID string + ActorAccountID string + TargetRef string +} + +func (q *Queries) InsertAskForgeMirror(ctx context.Context, arg InsertAskForgeMirrorParams) error { + _, err := q.db.Exec(ctx, insertAskForgeMirror, + arg.ID, + arg.AskID, + arg.ActorAccountID, + arg.TargetRef, + ) + return err +} + +const leaseForgeMirror = `-- name: LeaseForgeMirror :one +UPDATE forge_mirrors + SET lease_until = now() + $1::interval, lease_token = $2 + WHERE id = $3 AND (state <> 2 OR recheck_at <= now()) + AND (lease_until IS NULL OR lease_until < now()) +RETURNING id, source, ask_id, source_key, actor_account_id, + COALESCE(agent_account_id, '')::text AS agent_account_id, + client_request_id, target_ref, body_rev, posted_rev, state, attempts, + comment_id, comment_key, comment_url, last_error, first_attempt_at, + last_attempt_at, client_comment_id::text AS client_comment_id, + recheck_at, lease_until, lease_token, updated_at +` + +type LeaseForgeMirrorParams struct { + Ttl pgtype.Interval + LeaseToken string + ID string +} + +type LeaseForgeMirrorRow struct { + ID string + Source int16 + AskID string + SourceKey string + ActorAccountID string + AgentAccountID string + ClientRequestID pgtype.Text + TargetRef string + BodyRev int16 + PostedRev int16 + State int16 + Attempts int32 + CommentID int64 + CommentKey string + CommentUrl string + LastError string + FirstAttemptAt pgtype.Timestamptz + LastAttemptAt pgtype.Timestamptz + ClientCommentID string + RecheckAt pgtype.Timestamptz + LeaseUntil pgtype.Timestamptz + LeaseToken string + UpdatedAt pgtype.Timestamptz +} + +func (q *Queries) LeaseForgeMirror(ctx context.Context, arg LeaseForgeMirrorParams) (LeaseForgeMirrorRow, error) { + row := q.db.QueryRow(ctx, leaseForgeMirror, arg.Ttl, arg.LeaseToken, arg.ID) + var i LeaseForgeMirrorRow + err := row.Scan( + &i.ID, + &i.Source, + &i.AskID, + &i.SourceKey, + &i.ActorAccountID, + &i.AgentAccountID, + &i.ClientRequestID, + &i.TargetRef, + &i.BodyRev, + &i.PostedRev, + &i.State, + &i.Attempts, + &i.CommentID, + &i.CommentKey, + &i.CommentUrl, + &i.LastError, + &i.FirstAttemptAt, + &i.LastAttemptAt, + &i.ClientCommentID, + &i.RecheckAt, + &i.LeaseUntil, + &i.LeaseToken, + &i.UpdatedAt, + ) + return i, err +} + +const listForgeMirrorSettings = `-- name: ListForgeMirrorSettings :many +SELECT s.forge_provider, s.forge_host, + (CASE WHEN s.forge_provider = 1 THEN lower(btrim(s.repo)) ELSE btrim(s.repo) END)::text AS repo, + COALESCE(m.mode, 0)::smallint AS mode + FROM forge_repo_subscriptions s + LEFT JOIN forge_mirror_settings m + ON m.forge_provider = s.forge_provider + AND m.forge_host = s.forge_host + AND m.repo = CASE WHEN s.forge_provider = 1 THEN lower(btrim(s.repo)) ELSE btrim(s.repo) END + WHERE s.enabled + ORDER BY s.forge_provider, s.forge_host, s.repo +` + +type ListForgeMirrorSettingsRow struct { + ForgeProvider int16 + ForgeHost string + Repo string + Mode int16 +} + +func (q *Queries) ListForgeMirrorSettings(ctx context.Context) ([]ListForgeMirrorSettingsRow, error) { + rows, err := q.db.Query(ctx, listForgeMirrorSettings) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListForgeMirrorSettingsRow + for rows.Next() { + var i ListForgeMirrorSettingsRow + if err := rows.Scan( + &i.ForgeProvider, + &i.ForgeHost, + &i.Repo, + &i.Mode, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const markForgeMirror = `-- name: MarkForgeMirror :execrows +UPDATE forge_mirrors + SET posted_rev = $1, + state = CASE WHEN $2::smallint = 2 AND body_rev > $1::smallint THEN 1::smallint ELSE $2::smallint END, + comment_id = CASE WHEN $3::bigint = 0 THEN comment_id ELSE $3 END, + comment_key = CASE WHEN $4::text = '' THEN comment_key ELSE $4 END, + comment_url = CASE WHEN $5::text = '' THEN comment_url ELSE $5 END, + last_error = $6, + recheck_at = CASE WHEN $7::boolean THEN now() + interval '10 minutes' + WHEN $8::boolean THEN NULL + ELSE recheck_at END, + lease_until = NULL, lease_token = '' + WHERE id = $9 AND lease_token = $10 AND lease_until > now() +` + +type MarkForgeMirrorParams struct { + PostedRev int16 + State int16 + CommentID int64 + CommentKey string + CommentUrl string + LastError string + Recheck bool + ClearRecheck bool + ID string + LeaseToken string +} + +func (q *Queries) MarkForgeMirror(ctx context.Context, arg MarkForgeMirrorParams) (int64, error) { + result, err := q.db.Exec(ctx, markForgeMirror, + arg.PostedRev, + arg.State, + arg.CommentID, + arg.CommentKey, + arg.CommentUrl, + arg.LastError, + arg.Recheck, + arg.ClearRecheck, + arg.ID, + arg.LeaseToken, + ) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} + +const openForgeMirrors = `-- name: OpenForgeMirrors :many +SELECT id, source, ask_id, source_key, actor_account_id, + COALESCE(agent_account_id, '')::text AS agent_account_id, + client_request_id, target_ref, body_rev, posted_rev, state, attempts, + comment_id, comment_key, comment_url, last_error, first_attempt_at, + last_attempt_at, client_comment_id::text AS client_comment_id, + recheck_at, lease_until, lease_token, updated_at + FROM forge_mirrors + WHERE agent_account_id = $1::text AND (state <> 2 OR recheck_at <= now()) + ORDER BY created_at, id +` + +type OpenForgeMirrorsRow struct { + ID string + Source int16 + AskID string + SourceKey string + ActorAccountID string + AgentAccountID string + ClientRequestID pgtype.Text + TargetRef string + BodyRev int16 + PostedRev int16 + State int16 + Attempts int32 + CommentID int64 + CommentKey string + CommentUrl string + LastError string + FirstAttemptAt pgtype.Timestamptz + LastAttemptAt pgtype.Timestamptz + ClientCommentID string + RecheckAt pgtype.Timestamptz + LeaseUntil pgtype.Timestamptz + LeaseToken string + UpdatedAt pgtype.Timestamptz +} + +func (q *Queries) OpenForgeMirrors(ctx context.Context, agentAccountID string) ([]OpenForgeMirrorsRow, error) { + rows, err := q.db.Query(ctx, openForgeMirrors, agentAccountID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []OpenForgeMirrorsRow + for rows.Next() { + var i OpenForgeMirrorsRow + if err := rows.Scan( + &i.ID, + &i.Source, + &i.AskID, + &i.SourceKey, + &i.ActorAccountID, + &i.AgentAccountID, + &i.ClientRequestID, + &i.TargetRef, + &i.BodyRev, + &i.PostedRev, + &i.State, + &i.Attempts, + &i.CommentID, + &i.CommentKey, + &i.CommentUrl, + &i.LastError, + &i.FirstAttemptAt, + &i.LastAttemptAt, + &i.ClientCommentID, + &i.RecheckAt, + &i.LeaseUntil, + &i.LeaseToken, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const renewForgeMirror = `-- name: RenewForgeMirror :execrows +UPDATE forge_mirrors SET lease_until = now() + $1::interval + WHERE id = $2 AND lease_token = $3 AND lease_until > now() +` + +type RenewForgeMirrorParams struct { + Ttl pgtype.Interval + ID string + LeaseToken string +} + +func (q *Queries) RenewForgeMirror(ctx context.Context, arg RenewForgeMirrorParams) (int64, error) { + result, err := q.db.Exec(ctx, renewForgeMirror, arg.Ttl, arg.ID, arg.LeaseToken) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} + +const upsertForgeMirrorSetting = `-- name: UpsertForgeMirrorSetting :exec +INSERT INTO forge_mirror_settings (forge_provider, forge_host, repo, mode, updated_by) +VALUES ($1, $2, $3, $4, $5) +ON CONFLICT (tenant_id, forge_provider, forge_host, repo) DO UPDATE + SET mode = EXCLUDED.mode, updated_by = EXCLUDED.updated_by +` + +type UpsertForgeMirrorSettingParams struct { + ForgeProvider int16 + ForgeHost string + Repo string + Mode int16 + UpdatedBy string +} + +func (q *Queries) UpsertForgeMirrorSetting(ctx context.Context, arg UpsertForgeMirrorSettingParams) error { + _, err := q.db.Exec(ctx, upsertForgeMirrorSetting, + arg.ForgeProvider, + arg.ForgeHost, + arg.Repo, + arg.Mode, + arg.UpdatedBy, + ) + return err +} diff --git a/go/internal/store/db/models.go b/go/internal/store/db/models.go index 0b71ade37..776a2a834 100644 --- a/go/internal/store/db/models.go +++ b/go/internal/store/db/models.go @@ -289,6 +289,45 @@ type ForgeAuthoredArtifact struct { TenantID string } +type ForgeMirror struct { + ID string + Source int16 + AskID string + SourceKey string + ActorAccountID string + AgentAccountID pgtype.Text + ClientRequestID pgtype.Text + TargetRef string + BodyRev int16 + PostedRev int16 + State int16 + Attempts int32 + CommentID int64 + CommentKey string + CommentUrl string + LastError string + FirstAttemptAt pgtype.Timestamptz + LastAttemptAt pgtype.Timestamptz + ClientCommentID pgtype.UUID + RecheckAt pgtype.Timestamptz + LeaseUntil pgtype.Timestamptz + LeaseToken string + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz + TenantID string +} + +type ForgeMirrorSetting struct { + ForgeProvider int16 + ForgeHost string + Repo string + Mode int16 + UpdatedBy string + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz + TenantID string +} + type ForgeRepoSubscription struct { ForgeProvider int16 ForgeHost string diff --git a/go/internal/store/db/querier.go b/go/internal/store/db/querier.go index 8a2e13f22..fa62d81d4 100644 --- a/go/internal/store/db/querier.go +++ b/go/internal/store/db/querier.go @@ -71,9 +71,12 @@ type Querier interface { // to both rollups, so a replayed id counts once. The arrays hold one element // per event, and no id repeats. AppendTokenUsageEvents(ctx context.Context, arg AppendTokenUsageEventsParams) error + AskForForgeMirror(ctx context.Context, arg AskForForgeMirrorParams) (AskForForgeMirrorRow, error) + AskForgeMirrors(ctx context.Context, askIds []string) ([]AskForgeMirrorsRow, error) AuthoredArtifactByCoordinate(ctx context.Context, arg AuthoredArtifactByCoordinateParams) (ForgeAuthoredArtifact, error) AuthoredArtifactByRequestID(ctx context.Context, arg AuthoredArtifactByRequestIDParams) (ForgeAuthoredArtifact, error) AuthoredCoordsForAgent(ctx context.Context, arg AuthoredCoordsForAgentParams) ([]AuthoredCoordsForAgentRow, error) + BeginForgeMirrorAttempt(ctx context.Context, arg BeginForgeMirrorAttemptParams) (BeginForgeMirrorAttemptRow, error) // Agent-transcript queries (sqlc adoption T5, RIG-3034). These replace the // inline SQL literals in internal/store/agent_transcripts.go; the hand-written // Store methods keep their exact signatures and own the two-tier flush @@ -88,6 +91,8 @@ type Querier interface { // pool (the eponymous method) and a snapshot tx (SessionResumeSnapshot, via // WithTx), so one generated query backs both call sites. BindLifetime(ctx context.Context, arg BindLifetimeParams) (int64, error) + BumpAskForgeMirror(ctx context.Context, askID string) error + BumpForgeMirrorSourceRevision(ctx context.Context, id string) (int64, error) // Keep handle joins optional so missing handle rows do not hide @everyone members. // reach: the author may reach agent aa ChannelAgentMembers(ctx context.Context, arg ChannelAgentMembersParams) ([]ChannelAgentMembersRow, error) @@ -101,6 +106,7 @@ type Querier interface { ChannelParticipant(ctx context.Context, arg ChannelParticipantParams) (pgtype.Bool, error) ChannelVisibleTo(ctx context.Context, arg ChannelVisibleToParams) (bool, error) ChannelsByNameForViewer(ctx context.Context, arg ChannelsByNameForViewerParams) ([]ChannelsByNameForViewerRow, error) + ClaimForgeMirror(ctx context.Context, arg ClaimForgeMirrorParams) (ClaimForgeMirrorRow, error) // The unique key makes concurrent first-run attempts a single-winner claim. ClaimTourStart(ctx context.Context, arg ClaimTourStartParams) (string, error) ClearForgeRepoMergeQueueConfigs(ctx context.Context, arg ClearForgeRepoMergeQueueConfigsParams) error @@ -152,6 +158,7 @@ type Querier interface { DeleteComputeUsageIntervalsBefore(ctx context.Context, arg DeleteComputeUsageIntervalsBeforeParams) (int64, error) // DeleteComputeUsageRollupsFrom clears rows the rebuild can reconstruct. DeleteComputeUsageRollupsFrom(ctx context.Context, horizon pgtype.Timestamptz) error + DeleteForgeMirrorSetting(ctx context.Context, arg DeleteForgeMirrorSettingParams) error // Delete marks the row as unconfigured while retaining its monotonic version. // It matches only the version the orphan check read, so a racing Put conflicts. DeleteModelRegistry(ctx context.Context, version int64) (int64, error) @@ -204,6 +211,10 @@ type Querier interface { EnsureForgeRepoSubscription(ctx context.Context, arg EnsureForgeRepoSubscriptionParams) error FallbackIssuesForTarget(ctx context.Context, arg FallbackIssuesForTargetParams) ([]FallbackIssuesForTargetRow, error) FindAskMessage(ctx context.Context, arg FindAskMessageParams) ([]FindAskMessageRow, error) + ForgeMirrorByConflictKeys(ctx context.Context, arg ForgeMirrorByConflictKeysParams) (ForgeMirrorByConflictKeysRow, error) + ForgeMirrorByID(ctx context.Context, id string) (ForgeMirrorByIDRow, error) + // Ask mirroring settings and outbox persistence. + ForgeMirrorMode(ctx context.Context, arg ForgeMirrorModeParams) (int16, error) ForgeScopeUserExists(ctx context.Context, accountID string) (bool, error) // Collects the coordinate's cursor IFF no subscription for it remains (the NOT // EXISTS guard leaves it in place if any other agent still subscribes). @@ -320,6 +331,7 @@ type Querier interface { InsertAgentSession(ctx context.Context, arg InsertAgentSessionParams) error InsertAgentWorkspaceIgnore(ctx context.Context, arg InsertAgentWorkspaceIgnoreParams) error InsertArchiveSegment(ctx context.Context, arg InsertArchiveSegmentParams) error + InsertAskForgeMirror(ctx context.Context, arg InsertAskForgeMirrorParams) error InsertChannel(ctx context.Context, arg InsertChannelParams) error // Channel-domain queries (sqlc adoption T3, RIG-3034). These replace the inline // SQL literals that lived in internal/store/channels.go; the hand-written Store @@ -404,6 +416,7 @@ type Querier interface { IssuesByCoords(ctx context.Context, arg IssuesByCoordsParams) ([]IssuesByCoordsRow, error) LatestCheckpointSeq(ctx context.Context, sessionID string) (int64, error) LatestSessionForAccount(ctx context.Context, agentAccountID string) (string, error) + LeaseForgeMirror(ctx context.Context, arg LeaseForgeMirrorParams) (LeaseForgeMirrorRow, error) LinearAgentSession(ctx context.Context, linearSessionID string) (LinearAgentSessionRow, error) // An account's own rows only; the owner's grants are not included. ListAgentForgeScopeRepos(ctx context.Context, arg ListAgentForgeScopeReposParams) ([]string, error) @@ -417,6 +430,7 @@ type Querier interface { ListChannels(ctx context.Context, accountID string) ([]ListChannelsRow, error) ListEnabledForgeRepoSubscriptions(ctx context.Context, arg ListEnabledForgeRepoSubscriptionsParams) ([]ListEnabledForgeRepoSubscriptionsRow, error) ListEnabledForgeRepos(ctx context.Context) ([]string, error) + ListForgeMirrorSettings(ctx context.Context) ([]ListForgeMirrorSettingsRow, error) // The reconcile sweep's work list for one (provider, host): each subscribed // coordinate with its LEFT-JOINed shared FETCH cursor (nullable when never // observed) and the subscriber rows, container-scope rows collapsed per @@ -536,6 +550,7 @@ type Querier interface { // LockTokenUsage serializes one tenant's appends and rebuilds. A rebuild beside // an append would count the append's events twice or lose them. LockTokenUsage(ctx context.Context) error + MarkForgeMirror(ctx context.Context, arg MarkForgeMirrorParams) (int64, error) MarkMentionsRouted(ctx context.Context, arg MarkMentionsRoutedParams) error MarkPRsBackfilled(ctx context.Context, arg MarkPRsBackfilledParams) (int64, error) MergeTopicLastSeq(ctx context.Context, arg MergeTopicLastSeqParams) error @@ -545,6 +560,7 @@ type Querier interface { MessagesHeadSeq(ctx context.Context) (int64, error) ModelRegistryVersion(ctx context.Context) (int64, error) MoveMessagesToTopic(ctx context.Context, arg MoveMessagesToTopicParams) error + OpenForgeMirrors(ctx context.Context, agentAccountID string) ([]OpenForgeMirrorsRow, error) // The accounts still owed a mention: a wake that failed before any Runner could // serve it is retried for these once one attaches. OwedMentionAccounts(ctx context.Context) ([]string, error) @@ -621,6 +637,7 @@ type Querier interface { RenameCoordinationChannel(ctx context.Context, arg RenameCoordinationChannelParams) (int64, error) RenameDMChannel(ctx context.Context, arg RenameDMChannelParams) (int64, error) RenameTopic(ctx context.Context, arg RenameTopicParams) error + RenewForgeMirror(ctx context.Context, arg RenewForgeMirrorParams) (int64, error) RequireAgentSessionOwner(ctx context.Context, arg RequireAgentSessionOwnerParams) (bool, error) RequireAgentSessionSubscriber(ctx context.Context, arg RequireAgentSessionSubscriberParams) (bool, error) ResolveAckMessage(ctx context.Context, arg ResolveAckMessageParams) (int64, error) @@ -771,6 +788,7 @@ type Querier interface { UpsertChannelSubscription(ctx context.Context, arg UpsertChannelSubscriptionParams) error UpsertExplicitPullRequestLink(ctx context.Context, arg UpsertExplicitPullRequestLinkParams) error UpsertForgeArtifactCursor(ctx context.Context, arg UpsertForgeArtifactCursorParams) error + UpsertForgeMirrorSetting(ctx context.Context, arg UpsertForgeMirrorSettingParams) error // Issue-domain queries (sqlc adoption T6, RIG-3034). These replace the inline // SQL literals in internal/store/issues.go; the hand-written Store methods keep // their signatures, the door-side validation, the ErrNotFound/ErrInvalidArgument diff --git a/go/internal/store/errors.go b/go/internal/store/errors.go index 5589bf9cf..e943f2a5c 100644 --- a/go/internal/store/errors.go +++ b/go/internal/store/errors.go @@ -19,6 +19,8 @@ const ( // error vars are idiomatic (gochecknoglobals disabled by design, .golangci.yml). var ( + // ErrLeaseLost reports a lease-fenced write made after the lease expired or changed. + ErrLeaseLost = errors.New("store: lease lost") // ErrNotFound is returned when a row addressed by id does not exist, or // exists but the actor may not see it — the two are deliberately // indistinguishable to the caller so a probe cannot enumerate ids it lacks diff --git a/go/internal/store/forge_mirror_ask.go b/go/internal/store/forge_mirror_ask.go new file mode 100644 index 000000000..300a0edae --- /dev/null +++ b/go/internal/store/forge_mirror_ask.go @@ -0,0 +1,46 @@ +package store + +import ( + "context" + "fmt" + + "github.com/RigelBuild/compass/go/internal/store/db" +) + +// ForgeMirrorAsk is the stored ask and its source names for rendering a mirror. +type ForgeMirrorAsk struct { + Message Message + ChannelID ChannelID + Channel string + Topic string + AnsweredBy string +} + +// AskForForgeMirror reads an ask and its answer attribution as a visible participant. +func (s *Store) AskForForgeMirror(ctx context.Context, actor AccountID, askID string) (ForgeMirrorAsk, error) { + if actor == "" || askID == "" { + return ForgeMirrorAsk{}, fmt.Errorf("%w: actor and ask id are required", ErrInvalidArgument) + } + + row, err := s.q.AskForForgeMirror(ctx, db.AskForForgeMirrorParams{ + AskID: askID, + AccountID: string(actor), + }) + if noRows(err) { + return ForgeMirrorAsk{}, fmt.Errorf("%w: ask %q", ErrNotFound, askID) + } + if err != nil { + return ForgeMirrorAsk{}, fmt.Errorf("store: read ask for forge mirror: %w", err) + } + message, err := messageFromParts(row.ID, row.TopicID, row.AuthorAccountID, row.AuthorHandle, row.AtUnixMs, row.Blocks, row.TurnSequence) + if err != nil { + return ForgeMirrorAsk{}, fmt.Errorf("store: decode ask for forge mirror: %w", err) + } + return ForgeMirrorAsk{ + Message: message, + ChannelID: ChannelID(row.ChannelID), + Channel: row.ChannelName, + Topic: row.TopicName, + AnsweredBy: row.AnsweredBy, + }, nil +} diff --git a/go/internal/store/forge_mirrors.go b/go/internal/store/forge_mirrors.go new file mode 100644 index 000000000..c3608d923 --- /dev/null +++ b/go/internal/store/forge_mirrors.go @@ -0,0 +1,390 @@ +package store + +import ( + "context" + "fmt" + "log/slog" + "math" + "time" + + "github.com/jackc/pgx/v5/pgtype" + + "github.com/RigelBuild/compass/go/internal/store/db" +) + +// ForgeMirrorMode is the user-selected ask mirroring policy for one forge repo. +type ForgeMirrorMode int16 + +const ( + ForgeMirrorOff ForgeMirrorMode = iota + ForgeMirrorManual + ForgeMirrorAuto +) + +// ForgeMirrorSetting describes a coordinate's configured mirroring mode. +type ForgeMirrorSetting struct { + Provider ForgeProvider + Host string + Repo string + Mode ForgeMirrorMode +} + +// ForgeMirrorRow is one durable mirror outbox entry. +type ForgeMirrorRow struct { + ID string + Source int16 + AskID string + SourceKey string + Actor AccountID + Agent AccountID + ClientRequestID string + TargetRef string + BodyRev int16 + PostedRev int16 + State int16 + Attempts int + CommentID uint64 + CommentKey string + CommentURL string + LastError string + FirstAttemptAt *time.Time + LastAttemptAt *time.Time + ClientCommentID string + RecheckAt *time.Time + LeaseUntil *time.Time + LeaseToken string + UpdatedAt time.Time +} + +// ForgeMirrorMark is the result of one mirror delivery or reconciliation pass. +type ForgeMirrorMark struct { + PostedRev int16 + State int16 + CommentID uint64 + CommentKey string + URL string + Err string + Recheck bool + ClearRecheck bool +} + +// ForgeMirrorMode returns the coordinate's configured mode, defaulting to Off. +func (s *Store) ForgeMirrorMode(ctx context.Context, provider ForgeProvider, host, repo string) (ForgeMirrorMode, error) { + coord := (ForgeCoord{Provider: provider, Host: host, Repo: repo}).Normalized() + if err := validCoordinate(coord.Provider, coord.Host, coord.Repo); err != nil { + return ForgeMirrorOff, err + } + mode, err := s.q.ForgeMirrorMode(ctx, db.ForgeMirrorModeParams{ + ForgeProvider: int16(coord.Provider), //nolint:gosec // G115: provider is a CHECK-constrained 1..4 enum. + ForgeHost: coord.Host, + Repo: coord.Repo, + }) + if err != nil { + return ForgeMirrorOff, fmt.Errorf("store: read forge mirror mode: %w", err) + } + return ForgeMirrorMode(mode), nil +} + +// ListForgeMirrorSettings lists each enabled subscription and its current mode. +func (s *Store) ListForgeMirrorSettings(ctx context.Context) ([]ForgeMirrorSetting, error) { + rows, err := s.q.ListForgeMirrorSettings(ctx) + if err != nil { + return nil, fmt.Errorf("store: list forge mirror settings: %w", err) + } + out := make([]ForgeMirrorSetting, 0, len(rows)) + for _, row := range rows { + out = append(out, ForgeMirrorSetting{ + Provider: ForgeProvider(row.ForgeProvider), + Host: row.ForgeHost, + Repo: row.Repo, + Mode: ForgeMirrorMode(row.Mode), + }) + } + return out, nil +} + +// SetForgeMirrorSetting removes an Off setting or upserts Manual and Auto. +func (s *Store) SetForgeMirrorSetting(ctx context.Context, actor AccountID, set ForgeMirrorSetting) error { + coord := (ForgeCoord{Provider: set.Provider, Host: set.Host, Repo: set.Repo}).Normalized() + if err := validCoordinate(coord.Provider, coord.Host, coord.Repo); err != nil { + return err + } + if set.Mode < ForgeMirrorOff || set.Mode > ForgeMirrorAuto { + return fmt.Errorf("%w: forge mirror mode %d", ErrInvalidArgument, set.Mode) + } + arg := db.DeleteForgeMirrorSettingParams{ + ForgeProvider: int16(coord.Provider), //nolint:gosec // G115: provider is a CHECK-constrained 1..4 enum. + ForgeHost: coord.Host, + Repo: coord.Repo, + } + if set.Mode == ForgeMirrorOff { + if err := s.q.DeleteForgeMirrorSetting(ctx, arg); err != nil { + return fmt.Errorf("store: delete forge mirror setting: %w", err) + } + return nil + } + if actor == "" { + return fmt.Errorf("%w: forge mirror setting actor is required", ErrInvalidArgument) + } + if err := s.q.UpsertForgeMirrorSetting(ctx, db.UpsertForgeMirrorSettingParams{ + ForgeProvider: arg.ForgeProvider, + ForgeHost: arg.ForgeHost, + Repo: arg.Repo, + Mode: int16(set.Mode), + UpdatedBy: string(actor), + }); err != nil { + return fmt.Errorf("store: set forge mirror setting: %w", err) + } + return nil +} + +// ClaimForgeMirror inserts a row or returns the row at its conflicting key. +func (s *Store) ClaimForgeMirror(ctx context.Context, mirror ForgeMirrorRow) (ForgeMirrorRow, bool, error) { + if (mirror.Source != 1 && mirror.Source != 2) || mirror.Actor == "" || mirror.TargetRef == "" || mirror.BodyRev < 1 || mirror.State < 1 || mirror.State > 3 { + return ForgeMirrorRow{}, false, fmt.Errorf("%w: invalid forge mirror row", ErrInvalidArgument) + } + if (mirror.Source == 1 && (mirror.AskID == "" || mirror.Agent == "")) || (mirror.Source == 2 && mirror.SourceKey == "") { + return ForgeMirrorRow{}, false, fmt.Errorf("%w: forge mirror idempotency key is required", ErrInvalidArgument) + } + row, err := s.q.ClaimForgeMirror(ctx, db.ClaimForgeMirrorParams{ + ID: newID(), + Source: mirror.Source, + AskID: mirror.AskID, + SourceKey: mirror.SourceKey, + ActorAccountID: string(mirror.Actor), + AgentAccountID: nullableString(string(mirror.Agent)), + ClientRequestID: nullableString(mirror.ClientRequestID), + TargetRef: mirror.TargetRef, + BodyRev: mirror.BodyRev, + State: mirror.State, + }) + if err == nil { + return forgeMirrorFromDB(row), true, nil + } + if !noRows(err) { + return ForgeMirrorRow{}, false, fmt.Errorf("store: claim forge mirror: %w", err) + } + conflict, err := s.forgeMirrorByConflictKeys(ctx, mirror) + if err != nil { + return ForgeMirrorRow{}, false, fmt.Errorf("store: find claimed forge mirror: %w", err) + } + if conflict.ConflictKind == 3 && mirror.Source == 2 && conflict.ActorAccountID == string(mirror.Actor) && conflict.AgentAccountID == string(mirror.Agent) { + if _, err := s.q.BumpForgeMirrorSourceRevision(ctx, conflict.ID); err != nil { + return ForgeMirrorRow{}, false, fmt.Errorf("store: bump forge mirror revision: %w", err) + } + conflict, err = s.forgeMirrorByConflictKeys(ctx, mirror) + if err != nil { + return ForgeMirrorRow{}, false, fmt.Errorf("store: reload claimed forge mirror: %w", err) + } + } + return forgeMirrorFromDB(db.ClaimForgeMirrorRow{ + ID: conflict.ID, Source: conflict.Source, AskID: conflict.AskID, SourceKey: conflict.SourceKey, + ActorAccountID: conflict.ActorAccountID, AgentAccountID: conflict.AgentAccountID, + ClientRequestID: conflict.ClientRequestID, TargetRef: conflict.TargetRef, + BodyRev: conflict.BodyRev, PostedRev: conflict.PostedRev, State: conflict.State, Attempts: conflict.Attempts, + CommentID: conflict.CommentID, CommentKey: conflict.CommentKey, CommentUrl: conflict.CommentUrl, LastError: conflict.LastError, + FirstAttemptAt: conflict.FirstAttemptAt, LastAttemptAt: conflict.LastAttemptAt, + ClientCommentID: conflict.ClientCommentID, RecheckAt: conflict.RecheckAt, LeaseUntil: conflict.LeaseUntil, + LeaseToken: conflict.LeaseToken, UpdatedAt: conflict.UpdatedAt, + }), false, nil +} + +func (s *Store) forgeMirrorByConflictKeys(ctx context.Context, mirror ForgeMirrorRow) (db.ForgeMirrorByConflictKeysRow, error) { + return s.q.ForgeMirrorByConflictKeys(ctx, db.ForgeMirrorByConflictKeysParams{ + AskID: mirror.AskID, AgentAccountID: string(mirror.Agent), + ClientRequestID: nullableString(mirror.ClientRequestID), SourceKey: mirror.SourceKey, TargetRef: mirror.TargetRef, + }) +} + +// ForgeMirrorByID reads one mirror row under the current tenant scope. +func (s *Store) ForgeMirrorByID(ctx context.Context, id string) (ForgeMirrorRow, error) { + if id == "" { + return ForgeMirrorRow{}, fmt.Errorf("%w: forge mirror id is required", ErrInvalidArgument) + } + row, err := s.q.ForgeMirrorByID(ctx, id) + if noRows(err) { + return ForgeMirrorRow{}, fmt.Errorf("%w: forge mirror %q", ErrNotFound, id) + } + if err != nil { + return ForgeMirrorRow{}, fmt.Errorf("store: read forge mirror: %w", err) + } + return forgeMirrorFromDB(db.ClaimForgeMirrorRow(row)), nil +} + +// OpenForgeMirrors returns pending and failed rows, and done rows due for recheck. +func (s *Store) OpenForgeMirrors(ctx context.Context, agent AccountID) ([]ForgeMirrorRow, error) { + if agent == "" { + return nil, fmt.Errorf("%w: forge mirror agent is required", ErrInvalidArgument) + } + rows, err := s.q.OpenForgeMirrors(ctx, string(agent)) + if err != nil { + return nil, fmt.Errorf("store: list open forge mirrors: %w", err) + } + out := make([]ForgeMirrorRow, 0, len(rows)) + for _, row := range rows { + out = append(out, forgeMirrorFromDB(db.ClaimForgeMirrorRow(row))) + } + return out, nil +} + +// AskForgeMirrors returns rows matching the supplied ask ids. +func (s *Store) AskForgeMirrors(ctx context.Context, askIDs []string) ([]ForgeMirrorRow, error) { + if len(askIDs) == 0 { + return nil, nil + } + rows, err := s.q.AskForgeMirrors(ctx, askIDs) + if err != nil { + return nil, fmt.Errorf("store: list ask forge mirrors: %w", err) + } + out := make([]ForgeMirrorRow, 0, len(rows)) + for _, row := range rows { + out = append(out, forgeMirrorFromDB(db.ClaimForgeMirrorRow(row))) + } + return out, nil +} + +// LeaseForgeMirror claims one eligible row until ttl elapses. +func (s *Store) LeaseForgeMirror(ctx context.Context, id, token string, ttl time.Duration) (ForgeMirrorRow, bool, error) { + if id == "" || token == "" || ttl <= 0 { + return ForgeMirrorRow{}, false, fmt.Errorf("%w: forge mirror lease id, token, and ttl are required", ErrInvalidArgument) + } + row, err := s.q.LeaseForgeMirror(ctx, db.LeaseForgeMirrorParams{Ttl: pgtype.Interval{Microseconds: ttl.Microseconds(), Valid: true}, LeaseToken: token, ID: id}) + if noRows(err) { + return ForgeMirrorRow{}, false, nil + } + if err != nil { + return ForgeMirrorRow{}, false, fmt.Errorf("store: lease forge mirror: %w", err) + } + return forgeMirrorFromDB(db.ClaimForgeMirrorRow(row)), true, nil +} + +// RenewForgeMirror extends a live lease held by token. +func (s *Store) RenewForgeMirror(ctx context.Context, id, token string, ttl time.Duration) (bool, error) { + if id == "" || token == "" || ttl <= 0 { + return false, fmt.Errorf("%w: forge mirror lease id, token, and ttl are required", ErrInvalidArgument) + } + n, err := s.q.RenewForgeMirror(ctx, db.RenewForgeMirrorParams{Ttl: pgtype.Interval{Microseconds: ttl.Microseconds(), Valid: true}, ID: id, LeaseToken: token}) + if err != nil { + return false, fmt.Errorf("store: renew forge mirror lease: %w", err) + } + return n != 0, nil +} + +// BeginForgeMirrorAttempt records one POST attempt under its current lease. +func (s *Store) BeginForgeMirrorAttempt(ctx context.Context, id, token string) (ForgeMirrorRow, bool, error) { + if id == "" || token == "" { + return ForgeMirrorRow{}, false, fmt.Errorf("%w: forge mirror attempt id and token are required", ErrInvalidArgument) + } + row, err := s.q.BeginForgeMirrorAttempt(ctx, db.BeginForgeMirrorAttemptParams{ID: id, LeaseToken: token}) + if noRows(err) { + return ForgeMirrorRow{}, false, nil + } + if err != nil { + return ForgeMirrorRow{}, false, fmt.Errorf("store: begin forge mirror attempt: %w", err) + } + return forgeMirrorFromDB(db.ClaimForgeMirrorRow(row)), true, nil +} + +// MarkForgeMirror stores delivery state only for the current live lease holder. +func (s *Store) MarkForgeMirror(ctx context.Context, id, token string, mark ForgeMirrorMark) error { + if id == "" || token == "" || mark.State < 1 || mark.State > 3 { + return fmt.Errorf("%w: forge mirror id, token, and state are required", ErrInvalidArgument) + } + if mark.CommentID > math.MaxInt64 { + return fmt.Errorf("%w: forge comment id exceeds BIGINT range", ErrInvalidArgument) + } + n, err := s.q.MarkForgeMirror(ctx, db.MarkForgeMirrorParams{ + ID: id, LeaseToken: token, PostedRev: mark.PostedRev, State: mark.State, + CommentID: int64(mark.CommentID), CommentKey: mark.CommentKey, CommentUrl: mark.URL, + LastError: mark.Err, Recheck: mark.Recheck, ClearRecheck: mark.ClearRecheck, + }) + if err != nil { + return fmt.Errorf("store: mark forge mirror: %w", err) + } + if n == 0 { + return fmt.Errorf("%w: forge mirror %q", ErrLeaseLost, id) + } + return nil +} + +func insertAskMirrors(ctx context.Context, q *db.Queries, msg Message, author AccountID) error { + hasMirrorAsk := false + for _, block := range msg.Blocks { + if block.Ask != nil && block.Ask.RefCoord != nil { + hasMirrorAsk = true + break + } + } + if !hasMirrorAsk { + return nil + } + isAgent, err := q.IsAgentAccount(ctx, string(author)) + if err != nil { + return fmt.Errorf("store: identify ask mirror author: %w", err) + } + if !isAgent { + return nil + } + for _, block := range msg.Blocks { + if block.Ask == nil || block.Ask.RefCoord == nil { + continue + } + coord := block.Ask.RefCoord.Normalized() + if err := validCoordinate(coord.Provider, coord.Host, coord.Repo); err != nil { + slog.WarnContext(ctx, "skip ask mirror with invalid forge coordinate", "ask_id", block.Ask.AskID, "error", err) + continue + } + mode, err := q.ForgeMirrorMode(ctx, db.ForgeMirrorModeParams{ + ForgeProvider: int16(coord.Provider), //nolint:gosec // G115: provider is a CHECK-constrained 1..4 enum. + ForgeHost: coord.Host, + Repo: coord.Repo, + }) + if err != nil { + return fmt.Errorf("store: read ask forge mirror mode: %w", err) + } + if ForgeMirrorMode(mode) != ForgeMirrorAuto { + continue + } + if err := q.InsertAskForgeMirror(ctx, db.InsertAskForgeMirrorParams{ + ID: newID(), AskID: block.Ask.AskID, ActorAccountID: string(author), TargetRef: block.Ask.Ref, + }); err != nil { + return fmt.Errorf("store: insert ask forge mirror: %w", err) + } + } + return nil +} + +func bumpAskMirror(ctx context.Context, q *db.Queries, askID string) error { + if err := q.BumpAskForgeMirror(ctx, askID); err != nil { + return fmt.Errorf("store: bump ask forge mirror: %w", err) + } + return nil +} + +func nullableString(value string) pgtype.Text { + return pgtype.Text{String: value, Valid: value != ""} +} + +func optionalTime(value pgtype.Timestamptz) *time.Time { + if !value.Valid { + return nil + } + at := value.Time + return &at +} + +// forgeMirrorFromDB converts any forge-mirror RETURNING row; sqlc emits one +// identical struct per query, so callers convert to this shape. +func forgeMirrorFromDB(row db.ClaimForgeMirrorRow) ForgeMirrorRow { + return ForgeMirrorRow{ + ID: row.ID, Source: row.Source, AskID: row.AskID, SourceKey: row.SourceKey, + Actor: AccountID(row.ActorAccountID), Agent: AccountID(row.AgentAccountID), + ClientRequestID: row.ClientRequestID.String, TargetRef: row.TargetRef, + BodyRev: row.BodyRev, PostedRev: row.PostedRev, State: row.State, Attempts: int(row.Attempts), + CommentID: uint64(row.CommentID), //nolint:gosec // G115: written only from a uint64 checked against MaxInt64 in MarkForgeMirror + CommentKey: row.CommentKey, CommentURL: row.CommentUrl, LastError: row.LastError, + FirstAttemptAt: optionalTime(row.FirstAttemptAt), LastAttemptAt: optionalTime(row.LastAttemptAt), + ClientCommentID: row.ClientCommentID, RecheckAt: optionalTime(row.RecheckAt), LeaseUntil: optionalTime(row.LeaseUntil), + LeaseToken: row.LeaseToken, UpdatedAt: row.UpdatedAt.Time, + } +} diff --git a/go/internal/store/forge_mirrors_pgtest_test.go b/go/internal/store/forge_mirrors_pgtest_test.go new file mode 100644 index 000000000..c763eb304 --- /dev/null +++ b/go/internal/store/forge_mirrors_pgtest_test.go @@ -0,0 +1,662 @@ +//go:build pgtest + +package store + +import ( + "context" + "errors" + "testing" + "time" +) + +func appendAskForMirrorTest(t *testing.T, ctx context.Context, s *Store, author AccountID, ask Ask, requestID string) (Message, bool) { + t.Helper() + ch, err := s.CreateChannel(ctx, author, NewChannel{Name: "mirror-" + newID(), Kind: ChannelKindChannel, MemberAccountIDs: []AccountID{author}}) + if err != nil { + t.Fatalf("CreateChannel: %v", err) + } + msg, inserted, err := s.AppendMessage(ctx, Message{ + AuthorAccountID: author, + Blocks: []MessageBlock{{Ask: &ask}}, + }, string(ch.ID), TopicRef{Name: testTopic, Create: true}, requestID) + if err != nil { + t.Fatalf("AppendMessage: %v", err) + } + return msg, inserted +} + +func mirrorTestAsk(ref string, coord *ForgeCoord) Ask { + return Ask{ + Ref: ref, + RefCoord: coord, + Questions: []AskQuestion{{ + QuestionID: "q1", + Question: "Question?", + Options: []AskOption{{ID: "yes", Label: "Yes"}}, + }}, + } +} + +func mirrorRowCount(t *testing.T, s *Store, ctx context.Context, agent AccountID) int { + t.Helper() + var count int + if err := s.pool.QueryRow(ctx, `SELECT count(*) FROM forge_mirrors WHERE agent_account_id = $1`, string(agent)).Scan(&count); err != nil { + t.Fatalf("count forge mirrors: %v", err) + } + return count +} + +func TestAskMirrorAppendAutoManualOffUserAndRetry(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + owner := mustUser(t, s, "mirror-append-owner") + agent := mustAgent(t, s, owner.ID, "mirror-append-agent") + coord := ForgeCoord{Provider: ForgeProviderLinear, Host: "linear.app", Repo: "RIG", Number: 1} + if err := s.EnsureForgeRepoSubscription(ctx, ForgeRepoSubscription{Provider: coord.Provider, Host: coord.Host, Repo: coord.Repo, Enabled: true}); err != nil { + t.Fatalf("EnsureForgeRepoSubscription: %v", err) + } + + for _, tc := range []struct { + name string + mode ForgeMirrorMode + want int + }{ + {name: "auto", mode: ForgeMirrorAuto, want: 1}, + {name: "manual", mode: ForgeMirrorManual}, + {name: "off", mode: ForgeMirrorOff}, + } { + t.Run(tc.name, func(t *testing.T) { + if err := s.SetForgeMirrorSetting(ctx, owner.ID, ForgeMirrorSetting{Provider: coord.Provider, Host: coord.Host, Repo: coord.Repo, Mode: tc.mode}); err != nil { + t.Fatalf("SetForgeMirrorSetting: %v", err) + } + msg, inserted := appendAskForMirrorTest(t, ctx, s, agent.ID, mirrorTestAsk("RIG-12", &coord), "") + if !inserted { + t.Fatal("AppendMessage inserted=false, want true") + } + rows, err := s.AskForgeMirrors(ctx, []string{msg.Blocks[0].Ask.AskID}) + if err != nil { + t.Fatalf("AskForgeMirrors: %v", err) + } + if len(rows) != tc.want { + t.Fatalf("ask mirror count = %d, want %d", len(rows), tc.want) + } + if tc.want == 1 { + row := rows[0] + if row.Source != 1 || row.AskID != msg.Blocks[0].Ask.AskID || row.Actor != agent.ID || row.Agent != agent.ID || row.TargetRef != "RIG-12" || row.BodyRev != 1 || row.State != 1 { + t.Fatalf("auto mirror row = %+v, unexpected fields", row) + } + } + }) + } + + if err := s.SetForgeMirrorSetting(ctx, owner.ID, ForgeMirrorSetting{Provider: coord.Provider, Host: coord.Host, Repo: coord.Repo, Mode: ForgeMirrorMode(-1)}); !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("negative mode error = %v, want ErrInvalidArgument", err) + } + if err := s.SetForgeMirrorSetting(ctx, owner.ID, ForgeMirrorSetting{Provider: coord.Provider, Host: coord.Host, Repo: coord.Repo, Mode: ForgeMirrorAuto}); err != nil { + t.Fatalf("SetForgeMirrorSetting(auto): %v", err) + } + userMsg, inserted := appendAskForMirrorTest(t, ctx, s, owner.ID, mirrorTestAsk("RIG-12", &coord), "user-ask") + if !inserted { + t.Fatal("AppendMessage(user) inserted=false, want true") + } + rows, err := s.AskForgeMirrors(ctx, []string{userMsg.Blocks[0].Ask.AskID}) + if err != nil { + t.Fatalf("AskForgeMirrors(user): %v", err) + } + if len(rows) != 0 { + t.Fatalf("user-authored ask created %d mirror rows, want none", len(rows)) + } + + retryCoord := ForgeCoord{Provider: ForgeProviderGitHub, Host: "github.com", Repo: "owner/repo", Number: 12} + if err := s.SetForgeMirrorSetting(ctx, owner.ID, ForgeMirrorSetting{Provider: retryCoord.Provider, Host: retryCoord.Host, Repo: retryCoord.Repo, Mode: ForgeMirrorAuto}); err != nil { + t.Fatalf("SetForgeMirrorSetting(retry): %v", err) + } + if err := s.EnsureForgeRepoSubscription(ctx, ForgeRepoSubscription{Provider: retryCoord.Provider, Host: retryCoord.Host, Repo: retryCoord.Repo, Enabled: true}); err != nil { + t.Fatalf("EnsureForgeRepoSubscription(retry): %v", err) + } + ask := mirrorTestAsk("owner/repo#12", &retryCoord) + ch, err := s.CreateChannel(ctx, owner.ID, NewChannel{Name: "mirror-retry-" + newID(), Kind: ChannelKindChannel, MemberAccountIDs: []AccountID{owner.ID, agent.ID}}) + if err != nil { + t.Fatalf("CreateChannel(retry): %v", err) + } + msg, inserted, err := s.AppendMessage(ctx, Message{AuthorAccountID: agent.ID, Blocks: []MessageBlock{{Ask: &ask}}}, string(ch.ID), TopicRef{Name: testTopic, Create: true}, "same-append-request") + if err != nil || !inserted { + t.Fatalf("AppendMessage(first) = (%t, %v), want inserted", inserted, err) + } + _, inserted, err = s.AppendMessage(ctx, Message{AuthorAccountID: agent.ID, Blocks: []MessageBlock{{Ask: &ask}}}, string(ch.ID), TopicRef{Name: testTopic, Create: true}, "same-append-request") + if err != nil { + t.Fatalf("AppendMessage(retry): %v", err) + } + if inserted { + t.Fatal("retry AppendMessage inserted=true, want false") + } + rows, err = s.AskForgeMirrors(ctx, []string{msg.Blocks[0].Ask.AskID}) + if err != nil { + t.Fatalf("AskForgeMirrors(retry): %v", err) + } + if len(rows) != 1 { + t.Fatalf("retry created %d mirror rows, want one", len(rows)) + } +} + +func TestAskMirrorAnswerBumpsPendingRevision(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + owner := mustUser(t, s, "mirror-answer-owner") + agent := mustAgent(t, s, owner.ID, "mirror-answer-agent") + coord := ForgeCoord{Provider: ForgeProviderGitHub, Host: "github.com", Repo: "owner/repo", Number: 12} + if err := s.SetForgeMirrorSetting(ctx, owner.ID, ForgeMirrorSetting{Provider: coord.Provider, Host: coord.Host, Repo: coord.Repo, Mode: ForgeMirrorAuto}); err != nil { + t.Fatalf("SetForgeMirrorSetting: %v", err) + } + ch, err := s.CreateChannel(ctx, owner.ID, NewChannel{Name: "mirror-answer-" + newID(), Kind: ChannelKindChannel, MemberAccountIDs: []AccountID{owner.ID, agent.ID}}) + if err != nil { + t.Fatalf("CreateChannel: %v", err) + } + msg, _, err := s.AppendMessage(ctx, Message{AuthorAccountID: agent.ID, Blocks: []MessageBlock{{Ask: &Ask{ + Ref: "owner/repo#12", RefCoord: &coord, + Questions: []AskQuestion{{QuestionID: "q1", Question: "Question?", Options: []AskOption{{ID: "yes", Label: "Yes"}}}}, + }}}}, string(ch.ID), TopicRef{Name: testTopic, Create: true}, "") + if err != nil { + t.Fatalf("AppendMessage: %v", err) + } + askID := msg.Blocks[0].Ask.AskID + if _, _, err := s.AnswerAsk(ctx, owner.ID, askID, []AskAnswer{{QuestionID: "q1", ChosenOptionIDs: []string{"yes"}}}); err != nil { + t.Fatalf("AnswerAsk: %v", err) + } + rows, err := s.AskForgeMirrors(ctx, []string{askID}) + if err != nil { + t.Fatalf("AskForgeMirrors: %v", err) + } + if len(rows) != 1 || rows[0].BodyRev != 2 || rows[0].State != 1 { + t.Fatalf("answer mirror rows = %+v, want one body_rev=2 pending row", rows) + } +} + +func TestClaimForgeMirrorConflictKeys(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + agent, owner := seedAgent(t, s, "mirror-claim") + base := ForgeMirrorRow{Source: 1, AskID: "claim-ask", Actor: agent, Agent: agent, TargetRef: "RIG-1", BodyRev: 1, State: 1} + created, ok, err := s.ClaimForgeMirror(ctx, base) + if err != nil || !ok { + t.Fatalf("ClaimForgeMirror(first) = (%+v, %t, %v), want created", created, ok, err) + } + conflict := base + conflict.TargetRef = "RIG-2" + got, ok, err := s.ClaimForgeMirror(ctx, conflict) + if err != nil || ok || got.ID != created.ID || got.TargetRef != "RIG-1" { + t.Fatalf("ask conflict = (%+v, %t, %v), want stored row unchanged", got, ok, err) + } + + request := ForgeMirrorRow{Source: 2, SourceKey: "request-key", Actor: owner, Agent: agent, ClientRequestID: "client-1", TargetRef: "owner/repo#1", BodyRev: 1, State: 1} + requestRow, ok, err := s.ClaimForgeMirror(ctx, request) + if err != nil || !ok { + t.Fatalf("ClaimForgeMirror(request) = (%+v, %t, %v), want created", requestRow, ok, err) + } + request.ClientRequestID = "client-1" + request.TargetRef = "owner/repo#2" + got, ok, err = s.ClaimForgeMirror(ctx, request) + if err != nil || ok || got.ID != requestRow.ID || got.TargetRef != "owner/repo#1" { + t.Fatalf("client request conflict = (%+v, %t, %v), want stored row unchanged", got, ok, err) + } + + source := ForgeMirrorRow{Source: 2, SourceKey: "topic-key", Actor: owner, Agent: agent, TargetRef: "owner/repo#3", BodyRev: 1, State: 2} + sourceRow, ok, err := s.ClaimForgeMirror(ctx, source) + if err != nil || !ok { + t.Fatalf("ClaimForgeMirror(source) = (%+v, %t, %v), want created", sourceRow, ok, err) + } + source.BodyRev = 1 + source.State = 1 + got, ok, err = s.ClaimForgeMirror(ctx, source) + if err != nil || ok || got.ID != sourceRow.ID || got.BodyRev != 2 || got.State != 1 { + t.Fatalf("source-key conflict = (%+v, %t, %v), want same row requeued at rev 2", got, ok, err) + } + if n := mirrorRowCount(t, s, ctx, agent); n != 3 { + t.Fatalf("mirror row count = %d, want 3", n) + } +} + +func TestForgeMirrorLeasesAttemptsAndMarks(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + agent, owner := seedAgent(t, s, "mirror-lease") + row, created, err := s.ClaimForgeMirror(ctx, ForgeMirrorRow{Source: 2, SourceKey: "lease-key", Actor: owner, Agent: agent, TargetRef: "owner/repo#1", BodyRev: 1, State: 1}) + if err != nil || !created { + t.Fatalf("ClaimForgeMirror: (%+v, %t, %v)", row, created, err) + } + leased, ok, err := s.LeaseForgeMirror(ctx, row.ID, "token-1", time.Minute) + if err != nil || !ok || leased.LeaseToken != "token-1" { + t.Fatalf("LeaseForgeMirror(first) = (%+v, %t, %v)", leased, ok, err) + } + if _, ok, err := s.LeaseForgeMirror(ctx, row.ID, "token-2", time.Minute); err != nil || ok { + t.Fatalf("LeaseForgeMirror(held) = (%t, %v), want false", ok, err) + } + if ok, err := s.RenewForgeMirror(ctx, row.ID, "stale", time.Minute); err != nil || ok { + t.Fatalf("RenewForgeMirror(stale) = (%t, %v), want false", ok, err) + } + firstAttempt, ok, err := s.BeginForgeMirrorAttempt(ctx, row.ID, "token-1") + if err != nil || !ok || firstAttempt.Attempts != 1 || firstAttempt.FirstAttemptAt == nil { + t.Fatalf("BeginForgeMirrorAttempt(first) = (%+v, %t, %v)", firstAttempt, ok, err) + } + secondAttempt, ok, err := s.BeginForgeMirrorAttempt(ctx, row.ID, "token-1") + if err != nil || !ok || secondAttempt.Attempts != 2 || secondAttempt.FirstAttemptAt == nil || secondAttempt.LastAttemptAt == nil || !secondAttempt.FirstAttemptAt.Equal(*firstAttempt.FirstAttemptAt) { + t.Fatalf("BeginForgeMirrorAttempt(second) = (%+v, %t, %v)", secondAttempt, ok, err) + } + if _, err := s.pool.Exec(ctx, `UPDATE forge_mirrors SET lease_until = now() - interval '1 second' WHERE id = $1`, row.ID); err != nil { + t.Fatalf("expire lease: %v", err) + } + leased, ok, err = s.LeaseForgeMirror(ctx, row.ID, "token-2", time.Minute) + if err != nil || !ok || leased.LeaseToken != "token-2" { + t.Fatalf("LeaseForgeMirror(expired) = (%+v, %t, %v)", leased, ok, err) + } + if err := s.MarkForgeMirror(ctx, row.ID, "token-1", ForgeMirrorMark{PostedRev: 1, State: 2, CommentID: 11, CommentKey: "key", URL: "url"}); !errors.Is(err, ErrLeaseLost) { + t.Fatalf("MarkForgeMirror(stale) = %v, want ErrLeaseLost", err) + } + assertMirrorDelivery(t, s, row.ID, 1, "", "token-2") + if err := s.MarkForgeMirror(ctx, row.ID, "token-2", ForgeMirrorMark{PostedRev: 1, State: 2, CommentID: 11, CommentKey: "key", URL: "url", Recheck: true}); err != nil { + t.Fatalf("MarkForgeMirror(valid): %v", err) + } + assertMirrorDelivery(t, s, row.ID, 2, "key", "") + if done, err := s.AskForgeMirrors(ctx, nil); err != nil || len(done) != 0 { + t.Fatalf("AskForgeMirrors(empty) = (%d, %v), want none", len(done), err) + } +} + +func assertMirrorDelivery(t *testing.T, s *Store, id string, state int16, commentKey, token string) { + t.Helper() + var gotState int16 + var gotKey, gotToken string + if err := s.pool.QueryRow(context.Background(), `SELECT state, comment_key, lease_token FROM forge_mirrors WHERE id = $1`, id).Scan(&gotState, &gotKey, &gotToken); err != nil { + t.Fatalf("read forge mirror %s: %v", id, err) + } + if gotState != state || gotKey != commentKey || gotToken != token { + t.Fatalf("forge mirror %s = state %d key %q token %q, want %d %q %q", id, gotState, gotKey, gotToken, state, commentKey, token) + } +} + +// OpenForgeMirrors lists pending and failed rows, plus done rows once recheck is due. +func TestForgeMirrorOpenRows(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + agent, owner := seedAgent(t, s, "mirror-lease") + row, created, err := s.ClaimForgeMirror(ctx, ForgeMirrorRow{Source: 2, SourceKey: "lease-key", Actor: owner, Agent: agent, TargetRef: "owner/repo#1", BodyRev: 1, State: 1}) + if err != nil || !created { + t.Fatalf("ClaimForgeMirror: (%+v, %t, %v)", row, created, err) + } + pendingRow, _, err := s.ClaimForgeMirror(ctx, ForgeMirrorRow{Source: 2, SourceKey: "pending-key", Actor: owner, Agent: agent, TargetRef: "owner/repo#2", BodyRev: 1, State: 1}) + if err != nil { + t.Fatalf("ClaimForgeMirror(pending): %v", err) + } + failedRow, _, err := s.ClaimForgeMirror(ctx, ForgeMirrorRow{Source: 2, SourceKey: "failed-key", Actor: owner, Agent: agent, TargetRef: "owner/repo#3", BodyRev: 1, State: 1}) + if err != nil { + t.Fatalf("ClaimForgeMirror(failed): %v", err) + } + if _, ok, err := s.LeaseForgeMirror(ctx, failedRow.ID, "failed-token", time.Minute); err != nil || !ok { + t.Fatalf("LeaseForgeMirror(failed): (%t, %v)", ok, err) + } + if err := s.MarkForgeMirror(ctx, failedRow.ID, "failed-token", ForgeMirrorMark{State: 3, Err: "failed"}); err != nil { + t.Fatalf("MarkForgeMirror(failed): %v", err) + } + if _, ok, err := s.LeaseForgeMirror(ctx, row.ID, "token-2", time.Minute); err != nil || !ok { + t.Fatalf("LeaseForgeMirror: (%t, %v)", ok, err) + } + if err := s.MarkForgeMirror(ctx, row.ID, "token-2", ForgeMirrorMark{PostedRev: 1, State: 2, CommentID: 11, CommentKey: "key", URL: "url", Recheck: true}); err != nil { + t.Fatalf("MarkForgeMirror: %v", err) + } + open, err := s.OpenForgeMirrors(ctx, agent) + if err != nil || len(open) != 2 || open[0].ID != pendingRow.ID || open[0].State != 1 || open[1].ID != failedRow.ID || open[1].State != 3 { + t.Fatalf("OpenForgeMirrors(pending and failed) = (%+v, %v), want the pending and failed rows", open, err) + } + if _, err := s.pool.Exec(ctx, `UPDATE forge_mirrors SET recheck_at = now() - interval '1 second' WHERE id = $1`, row.ID); err != nil { + t.Fatalf("expire recheck: %v", err) + } + open, err = s.OpenForgeMirrors(ctx, agent) + if err != nil || len(open) != 3 || open[0].ID != row.ID || open[1].ID != pendingRow.ID || open[2].ID != failedRow.ID || open[0].LeaseToken != "" || open[0].CommentKey != "key" { + t.Fatalf("OpenForgeMirrors(past recheck) = (%+v, %v), want the done, pending, and failed rows", open, err) + } + if _, ok, err := s.LeaseForgeMirror(ctx, row.ID, "token-4", time.Minute); err != nil || !ok { + t.Fatalf("LeaseForgeMirror(past recheck) = (%t, %v), want true", ok, err) + } +} + +func TestForgeMirrorSettingsAndSubscriptionList(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + owner := mustUser(t, s, "mirror-settings-owner") + enabled := ForgeRepoSubscription{Provider: ForgeProviderGitHub, Host: "github.com", Repo: "owner/enabled", Enabled: true} + disabled := ForgeRepoSubscription{Provider: ForgeProviderLinear, Host: "linear.app", Repo: "DISABLED", Enabled: false} + if err := s.EnsureForgeRepoSubscription(ctx, enabled); err != nil { + t.Fatalf("EnsureForgeRepoSubscription(enabled): %v", err) + } + if err := s.EnsureForgeRepoSubscription(ctx, disabled); err != nil { + t.Fatalf("EnsureForgeRepoSubscription(disabled): %v", err) + } + if err := s.SetForgeMirrorSetting(ctx, owner.ID, ForgeMirrorSetting{Provider: disabled.Provider, Host: disabled.Host, Repo: disabled.Repo, Mode: ForgeMirrorAuto}); err != nil { + t.Fatalf("SetForgeMirrorSetting(disabled): %v", err) + } + settings, err := s.ListForgeMirrorSettings(ctx) + if err != nil || len(settings) != 1 || settings[0].Mode != ForgeMirrorOff || settings[0].Repo != enabled.Repo { + t.Fatalf("ListForgeMirrorSettings(default off) = (%+v, %v), want enabled repo only", settings, err) + } + if err := s.SetForgeMirrorSetting(ctx, owner.ID, ForgeMirrorSetting{Provider: enabled.Provider, Host: enabled.Host, Repo: enabled.Repo, Mode: ForgeMirrorAuto}); err != nil { + t.Fatalf("SetForgeMirrorSetting(auto): %v", err) + } + settings, err = s.ListForgeMirrorSettings(ctx) + if err != nil || len(settings) != 1 || settings[0].Mode != ForgeMirrorAuto { + t.Fatalf("ListForgeMirrorSettings(auto) = (%+v, %v), want Auto", settings, err) + } + if err := s.SetForgeMirrorSetting(ctx, owner.ID, ForgeMirrorSetting{Provider: enabled.Provider, Host: enabled.Host, Repo: enabled.Repo, Mode: ForgeMirrorOff}); err != nil { + t.Fatalf("SetForgeMirrorSetting(off): %v", err) + } + var count int + if err := s.pool.QueryRow(ctx, `SELECT count(*) FROM forge_mirror_settings WHERE forge_provider = $1 AND forge_host = $2 AND repo = $3`, int16(enabled.Provider), enabled.Host, enabled.Repo).Scan(&count); err != nil || count != 0 { + t.Fatalf("Off left %d setting rows (err=%v), want zero", count, err) + } + if err := s.SetForgeMirrorSetting(ctx, owner.ID, ForgeMirrorSetting{Provider: enabled.Provider, Host: enabled.Host, Repo: enabled.Repo, Mode: ForgeMirrorMode(3)}); !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("invalid mode error = %v, want ErrInvalidArgument", err) + } + if mode, err := s.ForgeMirrorMode(ctx, enabled.Provider, enabled.Host, enabled.Repo); err != nil || mode != ForgeMirrorOff { + t.Fatalf("ForgeMirrorMode(after off) = (%d, %v), want Off", mode, err) + } +} + +func TestForgeMirrorTenantIsolation(t *testing.T) { + s := newTestStore(t) + tenantA := seedTenant(t, s, "mirror-tenant-a") + ctxA := WithTenant(context.Background(), tenantA) + ctxB := WithTenant(context.Background(), seedTenant(t, s, "mirror-tenant-b")) + owner, err := s.CreateUser(ctxA, NewUser{Handle: "mirror-tenant-owner", DisplayName: "owner"}) + if err != nil { + t.Fatalf("CreateUser(A): %v", err) + } + agent, err := s.CreateAgent(ctxA, owner.ID, NewAgent{Handle: "mirror-tenant-agent", DisplayName: "agent"}) + if err != nil { + t.Fatalf("CreateAgent(A): %v", err) + } + coord := ForgeCoord{Provider: ForgeProviderGitHub, Host: "github.com", Repo: "owner/repo"} + setting := ForgeMirrorSetting{Provider: coord.Provider, Host: coord.Host, Repo: coord.Repo, Mode: ForgeMirrorAuto} + if err := s.SetForgeMirrorSetting(ctxA, owner.ID, setting); err != nil { + t.Fatalf("SetForgeMirrorSetting(A): %v", err) + } + row, created, err := s.ClaimForgeMirror(ctxA, ForgeMirrorRow{Source: 2, SourceKey: "tenant-key", AskID: "tenant-ask", Actor: owner.ID, Agent: agent.ID, TargetRef: "owner/repo#1", BodyRev: 1, State: 1}) + if err != nil || !created { + t.Fatalf("ClaimForgeMirror(A): (%+v, %t, %v)", row, created, err) + } + if mode, err := s.ForgeMirrorMode(ctxB, coord.Provider, coord.Host, coord.Repo); err != nil || mode != ForgeMirrorOff { + t.Fatalf("ForgeMirrorMode(B) = (%d, %v), want Off", mode, err) + } + if rows, err := s.OpenForgeMirrors(ctxB, agent.ID); err != nil || len(rows) != 0 { + t.Fatalf("OpenForgeMirrors(B) = (%+v, %v), want no rows", rows, err) + } + if rows, err := s.AskForgeMirrors(ctxB, nil); err != nil || len(rows) != 0 { + t.Fatalf("AskForgeMirrors(B) = (%+v, %v), want no rows", rows, err) + } +} + +func TestAskForForgeMirrorRequiresParticipationAndReturnsAnswerer(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + owner, err := s.CreateUser(ctx, NewUser{Handle: "ask-mirror-owner", DisplayName: "Owner"}) + if err != nil { + t.Fatalf("CreateUser: %v", err) + } + agent, err := s.CreateAgent(ctx, owner.ID, NewAgent{Handle: "ask-mirror-agent", DisplayName: "Agent"}) + if err != nil { + t.Fatalf("CreateAgent: %v", err) + } + message, inserted := appendAskForMirrorTest(t, ctx, s, agent.ID, mirrorTestAsk("RIG-12", nil), "ask-mirror-request") + if !inserted { + t.Fatal("AppendMessage inserted=false") + } + askID := message.Blocks[0].Ask.AskID + if _, _, err := s.AnswerAsk(ctx, owner.ID, askID, []AskAnswer{{QuestionID: "q1", ChosenOptionIDs: []string{"yes"}}}); err != nil { + t.Fatalf("AnswerAsk: %v", err) + } + got, err := s.AskForForgeMirror(ctx, agent.ID, askID) + if err != nil { + t.Fatalf("AskForForgeMirror: %v", err) + } + if got.Message.ID != message.ID || got.Channel == "" || got.Topic == "" || got.AnsweredBy != "ask-mirror-owner" { + t.Fatalf("AskForForgeMirror = %+v, want original ask, source names, and owner answerer", got) + } + stranger, err := s.CreateUser(ctx, NewUser{Handle: "ask-mirror-stranger", DisplayName: "Stranger"}) + if err != nil { + t.Fatalf("CreateUser(stranger): %v", err) + } + if _, err := s.AskForForgeMirror(ctx, stranger.ID, askID); err == nil { + t.Fatal("AskForForgeMirror allowed an account outside the channel") + } + if _, err := s.AskForForgeMirror(ctx, agent.ID, "missing-ask"); err == nil { + t.Fatal("AskForForgeMirror returned no error for a missing ask") + } +} +func TestMarkForgeMirrorKeepsPendingAfterRevisionBump(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + agent, owner := seedAgent(t, s, "mirror-revision-race") + row, created, err := s.ClaimForgeMirror(ctx, ForgeMirrorRow{ + Source: 2, SourceKey: "revision-race", Actor: owner, Agent: agent, + TargetRef: "owner/repo#1", BodyRev: 1, State: 1, + }) + if err != nil || !created { + t.Fatalf("ClaimForgeMirror = (created=%v, err=%v), want created row", created, err) + } + if _, ok, err := s.LeaseForgeMirror(ctx, row.ID, "revision-token", time.Minute); err != nil || !ok { + t.Fatalf("LeaseForgeMirror = (ok=%v, err=%v), want leased row", ok, err) + } + if _, _, err := s.ClaimForgeMirror(ctx, ForgeMirrorRow{ + Source: 2, SourceKey: "revision-race", Actor: owner, Agent: agent, + TargetRef: "owner/repo#1", BodyRev: 1, State: 1, + }); err != nil { + t.Fatalf("ClaimForgeMirror(revision bump): %v", err) + } + if err := s.MarkForgeMirror(ctx, row.ID, "revision-token", ForgeMirrorMark{ + PostedRev: 1, State: 2, CommentID: 7, CommentKey: "comment", + }); err != nil { + t.Fatalf("MarkForgeMirror: %v", err) + } + var bodyRev, postedRev, state int16 + if err := s.pool.QueryRow(ctx, `SELECT body_rev, posted_rev, state FROM forge_mirrors WHERE id = $1`, row.ID).Scan(&bodyRev, &postedRev, &state); err != nil { + t.Fatalf("read forge mirror: %v", err) + } + if bodyRev != 2 || postedRev != 1 || state != 1 { + t.Fatalf("mirror revision state = (%d, %d, %d), want (2, 1, 1)", bodyRev, postedRev, state) + } +} + +func TestMarkForgeMirrorPreservesRecheckUntilExplicitClear(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + agent, owner := seedAgent(t, s, "mirror-recheck-preserve") + row, created, err := s.ClaimForgeMirror(ctx, ForgeMirrorRow{ + Source: 2, SourceKey: "recheck-preserve", Actor: owner, Agent: agent, + TargetRef: "owner/repo#1", BodyRev: 1, State: 1, + }) + if err != nil || !created { + t.Fatalf("ClaimForgeMirror = (created=%v, err=%v), want created row", created, err) + } + if _, ok, err := s.LeaseForgeMirror(ctx, row.ID, "recheck-token-1", time.Minute); err != nil || !ok { + t.Fatalf("LeaseForgeMirror(first) = (ok=%v, err=%v)", ok, err) + } + if err := s.MarkForgeMirror(ctx, row.ID, "recheck-token-1", ForgeMirrorMark{ + PostedRev: 1, State: 2, CommentID: 7, CommentKey: "comment", Recheck: true, + }); err != nil { + t.Fatalf("MarkForgeMirror(schedule recheck): %v", err) + } + if _, _, err := s.ClaimForgeMirror(ctx, ForgeMirrorRow{ + Source: 2, SourceKey: "recheck-preserve", Actor: owner, Agent: agent, + TargetRef: "owner/repo#1", BodyRev: 1, State: 1, + }); err != nil { + t.Fatalf("ClaimForgeMirror(revision bump): %v", err) + } + if _, ok, err := s.LeaseForgeMirror(ctx, row.ID, "recheck-token-2", time.Minute); err != nil || !ok { + t.Fatalf("LeaseForgeMirror(second) = (ok=%v, err=%v)", ok, err) + } + if err := s.MarkForgeMirror(ctx, row.ID, "recheck-token-2", ForgeMirrorMark{ + PostedRev: 2, State: 2, CommentID: 7, CommentKey: "comment", + }); err != nil { + t.Fatalf("MarkForgeMirror(delivery): %v", err) + } + var recheckSet bool + if err := s.pool.QueryRow(ctx, `SELECT recheck_at IS NOT NULL FROM forge_mirrors WHERE id = $1`, row.ID).Scan(&recheckSet); err != nil { + t.Fatalf("read recheck state: %v", err) + } + if !recheckSet { + t.Fatal("delivery cleared a scheduled recheck") + } + if _, err := s.pool.Exec(ctx, `UPDATE forge_mirrors SET recheck_at = now() - interval '1 second' WHERE id = $1`, row.ID); err != nil { + t.Fatalf("make scheduled recheck due: %v", err) + } + if _, ok, err := s.LeaseForgeMirror(ctx, row.ID, "recheck-token-3", time.Minute); err != nil || !ok { + t.Fatalf("LeaseForgeMirror(third) = (ok=%v, err=%v)", ok, err) + } + if err := s.MarkForgeMirror(ctx, row.ID, "recheck-token-3", ForgeMirrorMark{ + PostedRev: 2, State: 2, CommentID: 7, CommentKey: "comment", ClearRecheck: true, + }); err != nil { + t.Fatalf("MarkForgeMirror(clear recheck): %v", err) + } + if err := s.pool.QueryRow(ctx, `SELECT recheck_at IS NOT NULL FROM forge_mirrors WHERE id = $1`, row.ID).Scan(&recheckSet); err != nil { + t.Fatalf("read cleared recheck state: %v", err) + } + if recheckSet { + t.Fatal("explicit recheck clear left a scheduled recheck") + } +} + +func TestClaimForgeMirrorSourceKeyDoesNotCrossAgentClass(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + agent, owner := seedAgent(t, s, "mirror-class-conflict") + manual, created, err := s.ClaimForgeMirror(ctx, ForgeMirrorRow{ + Source: 2, SourceKey: "manual-class", Actor: owner, + TargetRef: "owner/repo#1", BodyRev: 1, State: 1, + }) + if err != nil || !created { + t.Fatalf("ClaimForgeMirror(manual) = (created=%v, err=%v)", created, err) + } + otherOwner, err := s.CreateUser(ctx, NewUser{Handle: "mirror-class-other-owner", DisplayName: "Other"}) + if err != nil { + t.Fatalf("CreateUser(other owner): %v", err) + } + agentClaim, created, err := s.ClaimForgeMirror(ctx, ForgeMirrorRow{ + Source: 2, SourceKey: "manual-class", Actor: otherOwner.ID, Agent: agent, + TargetRef: "owner/repo#1", BodyRev: 1, State: 1, + }) + if err != nil || created || agentClaim.ID != manual.ID || agentClaim.Agent != "" || agentClaim.Actor != owner || agentClaim.BodyRev != 1 { + t.Fatalf("other actor conflict = (%+v, created=%v, err=%v), want unchanged row", agentClaim, created, err) + } + agentClassClaim, created, err := s.ClaimForgeMirror(ctx, ForgeMirrorRow{ + Source: 2, SourceKey: "manual-class", Actor: agent, Agent: agent, + TargetRef: "owner/repo#1", BodyRev: 1, State: 1, + }) + if err != nil || created || agentClassClaim.ID != manual.ID || agentClassClaim.Agent != "" || agentClassClaim.BodyRev != 1 { + t.Fatalf("agent class conflict = (%+v, created=%v, err=%v), want unchanged Manual row", agentClassClaim, created, err) + } + automatic, created, err := s.ClaimForgeMirror(ctx, ForgeMirrorRow{ + Source: 2, SourceKey: "automatic-class", Actor: agent, Agent: agent, + TargetRef: "owner/repo#2", BodyRev: 1, State: 1, + }) + if err != nil || !created { + t.Fatalf("ClaimForgeMirror(auto) = (created=%v, err=%v)", created, err) + } + userClaim, created, err := s.ClaimForgeMirror(ctx, ForgeMirrorRow{ + Source: 2, SourceKey: "automatic-class", Actor: owner, + TargetRef: "owner/repo#2", BodyRev: 1, State: 1, + }) + if err != nil || created || userClaim.ID != automatic.ID || userClaim.Agent != agent || userClaim.Actor != agent || userClaim.BodyRev != 1 { + t.Fatalf("user conflict = (%+v, created=%v, err=%v), want unchanged Auto row", userClaim, created, err) + } +} + +func TestClaimForgeMirrorSourceTwoRequiresSourceKey(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + _, owner := seedAgent(t, s, "mirror-source-key-required") + _, _, err := s.ClaimForgeMirror(ctx, ForgeMirrorRow{ + Source: 2, Actor: owner, ClientRequestID: "request-only", + TargetRef: "owner/repo#1", BodyRev: 1, State: 1, + }) + if !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("ClaimForgeMirror(request key only) error = %v, want ErrInvalidArgument", err) + } +} + +func TestForgeMirrorExpiredLeaseFencesAttemptsAndMarks(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + agent, owner := seedAgent(t, s, "mirror-expired-lease") + row, created, err := s.ClaimForgeMirror(ctx, ForgeMirrorRow{ + Source: 2, SourceKey: "expired-lease", Actor: owner, Agent: agent, + TargetRef: "owner/repo#1", BodyRev: 1, State: 1, + }) + if err != nil || !created { + t.Fatalf("ClaimForgeMirror = (created=%v, err=%v)", created, err) + } + if _, ok, err := s.LeaseForgeMirror(ctx, row.ID, "expired-token", time.Minute); err != nil || !ok { + t.Fatalf("LeaseForgeMirror = (ok=%v, err=%v)", ok, err) + } + if _, err := s.pool.Exec(ctx, `UPDATE forge_mirrors SET lease_until = now() - interval '1 second' WHERE id = $1`, row.ID); err != nil { + t.Fatalf("expire lease: %v", err) + } + if _, ok, err := s.BeginForgeMirrorAttempt(ctx, row.ID, "expired-token"); err != nil || ok { + t.Fatalf("BeginForgeMirrorAttempt(expired) = (ok=%v, err=%v), want false", ok, err) + } + if _, err := s.pool.Exec(ctx, `UPDATE forge_mirrors SET lease_until = now() + interval '1 minute' WHERE id = $1`, row.ID); err != nil { + t.Fatalf("renew test lease: %v", err) + } + if _, ok, err := s.BeginForgeMirrorAttempt(ctx, row.ID, "expired-token"); err != nil || !ok { + t.Fatalf("BeginForgeMirrorAttempt(live) = (ok=%v, err=%v), want true", ok, err) + } + if _, err := s.pool.Exec(ctx, `UPDATE forge_mirrors SET lease_until = now() - interval '1 second' WHERE id = $1`, row.ID); err != nil { + t.Fatalf("expire lease before mark: %v", err) + } + err = s.MarkForgeMirror(ctx, row.ID, "expired-token", ForgeMirrorMark{PostedRev: 1, State: 2}) + if !errors.Is(err, ErrLeaseLost) { + t.Fatalf("MarkForgeMirror(expired) error = %v, want ErrLeaseLost", err) + } +} + +func TestAskMirrorInvalidCoordinateDoesNotRejectMessage(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + owner := mustUser(t, s, "mirror-invalid-coordinate-owner") + agent := mustAgent(t, s, owner.ID, "mirror-invalid-coordinate-agent") + coord := ForgeCoord{Provider: ForgeProviderGitHub, Host: "", Repo: "owner/repo"} + msg, inserted := appendAskForMirrorTest(t, ctx, s, agent.ID, mirrorTestAsk("owner/repo#1", &coord), "invalid-coordinate") + if !inserted || msg.ID == "" { + t.Fatalf("AppendMessage = (inserted=%v, id=%q), want persisted message", inserted, msg.ID) + } + rows, err := s.AskForgeMirrors(ctx, []string{msg.Blocks[0].Ask.AskID}) + if err != nil || len(rows) != 0 { + t.Fatalf("AskForgeMirrors(invalid coordinate) = (%+v, %v), want no mirror rows", rows, err) + } +} + +func TestForgeMirrorSettingsNormalizeMixedCaseGitHubRepo(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + owner := mustUser(t, s, "mirror-settings-case-owner") + if err := s.EnsureForgeRepoSubscription(ctx, ForgeRepoSubscription{ + Provider: ForgeProviderGitHub, Host: "github.com", Repo: "Owner/MixedCase", Enabled: true, + }); err != nil { + t.Fatalf("EnsureForgeRepoSubscription: %v", err) + } + if err := s.SetForgeMirrorSetting(ctx, owner.ID, ForgeMirrorSetting{ + Provider: ForgeProviderGitHub, Host: "github.com", Repo: "Owner/MixedCase", Mode: ForgeMirrorAuto, + }); err != nil { + t.Fatalf("SetForgeMirrorSetting: %v", err) + } + settings, err := s.ListForgeMirrorSettings(ctx) + if err != nil { + t.Fatalf("ListForgeMirrorSettings: %v", err) + } + for _, setting := range settings { + if setting.Provider == ForgeProviderGitHub && setting.Repo == "owner/mixedcase" { + if setting.Mode != ForgeMirrorAuto { + t.Fatalf("mixed-case subscription mode = %d, want Auto", setting.Mode) + } + return + } + } + t.Fatalf("ListForgeMirrorSettings() = %+v, want normalized owner/mixedcase with Auto mode", settings) +} diff --git a/go/internal/store/messages.go b/go/internal/store/messages.go index c8fab1fe0..59ea7e731 100644 --- a/go/internal/store/messages.go +++ b/go/internal/store/messages.go @@ -101,6 +101,10 @@ func (s *Store) AppendMessage(ctx context.Context, m Message, channelID string, return Message{}, false, err } + if err := insertAskMirrors(ctx, db.New(tx), inserted, m.AuthorAccountID); err != nil { + return Message{}, false, err + } + if err := tx.Commit(ctx); err != nil { return Message{}, false, fmt.Errorf("store: commit append message: %w", err) } @@ -597,6 +601,9 @@ func (s *Store) AnswerAsk(ctx context.Context, actor AccountID, askID string, an if err != nil { return Message{}, Message{}, fmt.Errorf("store: insert answer message: %w", err) } + if err := bumpAskMirror(ctx, db.New(tx), askID); err != nil { + return Message{}, Message{}, err + } if err := tx.Commit(ctx); err != nil { return Message{}, Message{}, fmt.Errorf("store: commit answer ask: %w", err) } diff --git a/go/internal/store/migrations/0001_init.sql b/go/internal/store/migrations/0001_init.sql index a30bf74bd..728d7d764 100644 --- a/go/internal/store/migrations/0001_init.sql +++ b/go/internal/store/migrations/0001_init.sql @@ -1088,6 +1088,51 @@ CREATE TABLE forge_repo_subscriptions ( tenant_id TEXT NOT NULL DEFAULT current_setting('compass.tenant_id', TRUE), PRIMARY KEY (tenant_id, forge_provider, forge_host, repo) ); +-- User-controlled mirroring mode per tenant and forge coordinate; no row means off. +CREATE TABLE forge_mirror_settings ( + forge_provider SMALLINT NOT NULL CHECK (forge_provider IN (1, 2, 3, 4)), + forge_host TEXT NOT NULL, + repo TEXT NOT NULL CHECK (repo <> ''), + mode SMALLINT NOT NULL CHECK (mode IN (1, 2)), + updated_by TEXT NOT NULL REFERENCES user_accounts (account_id) ON DELETE RESTRICT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + tenant_id TEXT NOT NULL DEFAULT current_setting('compass.tenant_id', TRUE), + PRIMARY KEY (tenant_id, forge_provider, forge_host, repo) +); + +CREATE TABLE forge_mirrors ( + id TEXT PRIMARY KEY, + source SMALLINT NOT NULL CHECK (source IN (1, 2)), + ask_id TEXT NOT NULL DEFAULT '', + source_key TEXT NOT NULL DEFAULT '', + actor_account_id TEXT NOT NULL REFERENCES accounts (id) ON DELETE RESTRICT, + agent_account_id TEXT REFERENCES agent_accounts (account_id) ON DELETE RESTRICT, + client_request_id TEXT, + target_ref TEXT NOT NULL CHECK (target_ref <> ''), + body_rev SMALLINT NOT NULL DEFAULT 1, + posted_rev SMALLINT NOT NULL DEFAULT 0, + state SMALLINT NOT NULL DEFAULT 1 CHECK (state IN (1, 2, 3)), + attempts INT NOT NULL DEFAULT 0, + comment_id BIGINT NOT NULL DEFAULT 0, + comment_key TEXT NOT NULL DEFAULT '', + comment_url TEXT NOT NULL DEFAULT '', + last_error TEXT NOT NULL DEFAULT '', + first_attempt_at TIMESTAMPTZ, + last_attempt_at TIMESTAMPTZ, + client_comment_id UUID NOT NULL DEFAULT gen_random_uuid(), + recheck_at TIMESTAMPTZ, + lease_until TIMESTAMPTZ, + lease_token TEXT NOT NULL DEFAULT '', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + tenant_id TEXT NOT NULL DEFAULT current_setting('compass.tenant_id', TRUE) +); +CREATE UNIQUE INDEX forge_mirrors_ask_key ON forge_mirrors (tenant_id, ask_id) WHERE ask_id <> ''; +CREATE UNIQUE INDEX forge_mirrors_request_key ON forge_mirrors (tenant_id, agent_account_id, client_request_id) + WHERE client_request_id IS NOT NULL; +CREATE UNIQUE INDEX forge_mirrors_source_key ON forge_mirrors (tenant_id, source_key, target_ref) WHERE source_key <> ''; +CREATE INDEX forge_mirrors_open_idx ON forge_mirrors (agent_account_id) WHERE state <> 2 OR recheck_at IS NOT NULL; -- pull_requests holds each ingested PR whole; pull_request_issue_links maps PRs -- to the issues they work on (source 1 explicit, 2 closing reference). @@ -1674,6 +1719,7 @@ DECLARE 'agent_boards', 'agent_board_tasks', 'linear_agent_sessions', 'issues', 'forge_repo_subscriptions', 'forge_artifact_cursors', + 'forge_mirror_settings', 'forge_mirrors', 'forge_state_transitions', 'account_forge_scopes', 'pull_requests', 'pull_request_issue_links', 'token_usage_events', 'token_usage_rollups_hourly', 'token_usage_rollups_daily', @@ -1759,6 +1805,7 @@ DECLARE 'agent_config_bundle', 'model_registry', 'forge_repo_subscriptions', + 'forge_mirror_settings', 'forge_mirrors', 'forge_state_transitions', 'agent_boards', 'agent_board_tasks', 'pull_requests', diff --git a/go/internal/store/queries/forge_mirror_ask.sql b/go/internal/store/queries/forge_mirror_ask.sql new file mode 100644 index 000000000..2ebb01995 --- /dev/null +++ b/go/internal/store/queries/forge_mirror_ask.sql @@ -0,0 +1,52 @@ +-- name: AskForForgeMirror :one +WITH RECURSIVE chain AS ( + SELECT aa.account_id, aa.parent_agent_id + FROM agent_accounts aa + WHERE aa.account_id = sqlc.arg(account_id) + UNION + SELECT a.account_id, a.parent_agent_id + FROM agent_accounts a + JOIN chain ch ON a.account_id = ch.parent_agent_id +), participating AS ( + SELECT cm.channel_id FROM channel_members cm WHERE cm.account_id = sqlc.arg(account_id) + UNION + SELECT c.id FROM channels c + WHERE c.membership_mode = 1 + AND ( + c.parent_agent_id IN (SELECT account_id FROM chain) + OR c.parent_agent_id IN ( + SELECT aa.account_id FROM agent_accounts aa WHERE aa.owner_user_id = sqlc.arg(account_id) + ) + ) +) +SELECT m.id, m.topic_id, m.author_account_id, + (CASE WHEN ah.owner_user_id IS NULL THEN COALESCE(ah.handle, '') + WHEN oh.handle IS NULL THEN '' ELSE oh.handle || '/' || ah.handle END)::text AS author_handle, + m.at_unix_ms, m.blocks, m.turn_sequence, t.channel_id, c.name AS channel_name, + t.name AS topic_name, COALESCE(answer.author_handle, '')::text AS answered_by + FROM messages m + LEFT JOIN account_handles ah ON ah.account_id = m.author_account_id + LEFT JOIN account_handles oh ON oh.account_id = ah.owner_user_id + JOIN topics t ON t.id = m.topic_id + JOIN channels c ON c.id = t.channel_id + JOIN participating p ON p.channel_id = t.channel_id + LEFT JOIN LATERAL ( + SELECT (CASE WHEN answer_handle.owner_user_id IS NULL THEN COALESCE(answer_handle.handle, '') + WHEN answer_owner.handle IS NULL THEN '' + ELSE answer_owner.handle || '/' || answer_handle.handle END)::text AS author_handle + FROM messages answer_message + LEFT JOIN account_handles answer_handle ON answer_handle.account_id = answer_message.author_account_id + LEFT JOIN account_handles answer_owner ON answer_owner.account_id = answer_handle.owner_user_id + WHERE answer_message.topic_id = m.topic_id + AND answer_message.blocks @> jsonb_build_array(jsonb_build_object( + 'kind', 'ask_answer', + 'ask_answer', jsonb_build_object('ask', jsonb_build_object('ask_id', sqlc.arg(ask_id)::text)) + )) + ORDER BY answer_message.seq + LIMIT 1 + ) answer ON TRUE + WHERE m.blocks @> jsonb_build_array(jsonb_build_object( + 'kind', 'ask', 'ask', jsonb_build_object('ask_id', sqlc.arg(ask_id)::text) + )) + ORDER BY m.seq + LIMIT 1; diff --git a/go/internal/store/queries/forge_mirrors.sql b/go/internal/store/queries/forge_mirrors.sql new file mode 100644 index 000000000..67da9a800 --- /dev/null +++ b/go/internal/store/queries/forge_mirrors.sql @@ -0,0 +1,147 @@ +-- Ask mirroring settings and outbox persistence. + +-- name: ForgeMirrorMode :one +SELECT COALESCE(( + SELECT mode FROM forge_mirror_settings + WHERE forge_provider = sqlc.arg(forge_provider)::smallint AND forge_host = sqlc.arg(forge_host)::text AND repo = sqlc.arg(repo)::text +), 0)::smallint AS mode; + +-- name: ListForgeMirrorSettings :many +SELECT s.forge_provider, s.forge_host, + (CASE WHEN s.forge_provider = 1 THEN lower(btrim(s.repo)) ELSE btrim(s.repo) END)::text AS repo, + COALESCE(m.mode, 0)::smallint AS mode + FROM forge_repo_subscriptions s + LEFT JOIN forge_mirror_settings m + ON m.forge_provider = s.forge_provider + AND m.forge_host = s.forge_host + AND m.repo = CASE WHEN s.forge_provider = 1 THEN lower(btrim(s.repo)) ELSE btrim(s.repo) END + WHERE s.enabled + ORDER BY s.forge_provider, s.forge_host, s.repo; + +-- name: DeleteForgeMirrorSetting :exec +DELETE FROM forge_mirror_settings + WHERE forge_provider = sqlc.arg(forge_provider) AND forge_host = sqlc.arg(forge_host) AND repo = sqlc.arg(repo); + +-- name: UpsertForgeMirrorSetting :exec +INSERT INTO forge_mirror_settings (forge_provider, forge_host, repo, mode, updated_by) +VALUES (sqlc.arg(forge_provider), sqlc.arg(forge_host), sqlc.arg(repo), sqlc.arg(mode), sqlc.arg(updated_by)) +ON CONFLICT (tenant_id, forge_provider, forge_host, repo) DO UPDATE + SET mode = EXCLUDED.mode, updated_by = EXCLUDED.updated_by; + +-- name: ClaimForgeMirror :one +INSERT INTO forge_mirrors + (id, source, ask_id, source_key, actor_account_id, agent_account_id, + client_request_id, target_ref, body_rev, state) +VALUES (sqlc.arg(id), sqlc.arg(source), sqlc.arg(ask_id), sqlc.arg(source_key), sqlc.arg(actor_account_id), + sqlc.narg(agent_account_id), sqlc.narg(client_request_id), sqlc.arg(target_ref), sqlc.arg(body_rev), sqlc.arg(state)) +ON CONFLICT DO NOTHING +RETURNING id, source, ask_id, source_key, actor_account_id, + COALESCE(agent_account_id, '')::text AS agent_account_id, + client_request_id, target_ref, body_rev, posted_rev, state, attempts, + comment_id, comment_key, comment_url, last_error, first_attempt_at, + last_attempt_at, client_comment_id::text AS client_comment_id, + recheck_at, lease_until, lease_token, updated_at; + +-- name: ForgeMirrorByConflictKeys :one +SELECT id, source, ask_id, source_key, actor_account_id, + COALESCE(agent_account_id, '')::text AS agent_account_id, + client_request_id, target_ref, body_rev, posted_rev, state, attempts, + comment_id, comment_key, comment_url, last_error, first_attempt_at, + last_attempt_at, client_comment_id::text AS client_comment_id, + recheck_at, lease_until, lease_token, updated_at, + CASE WHEN agent_account_id = NULLIF(sqlc.arg(agent_account_id)::text, '') AND client_request_id = sqlc.narg(client_request_id)::text + THEN 2::smallint + WHEN ask_id = NULLIF(sqlc.arg(ask_id)::text, '') THEN 1::smallint + ELSE 3::smallint END AS conflict_kind + FROM forge_mirrors + WHERE (sqlc.arg(ask_id)::text <> '' AND ask_id = sqlc.arg(ask_id)::text) + OR (sqlc.arg(agent_account_id)::text <> '' AND agent_account_id = sqlc.arg(agent_account_id)::text AND client_request_id = sqlc.narg(client_request_id)::text) + OR (sqlc.arg(source_key)::text <> '' AND source_key = sqlc.arg(source_key)::text AND target_ref = sqlc.arg(target_ref)::text) + ORDER BY CASE WHEN agent_account_id = NULLIF(sqlc.arg(agent_account_id)::text, '') AND client_request_id = sqlc.narg(client_request_id)::text THEN 1 + WHEN ask_id = NULLIF(sqlc.arg(ask_id)::text, '') THEN 2 + ELSE 3 END + LIMIT 1; + +-- name: BumpForgeMirrorSourceRevision :execrows +UPDATE forge_mirrors + SET body_rev = LEAST(body_rev::integer + 1, 32767)::smallint, state = 1 + WHERE id = sqlc.arg(id); + +-- name: OpenForgeMirrors :many +SELECT id, source, ask_id, source_key, actor_account_id, + COALESCE(agent_account_id, '')::text AS agent_account_id, + client_request_id, target_ref, body_rev, posted_rev, state, attempts, + comment_id, comment_key, comment_url, last_error, first_attempt_at, + last_attempt_at, client_comment_id::text AS client_comment_id, + recheck_at, lease_until, lease_token, updated_at + FROM forge_mirrors + WHERE agent_account_id = sqlc.arg(agent_account_id)::text AND (state <> 2 OR recheck_at <= now()) + ORDER BY created_at, id; + +-- name: AskForgeMirrors :many +SELECT id, source, ask_id, source_key, actor_account_id, + COALESCE(agent_account_id, '')::text AS agent_account_id, + client_request_id, target_ref, body_rev, posted_rev, state, attempts, + comment_id, comment_key, comment_url, last_error, first_attempt_at, + last_attempt_at, client_comment_id::text AS client_comment_id, + recheck_at, lease_until, lease_token, updated_at + FROM forge_mirrors WHERE ask_id = ANY(sqlc.arg(ask_ids)::text[]) ORDER BY created_at, id; + +-- name: LeaseForgeMirror :one +UPDATE forge_mirrors + SET lease_until = now() + sqlc.arg(ttl)::interval, lease_token = sqlc.arg(lease_token) + WHERE id = sqlc.arg(id) AND (state <> 2 OR recheck_at <= now()) + AND (lease_until IS NULL OR lease_until < now()) +RETURNING id, source, ask_id, source_key, actor_account_id, + COALESCE(agent_account_id, '')::text AS agent_account_id, + client_request_id, target_ref, body_rev, posted_rev, state, attempts, + comment_id, comment_key, comment_url, last_error, first_attempt_at, + last_attempt_at, client_comment_id::text AS client_comment_id, + recheck_at, lease_until, lease_token, updated_at; + +-- name: RenewForgeMirror :execrows +UPDATE forge_mirrors SET lease_until = now() + sqlc.arg(ttl)::interval + WHERE id = sqlc.arg(id) AND lease_token = sqlc.arg(lease_token) AND lease_until > now(); + +-- name: BeginForgeMirrorAttempt :one +UPDATE forge_mirrors + SET first_attempt_at = COALESCE(first_attempt_at, now()), + last_attempt_at = now(), attempts = attempts + 1 + WHERE id = sqlc.arg(id) AND lease_token = sqlc.arg(lease_token) AND lease_until > now() +RETURNING id, source, ask_id, source_key, actor_account_id, + COALESCE(agent_account_id, '')::text AS agent_account_id, + client_request_id, target_ref, body_rev, posted_rev, state, attempts, + comment_id, comment_key, comment_url, last_error, first_attempt_at, + last_attempt_at, client_comment_id::text AS client_comment_id, + recheck_at, lease_until, lease_token, updated_at; + +-- name: MarkForgeMirror :execrows +UPDATE forge_mirrors + SET posted_rev = sqlc.arg(posted_rev), + state = CASE WHEN sqlc.arg(state)::smallint = 2 AND body_rev > sqlc.arg(posted_rev)::smallint THEN 1::smallint ELSE sqlc.arg(state)::smallint END, + comment_id = CASE WHEN sqlc.arg(comment_id)::bigint = 0 THEN comment_id ELSE sqlc.arg(comment_id) END, + comment_key = CASE WHEN sqlc.arg(comment_key)::text = '' THEN comment_key ELSE sqlc.arg(comment_key) END, + comment_url = CASE WHEN sqlc.arg(comment_url)::text = '' THEN comment_url ELSE sqlc.arg(comment_url) END, + last_error = sqlc.arg(last_error), + recheck_at = CASE WHEN sqlc.arg(recheck)::boolean THEN now() + interval '10 minutes' + WHEN sqlc.arg(clear_recheck)::boolean THEN NULL + ELSE recheck_at END, + lease_until = NULL, lease_token = '' + WHERE id = sqlc.arg(id) AND lease_token = sqlc.arg(lease_token) AND lease_until > now(); + +-- name: ForgeMirrorByID :one +SELECT id, source, ask_id, source_key, actor_account_id, + COALESCE(agent_account_id, '')::text AS agent_account_id, + client_request_id, target_ref, body_rev, posted_rev, state, attempts, + comment_id, comment_key, comment_url, last_error, first_attempt_at, + last_attempt_at, client_comment_id::text AS client_comment_id, + recheck_at, lease_until, lease_token, updated_at + FROM forge_mirrors WHERE id = sqlc.arg(id); + +-- name: InsertAskForgeMirror :exec +INSERT INTO forge_mirrors + (id, source, ask_id, actor_account_id, agent_account_id, target_ref, body_rev, state) +VALUES (sqlc.arg(id), 1, sqlc.arg(ask_id), sqlc.arg(actor_account_id), sqlc.arg(actor_account_id), sqlc.arg(target_ref), 1, 1); + +-- name: BumpAskForgeMirror :exec +UPDATE forge_mirrors SET body_rev = 2, state = 1 WHERE ask_id = sqlc.arg(ask_id); diff --git a/go/internal/store/rls_pgtest_test.go b/go/internal/store/rls_pgtest_test.go index aae0261f4..e6a2363a9 100644 --- a/go/internal/store/rls_pgtest_test.go +++ b/go/internal/store/rls_pgtest_test.go @@ -645,6 +645,7 @@ func TestRLSCatalogEnabledAndForced(t *testing.T) { "agent_boards", "agent_board_tasks", "linear_agent_sessions", "issues", "forge_repo_subscriptions", "forge_artifact_cursors", + "forge_mirror_settings", "forge_mirrors", "forge_state_transitions", "token_usage_events", "token_usage_rollups_hourly", "token_usage_rollups_daily", "compute_usage_events", "compute_usage_rollups_hourly", "compute_usage_rollups_daily", diff --git a/go/internal/store/types.go b/go/internal/store/types.go index 60577c4f6..55d5a2422 100644 --- a/go/internal/store/types.go +++ b/go/internal/store/types.go @@ -457,6 +457,8 @@ type Ask struct { AskID string // Ref is the explicit issue or pull request this ask concerns. Ref string + // RefCoord is derived at the comms edge for auto-mirror routing; it is not stored. + RefCoord *ForgeCoord // Questions are the questions the agent asked, in order. At least one; a // zero-question ask is rejected on both marshal and unmarshal. Questions []AskQuestion diff --git a/go/internal/store/updated_at_pgtest_test.go b/go/internal/store/updated_at_pgtest_test.go index 74b56991b..5afd8fe37 100644 --- a/go/internal/store/updated_at_pgtest_test.go +++ b/go/internal/store/updated_at_pgtest_test.go @@ -203,7 +203,7 @@ func TestUpdatedAtTriggerCatalogFloor(t *testing.T) { for _, tbl := range []string{ "secrets", "agent_placements", "session_bindings", "agent_config_bundle", "model_registry", "forge_repo_subscriptions", - "pull_requests", + "forge_mirror_settings", "forge_mirrors", "pull_requests", } { var n int if err := s.pool.QueryRow(ctx, diff --git a/go/server/deeplink.go b/go/server/deeplink.go index 3671a44cf..211c22777 100644 --- a/go/server/deeplink.go +++ b/go/server/deeplink.go @@ -36,6 +36,14 @@ func deepLinkFor(base, channelID string) string { return strings.TrimRight(base, "/") + "/#/channel/" + url.PathEscape(channelID) } +// topicLinkFor builds the UI's HashRouter link to one topic in a channel. +func topicLinkFor(base, channelID, topicID string) string { + if base == "" { + return "" + } + return strings.TrimRight(base, "/") + "/#/channel/" + url.PathEscape(channelID) + "/topic/" + url.PathEscape(topicID) +} + // sessionLinkFor builds the stable click-time redirect URL from the Linear // session ID; trimming and escaping match deepLinkFor's base and path handling. func sessionLinkFor(base, linearSessionID string) string { diff --git a/go/server/deeplink_test.go b/go/server/deeplink_test.go index 53ba630f0..fd41d8d49 100644 --- a/go/server/deeplink_test.go +++ b/go/server/deeplink_test.go @@ -52,6 +52,27 @@ func TestDeepLinkFor(t *testing.T) { } } +func TestTopicLinkFor(t *testing.T) { + tests := []struct { + name string + base string + channelID string + topicID string + want string + }{ + {name: "missing public base", channelID: "channel", topicID: "topic", want: ""}, + {name: "topic route", base: "https://compass.example.com/", channelID: "ch-home", topicID: "topic-1", want: "https://compass.example.com/#/channel/ch-home/topic/topic-1"}, + {name: "escaped ids", base: "https://compass.example.com", channelID: "ch a/b", topicID: "topic?x", want: "https://compass.example.com/#/channel/ch%20a%2Fb/topic/topic%3Fx"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := topicLinkFor(tc.base, tc.channelID, tc.topicID); got != tc.want { + t.Errorf("topicLinkFor(%q, %q, %q) = %q, want %q", tc.base, tc.channelID, tc.topicID, got, tc.want) + } + }) + } +} + func TestSessionLinkFor(t *testing.T) { tests := []struct { name string diff --git a/go/server/forge.go b/go/server/forge.go index fea6064d7..db1cb4178 100644 --- a/go/server/forge.go +++ b/go/server/forge.go @@ -13,6 +13,7 @@ import ( "log/slog" "math" "strings" + "sync" "time" "connectrpc.com/connect" @@ -21,6 +22,7 @@ import ( "github.com/RigelBuild/compass/go/internal/board" "github.com/RigelBuild/compass/go/internal/forge" compassv1internal "github.com/RigelBuild/compass/go/internal/gen/compass/v1" + "github.com/RigelBuild/compass/go/internal/mirror" "github.com/RigelBuild/compass/go/internal/store" "google.golang.org/protobuf/encoding/protojson" ) @@ -134,20 +136,40 @@ type forgeStore interface { DeleteAgentForgeSubscription(ctx context.Context, agent store.AccountID, subscriptionID string) error RecordStateTransition(ctx context.Context, provider store.ForgeProvider, host, repo string, kind store.ForgeArtifactKind, number uint64, state string, agent store.AccountID, at time.Time) error HasForgeScope(ctx context.Context, account store.AccountID, provider store.ForgeProvider, host, repo string) (bool, error) + ForgeMirrorMode(ctx context.Context, provider store.ForgeProvider, host, repo string) (store.ForgeMirrorMode, error) + PullRequestsByCoords(ctx context.Context, coords []store.ForgeCoord) ([]store.PullRequestRow, error) } // forgeService is the ForgeCaller implementation and the DL-050 write // chokepoint. It holds the narrow store (attribution + ownership index), the // issue projection (the OQ-A tracked-read source), the provider registry // (author/reviewer clients per coordinate), and an injectable clock (the DL-055 -// row's created_at). +// row's created_at). The mirror fields bound detached mirror drives and recheck +// timers to the server's lifetime. type forgeService struct { - store forgeStore - issueBrd *board.IssueProjection - providers *forgeProviderRegistry - now func() time.Time + store forgeStore + issueBrd *board.IssueProjection + providers *forgeProviderRegistry + now func() time.Time + mirrorStoreDB *store.Store + publicURL string // enforceScopes gates every coordinate write on an account_forge_scopes grant. - enforceScopes bool + enforceScopes bool + mirrorMu sync.Mutex + mirrorDone <-chan struct{} + mirrorCancel context.CancelFunc + mirrorClosing bool + mirrorWG sync.WaitGroup + mirrorTimers map[string]*mirrorTimer + scheduleMirrorAfter func(time.Duration, func()) func() bool + mirrorPoster mirror.Poster + mirrorRecheckRetry map[string]int +} + +type mirrorTimer struct { + stop func() bool + due time.Time + once sync.Once } // newForgeService constructs the forge caller over the store, the issue @@ -156,7 +178,7 @@ type forgeService struct { // hub<->forgeService construction cycle exactly as newBoardService does // (sinks.go). func newForgeService(st *store.Store, issueBrd *board.IssueProjection, providers *forgeProviderRegistry) *forgeService { - return &forgeService{store: st, issueBrd: issueBrd, providers: providers, now: time.Now} + return &forgeService{store: st, issueBrd: issueBrd, providers: providers, now: time.Now, mirrorStoreDB: st} } // ExecuteForgeCallAsAccount is the ForgeCaller entry point and the per-arm diff --git a/go/server/forge_mirror.go b/go/server/forge_mirror.go new file mode 100644 index 000000000..3a6f28997 --- /dev/null +++ b/go/server/forge_mirror.go @@ -0,0 +1,798 @@ +//go:build unix + +package server + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "log/slog" + "slices" + "sort" + "strings" + "sync" + "time" + "unicode/utf8" + + "connectrpc.com/connect" + + compassv1 "github.com/RigelBuild/compass/go/gen/compass/v1" + "github.com/RigelBuild/compass/go/internal/forge" + compassv1internal "github.com/RigelBuild/compass/go/internal/gen/compass/v1" + "github.com/RigelBuild/compass/go/internal/mirror" + "github.com/RigelBuild/compass/go/internal/refs" + "github.com/RigelBuild/compass/go/internal/store" +) + +// Owner-header session id for agent mirror comments; user mirrors use the +// render attribution and do not carry an owner header. +const mirrorAgentSession = "mirror" + +var mirrorOwnerHeaderAllowance = forge.MaxOwnerHeaderLen(mirrorAgentSession) + +var _ mirror.Poster = (*forgeService)(nil) + +type userMirrorPoster struct { + *forgeService +} + +func (p userMirrorPoster) BodyLimit(ref refs.Ref) int { + return p.ProviderBodyLimit(ref) +} + +type agentMirrorPoster struct { + *forgeService +} + +func (p agentMirrorPoster) BodyLimit(ref refs.Ref) int { + return p.forgeService.BodyLimit(ref) +} + +type mirrorRefusalError struct { + code connect.Code + message string +} + +func (e *mirrorRefusalError) Error() string { return e.message } + +func (e *mirrorRefusalError) Unwrap() error { return mirror.ErrRefused } + +func (s *forgeService) mirrorAllowed(ctx context.Context, account store.Account, need store.ForgeMirrorMode, ref refs.Ref) (resolvedForge, string, uint64, *compassv1internal.ForgeCallError) { + if account.ID == "" { + return resolvedForge{}, "", 0, forgeErr(connect.CodeUnauthenticated, "forge mirror: caller is required") + } + rf, repo, number, fe := s.resolveMirrorCoordinate(ref) + if fe != nil { + return resolvedForge{}, "", 0, fe + } + mode, err := s.store.ForgeMirrorMode(ctx, store.ForgeProvider(rf.provider), rf.host, repo) + if err != nil { + return resolvedForge{}, "", 0, storeForgeError(err) + } + if mode < need { + return resolvedForge{}, "", 0, forgeErr(connect.CodeFailedPrecondition, "forge mirror is disabled for "+repo) + } + if account.IsAgent() { + if fe := s.requireForgeScope(ctx, account.ID, rf, repo); fe != nil { + return resolvedForge{}, "", 0, fe + } + } else if account.User == nil { + return resolvedForge{}, "", 0, forgeErr(connect.CodePermissionDenied, "forge mirror: caller must be a user or agent") + } + return rf, repo, number, nil +} + +func (s *forgeService) BodyLimit(ref refs.Ref) int { + limit := s.ProviderBodyLimit(ref) + if limit == int(^uint(0)>>1) { + return limit + } + limit -= mirrorOwnerHeaderAllowance + if limit <= 0 { + return 1 + } + return limit +} + +func (s *forgeService) ProviderBodyLimit(ref refs.Ref) int { + rf, _, _, fe := s.resolveMirrorCoordinate(ref) + if fe != nil { + return 0 + } + if rf.bodyLimit <= 0 { + return int(^uint(0) >> 1) + } + return rf.bodyLimit +} + +func (s *forgeService) resolveMirrorCoordinate(ref refs.Ref) (resolvedForge, string, uint64, *compassv1internal.ForgeCallError) { + var provider compassv1.ForgeProvider + var host, repo string + switch ref.Kind { + case refs.KindTracker: + provider, host, repo = compassv1.ForgeProvider_FORGE_PROVIDER_LINEAR, forge.LinearHost, ref.Key + case refs.KindForge: + provider, repo = compassv1.ForgeProvider_FORGE_PROVIDER_GITHUB, ref.Owner+"/"+ref.Repo + default: + return resolvedForge{}, "", 0, forgeErr(connect.CodeInvalidArgument, "forge mirror: unsupported reference kind") + } + if ref.Number == 0 { + return resolvedForge{}, "", 0, forgeErr(connect.CodeInvalidArgument, "forge mirror: reference number is required") + } + rf, ok := s.providers.resolve(&compassv1.ForgeRef{Provider: provider, Host: host}) + if !ok { + return resolvedForge{}, "", 0, forgeErr(connect.CodeNotFound, "forge mirror: provider is not configured") + } + return rf, repo, ref.Number, nil +} +func (s *forgeService) PostMirror(ctx context.Context, actor store.AccountID, need store.ForgeMirrorMode, ref refs.Ref, commentKey, clientID, body string) (forge.Comment, error) { + account, err := s.store.GetAccount(ctx, actor) + if err != nil { + return forge.Comment{}, fmt.Errorf("mirror: read caller: %w", err) + } + rf, repo, number, fe := s.mirrorAllowed(ctx, account, need, ref) + if fe != nil { + return forge.Comment{}, &mirrorRefusalError{code: parseConnectCode(fe.GetCode()), message: fe.GetMessage()} + } + switch { + case account.IsAgent(): + identity, identityErr := s.resolveIdentity(ctx, actor, mirrorAgentSession) + if identityErr != nil { + return forge.Comment{}, fmt.Errorf("mirror: resolve agent attribution: %s", identityErr.GetMessage()) + } + body, err = forge.StampOwner(body, identity.author, rf.bodyLimit) + if err != nil { + return forge.Comment{}, fmt.Errorf("mirror: stamp owner header: %w", err) + } + case account.User == nil: + return forge.Comment{}, &mirrorRefusalError{code: connect.CodePermissionDenied, message: "caller must be a user or agent"} + default: + body, _, _ = forge.StripOwner(body) + } + if commentKey != "" { + return rf.author.EditComment(ctx, repo, commentKey, body) + } + if ref.Kind == refs.KindForge { + rows, err := s.store.PullRequestsByCoords(ctx, []store.ForgeCoord{{Provider: store.ForgeProvider(rf.provider), Host: rf.host, Repo: repo, Number: number}}) + if err != nil { + return forge.Comment{}, fmt.Errorf("mirror: look up pull request: %w", err) + } + for _, row := range rows { + if row.Coord.Number == number { + return rf.author.CommentOnPullRequest(ctx, repo, number, body, clientID) + } + } + } + return rf.author.CommentOnIssue(ctx, repo, number, body, clientID) +} + +func (s *forgeService) ListMirrorComments(ctx context.Context, actor store.AccountID, ref refs.Ref, since time.Time) ([]forge.Comment, error) { + account, err := s.store.GetAccount(ctx, actor) + if err != nil { + return nil, err + } + need := store.ForgeMirrorAuto + if account.User != nil { + need = store.ForgeMirrorManual + } + rf, repo, number, fe := s.mirrorAllowed(ctx, account, need, ref) + if fe != nil { + return nil, &mirrorRefusalError{code: parseConnectCode(fe.GetCode()), message: fe.GetMessage()} + } + identity, ok := rf.author.(interface { + AppLogin(ctx context.Context) (string, error) + }) + if !ok { + return nil, errors.New("mirror: provider has no app identity lookup") + } + login, err := identity.AppLogin(ctx) + if err != nil { + return nil, fmt.Errorf("mirror: resolve app author for marker scan: %w", err) + } + login = strings.TrimSpace(login) + if login == "" { + return nil, errors.New("mirror: app author identity is empty") + } + comments, err := rf.author.ListCommentsSince(ctx, repo, number, since) + if err != nil { + return nil, err + } + filtered := make([]forge.Comment, 0, len(comments)) + for _, comment := range comments { + if strings.EqualFold(comment.ForgeAccount, login) { + filtered = append(filtered, comment) + } + } + return filtered, nil +} + +func (s *forgeService) DeleteMirrorComment(ctx context.Context, actor store.AccountID, ref refs.Ref, commentKey string) error { + account, err := s.store.GetAccount(ctx, actor) + if err != nil { + return err + } + need := store.ForgeMirrorAuto + if account.User != nil { + need = store.ForgeMirrorManual + } + rf, repo, _, fe := s.mirrorAllowed(ctx, account, need, ref) + if fe != nil { + return &mirrorRefusalError{code: parseConnectCode(fe.GetCode()), message: fe.GetMessage()} + } + return rf.author.DeleteComment(ctx, repo, commentKey) +} + +func (s *forgeService) SetMirrorLifetime(ctx context.Context) { + life, cancel := context.WithCancel(ctx) + s.mirrorMu.Lock() + defer s.mirrorMu.Unlock() + if s.mirrorCancel == nil { + s.mirrorDone = life.Done() + s.mirrorCancel = cancel + } else { + cancel() + } + if s.mirrorTimers == nil { + s.mirrorTimers = make(map[string]*mirrorTimer) + } +} +func (s *forgeService) StopMirrorLifetime() { + s.mirrorMu.Lock() + s.mirrorClosing = true + if s.mirrorCancel != nil { + s.mirrorCancel() + } + for rowID, handle := range s.mirrorTimers { + if handle.stop != nil && handle.stop() { + handle.finish(s) + } + delete(s.mirrorTimers, rowID) + } + s.mirrorMu.Unlock() + s.mirrorWG.Wait() +} + +func (s *forgeService) beginMirrorWork(parent context.Context) (context.Context, func(), bool) { + s.mirrorMu.Lock() + defer s.mirrorMu.Unlock() + if s.mirrorClosing { + return nil, nil, false + } + if s.mirrorDone == nil { + life, cancel := context.WithCancel(context.WithoutCancel(parent)) + s.mirrorDone = life.Done() + s.mirrorCancel = cancel + } + s.mirrorWG.Add(1) + work, cancel := context.WithCancel(context.WithoutCancel(parent)) + stop := cancelOnDone(s.mirrorDone, cancel) + done := func() { + stop() + cancel() + s.mirrorWG.Done() + } + return work, done, true +} + +func (s *forgeService) scheduleMirrorRecheck(ctx context.Context, row store.ForgeMirrorRow, bodyFn mirror.BodyFunc, failed bool) { + if row.RecheckAt == nil { + return + } + delay := max(time.Until(*row.RecheckAt), 0) + parent := context.WithoutCancel(ctx) + + s.mirrorMu.Lock() + if s.mirrorClosing { + s.mirrorMu.Unlock() + return + } + if s.mirrorTimers == nil { + s.mirrorTimers = make(map[string]*mirrorTimer) + } + if s.mirrorRecheckRetry == nil { + s.mirrorRecheckRetry = make(map[string]int) + } + retry := s.mirrorRecheckRetry[row.ID] + if failed { + delay = mirrorRecheckBackoff(retry) + } + if row.LeaseUntil != nil { + delay = max(delay, time.Until(*row.LeaseUntil)) + } + due := time.Now().Add(delay) + if current := s.mirrorTimers[row.ID]; current != nil { + if !due.Before(current.due) { + s.mirrorMu.Unlock() + return + } + if failed { + s.mirrorRecheckRetry[row.ID] = min(retry+1, 2) + } + if current.stop != nil && current.stop() { + current.finish(s) + } + delete(s.mirrorTimers, row.ID) + } else if failed { + s.mirrorRecheckRetry[row.ID] = min(retry+1, 2) + } + handle := &mirrorTimer{due: due} + s.mirrorTimers[row.ID] = handle + s.mirrorWG.Add(1) + sched := s.scheduleMirrorAfter + if sched == nil { + sched = func(delay time.Duration, fn func()) func() bool { + timer := time.AfterFunc(delay, fn) + return timer.Stop + } + } + done := s.mirrorDone + handle.stop = sched(delay, func() { + defer handle.finish(s) + s.mirrorMu.Lock() + current := s.mirrorTimers[row.ID] == handle + if current { + delete(s.mirrorTimers, row.ID) + } + closing := s.mirrorClosing + s.mirrorMu.Unlock() + if !current || closing { + return + } + timerCtx, cancel := context.WithCancel(parent) + stop := cancelOnDone(done, cancel) + defer stop() + defer cancel() + recheckRow, driveErr := s.driveMirrorRow(timerCtx, row.ID, bodyFn) + s.mirrorMu.Lock() + if driveErr == nil { + delete(s.mirrorRecheckRetry, row.ID) + } + s.mirrorMu.Unlock() + if driveErr != nil && errors.Is(driveErr, store.ErrLeaseLost) { + recheckRow, err := s.mirrorStoreDB.ForgeMirrorByID(timerCtx, row.ID) + if err != nil { + // A failed read keeps the timer's own row, so its recheck still retries with backoff. + slog.Default().ErrorContext(timerCtx, "read forge mirror after lease loss", "mirror_id", row.ID, "error", err) + recheckRow = row + } + if recheckRow.RecheckAt != nil { + s.scheduleMirrorRecheck(timerCtx, recheckRow, bodyFn, true) + } + return + } + s.scheduleMirrorRecheck(timerCtx, recheckRow, bodyFn, driveErr != nil) + }) + s.mirrorMu.Unlock() +} + +func mirrorRecheckBackoff(attempt int) time.Duration { + switch min(attempt, 2) { + case 0: + return time.Minute + case 1: + return 5 * time.Minute + default: + return 15 * time.Minute + } +} + +func (h *mirrorTimer) finish(s *forgeService) { + h.once.Do(s.mirrorWG.Done) +} + +func cancelOnDone(done <-chan struct{}, cancel context.CancelFunc) func() { + if done == nil { + return func() {} + } + stop := make(chan struct{}) + var once sync.Once + go func() { + select { + case <-done: + cancel() + case <-stop: + } + }() + return func() { once.Do(func() { close(stop) }) } +} + +func (s *forgeService) driveMirrorRow(ctx context.Context, rowID string, bodyFn mirror.BodyFunc) (store.ForgeMirrorRow, error) { + row, err := s.mirrorStoreDB.ForgeMirrorByID(ctx, rowID) + if err != nil { + slog.Default().ErrorContext(ctx, "read forge mirror for drive", "mirror_id", rowID, "error", err) + return store.ForgeMirrorRow{}, err + } + if row.Source == 2 && bodyFn == nil && row.AskID != "" { + bodyFn = s.userAskBodyForRow() + } + poster := mirror.Poster(userMirrorPoster{s}) + if row.Agent != "" { + poster = agentMirrorPoster{s} + } + if s.mirrorPoster != nil { + poster = s.mirrorPoster + } + result, err := mirror.DriveOne(ctx, s.mirrorStoreDB, poster, row, func(channelID, topicID string) mirror.Link { + return mirror.Link{URL: topicLinkFor(s.publicURL, channelID, topicID)} + }, bodyFn) + if err != nil { + slog.Default().ErrorContext(ctx, "drive forge mirror", "mirror_id", row.ID, "error", err) + } + return result, err +} + +func (s *forgeService) userAskBodyForRow() mirror.BodyFunc { + return func(ctx context.Context, row store.ForgeMirrorRow, limit int) (string, error) { + account, err := s.mirrorStoreDB.GetAccount(ctx, row.Actor) + if err != nil { + return "", err + } + current, err := s.mirrorStoreDB.AskForForgeMirror(ctx, row.Actor, row.AskID) + if err != nil { + return "", err + } + var ask *store.Ask + for _, block := range current.Message.Blocks { + if block.Ask != nil { + ask = block.Ask + break + } + } + if ask == nil { + return "", store.ErrNotFound + } + link := mirror.Link{Channel: current.Channel, Topic: current.Topic, URL: topicLinkFor(s.publicURL, string(current.ChannelID), current.Message.TopicID)} + body, _, _ := forge.StripOwner(mirror.RenderAsk(*ask, current.AnsweredBy, link)) + return truncateBody("Mirrored by @"+account.Handle+"\n\n"+body, limit), nil + } +} + +func (s *forgeService) DriveAsks(ctx context.Context, askIDs []string) { + life, done, ok := s.beginMirrorWork(ctx) + if !ok { + return + } + defer done() + rows, err := s.mirrorStoreDB.AskForgeMirrors(life, askIDs) + if err != nil { + slog.Default().ErrorContext(life, "list ask forge mirrors", "ask_ids", askIDs, "error", err) + return + } + for _, row := range rows { + result, err := s.driveMirrorRow(life, row.ID, nil) + s.scheduleMirrorRecheck(life, result, nil, err != nil) + } +} +func (s *forgeService) MirrorAsUser(ctx context.Context, account store.Account, req *compassv1.MirrorToForgeRequest) (string, error) { + fail := func(code connect.Code, err error) (string, error) { return "", connect.NewError(code, err) } + if req == nil { + return fail(connect.CodeInvalidArgument, errors.New("mirror request is required")) + } + ref, err := refs.Parse(req.GetRef()) + if err != nil { + return fail(connect.CodeInvalidArgument, err) + } + if account.ID == "" || account.User == nil { + return fail(connect.CodePermissionDenied, errors.New("forge mirror: caller must be a user")) + } + askID, topicID, messageIDs := req.GetAskId(), req.GetTopicId(), req.GetMessageIds() + sourceCount := 0 + if askID != "" { + sourceCount++ + } + if topicID != "" { + sourceCount++ + } + if len(messageIDs) > 0 { + sourceCount++ + } + if sourceCount != 1 || req.GetChannelId() == "" || (len(messageIDs) > 0 && len(messageIDs) > 50) { + return fail(connect.CodeInvalidArgument, errors.New("exactly one source and channel_id are required; message_ids must contain 1 to 50 ids")) + } + member, err := s.mirrorStoreDB.IsChannelMember(ctx, account.ID, store.ChannelID(req.GetChannelId())) + if err != nil { + return "", mirrorStoreError(err) + } + if !member { + return fail(connect.CodeNotFound, store.ErrNotFound) + } + if _, _, _, fe := s.mirrorAllowed(ctx, account, store.ForgeMirrorManual, ref); fe != nil { + return fail(parseConnectCode(fe.GetCode()), errors.New(fe.GetMessage())) + } + channel, err := s.mirrorStoreDB.GetChannel(ctx, store.ChannelID(req.GetChannelId())) + if err != nil { + return "", mirrorStoreError(err) + } + if askID != "" { + return s.mirrorAskAsUser(ctx, account, ref, req.GetChannelId(), channel.Name, askID, fail) + } + sourceKey, renderBody, err := s.resolveUserMirrorSource(ctx, account, req, channel.Name) + if err != nil { + return "", err + } + return s.claimAndDriveUserMirror(ctx, account, ref, sourceKey, renderBody, fail) +} + +func (s *forgeService) resolveUserMirrorSource(ctx context.Context, account store.Account, req *compassv1.MirrorToForgeRequest, channel string) (string, mirror.BodyFunc, error) { + if req.GetTopicId() != "" { + return s.resolveUserTopicSource(ctx, account, req, channel) + } + ids := append([]string(nil), req.GetMessageIds()...) + slices.Sort(ids) + for i, id := range ids { + if id == "" || i > 0 && id == ids[i-1] { + return "", nil, connect.NewError(connect.CodeInvalidArgument, errors.New("message ids must be non-empty and unique")) + } + } + hash := sha256.Sum256([]byte(strings.Join(ids, "\n"))) + messages := make([]store.Message, 0, len(ids)) + for _, id := range ids { + messageChannel, err := s.mirrorStoreDB.MessageChannel(ctx, id) + if errors.Is(err, store.ErrNotFound) || err == nil && messageChannel != store.ChannelID(req.GetChannelId()) { + return "", nil, connect.NewError(connect.CodeNotFound, store.ErrNotFound) + } + if err != nil { + return "", nil, mirrorStoreError(err) + } + message, err := s.mirrorStoreDB.MessageByID(ctx, id) + if err != nil { + return "", nil, mirrorStoreError(err) + } + messages = append(messages, message) + } + member, err := s.mirrorStoreDB.IsChannelMember(ctx, account.ID, store.ChannelID(req.GetChannelId())) + if err != nil { + return "", nil, mirrorStoreError(err) + } + if !member { + return "", nil, connect.NewError(connect.CodeNotFound, store.ErrNotFound) + } + sort.Slice(messages, func(i, j int) bool { + if messages[i].At.Equal(messages[j].At) { + return messages[i].ID < messages[j].ID + } + return messages[i].At.Before(messages[j].At) + }) + key := hex.EncodeToString(hash[:]) + link := mirror.Link{Channel: channel, Topic: "selected messages", URL: deepLinkFor(s.publicURL, req.GetChannelId())} + return key, userMessageBody(messages, messageAuthors(messages), link, account.Handle, ""), nil + +} + +func (s *forgeService) resolveUserTopicSource(ctx context.Context, account store.Account, req *compassv1.MirrorToForgeRequest, channel string) (string, mirror.BodyFunc, error) { + topics, err := s.mirrorStoreDB.ListTopics(ctx, string(account.ID), req.GetChannelId(), true) + if err != nil { + return "", nil, mirrorStoreError(err) + } + for _, topic := range topics { + if topic.ID != req.GetTopicId() { + continue + } + messages, err := s.mirrorStoreDB.ListMessages(ctx, store.ListMessagesQuery{Actor: account.ID, ChannelID: store.ChannelID(req.GetChannelId()), TopicID: topic.ID, Page: store.Page{Limit: 51}}) + if err != nil { + return "", nil, mirrorStoreError(err) + } + if messages == nil { + messages = []store.Message{} + } + showLimitNote := len(messages) > 50 + if showLimitNote { + messages = append([]store.Message(nil), messages[:50]...) + } + slices.Reverse(messages) + note := "" + if showLimitNote { + note = "Showing the newest 50 messages." + } + link := mirror.Link{Channel: channel, Topic: topic.Name, URL: topicLinkFor(s.publicURL, req.GetChannelId(), topic.ID)} + return topic.ID, userMessageBody(messages, messageAuthors(messages), link, account.Handle, note), nil + } + return "", nil, connect.NewError(connect.CodeNotFound, store.ErrNotFound) +} + +func userMessageBody(messages []store.Message, authors map[store.AccountID]string, link mirror.Link, handle, note string) mirror.BodyFunc { + return func(_ context.Context, _ store.ForgeMirrorRow, limit int) (string, error) { + prefix := "Mirrored by @" + handle + if note != "" { + prefix += "\n\n" + note + } + body := mirror.Render(messages, authors, link, prefix, limit) + clean, _, _ := forge.StripOwner(body) + return truncateBody(clean, limit), nil + } +} + +func (s *forgeService) mirrorAskAsUser(ctx context.Context, account store.Account, ref refs.Ref, channelID, channelName, askID string, fail func(connect.Code, error) (string, error)) (string, error) { + source, err := s.mirrorStoreDB.AskForForgeMirror(ctx, account.ID, askID) + if err != nil { + return "", mirrorStoreError(err) + } + if source.ChannelID != store.ChannelID(channelID) { + return fail(connect.CodeNotFound, store.ErrNotFound) + } + rows, err := s.mirrorStoreDB.AskForgeMirrors(ctx, []string{askID}) + if err != nil { + return "", mirrorStoreError(err) + } + for _, row := range rows { + if row.TargetRef != ref.String() { + continue + } + if row.Source == 1 { + return s.clickDrive(ctx, row, nil, fail) + } + if row.Source != 2 || row.Actor != account.ID || row.AskID != askID { + return fail(connect.CodeFailedPrecondition, errors.New("mirror source conflicts with another actor or source")) + } + return s.clickDrive(ctx, row, s.userAskBody(account, channelID, channelName), fail) + } + row, created, err := s.mirrorStoreDB.ClaimForgeMirror(ctx, store.ForgeMirrorRow{ + Source: 2, AskID: askID, SourceKey: "ask:" + askID, Actor: account.ID, TargetRef: ref.String(), BodyRev: 1, State: 1, + }) + if err != nil { + return "", mirrorStoreError(err) + } + if !created && (row.Source != 2 || row.AskID != askID || row.Actor != account.ID || row.TargetRef != ref.String()) { + return fail(connect.CodeFailedPrecondition, errors.New("mirror source conflicts with another actor or target")) + } + return s.clickDrive(ctx, row, s.userAskBody(account, channelID, channelName), fail) +} + +func (s *forgeService) userAskBody(account store.Account, channelID, channelName string) mirror.BodyFunc { + return func(bodyCtx context.Context, row store.ForgeMirrorRow, limit int) (string, error) { + current, err := s.mirrorStoreDB.AskForForgeMirror(bodyCtx, account.ID, row.AskID) + if err != nil { + return "", err + } + var ask *store.Ask + for _, block := range current.Message.Blocks { + if block.Ask != nil { + ask = block.Ask + break + } + } + if ask == nil { + return "", store.ErrNotFound + } + link := mirror.Link{Channel: channelName, Topic: current.Topic, URL: topicLinkFor(s.publicURL, channelID, current.Message.TopicID)} + clean, _, _ := forge.StripOwner(mirror.RenderAsk(*ask, current.AnsweredBy, link)) + return truncateBody("Mirrored by @"+account.Handle+"\n\n"+clean, limit), nil + } +} + +func (s *forgeService) claimAndDriveUserMirror(ctx context.Context, account store.Account, ref refs.Ref, sourceKey string, bodyFn mirror.BodyFunc, fail func(connect.Code, error) (string, error)) (string, error) { + row, created, err := s.mirrorStoreDB.ClaimForgeMirror(ctx, store.ForgeMirrorRow{Source: 2, SourceKey: sourceKey, Actor: account.ID, TargetRef: ref.String(), BodyRev: 1, State: 1}) + if err != nil { + return "", mirrorStoreError(err) + } + if !created && (row.Source != 2 || row.Agent != "" || row.Actor != account.ID || row.SourceKey != sourceKey || row.TargetRef != ref.String()) { + return fail(connect.CodeFailedPrecondition, errors.New("mirror source conflicts with another actor")) + } + return s.clickDrive(ctx, row, bodyFn, fail) +} +func (s *forgeService) clickDrive(ctx context.Context, row store.ForgeMirrorRow, bodyFn mirror.BodyFunc, fail func(connect.Code, error) (string, error)) (string, error) { + s.mirrorMu.Lock() + if s.mirrorClosing { + s.mirrorMu.Unlock() + return fail(connect.CodeUnavailable, errors.New("mirror still delivering")) + } + done := s.mirrorDone + if done == nil { + life, lifeCancel := context.WithCancel(context.WithoutCancel(ctx)) + done = life.Done() + s.mirrorDone = done + s.mirrorCancel = lifeCancel + } + s.mirrorWG.Add(1) + s.mirrorMu.Unlock() + workCtx, cancel := context.WithCancel(context.WithoutCancel(ctx)) + stopLife := cancelOnDone(done, cancel) + + type driveResult struct { + row store.ForgeMirrorRow + err error + } + resultCh := make(chan driveResult, 1) + go func() { + defer s.mirrorWG.Done() + defer cancel() + if stopLife != nil { + defer stopLife() + } + poster := mirror.Poster(userMirrorPoster{s}) + if row.Agent != "" { + poster = agentMirrorPoster{s} + } + if s.mirrorPoster != nil { + poster = s.mirrorPoster + } + result, err := mirror.DriveOne(workCtx, s.mirrorStoreDB, poster, row, func(channelID, topicID string) mirror.Link { + return mirror.Link{URL: topicLinkFor(s.publicURL, channelID, topicID)} + }, bodyFn) + if err != nil { + slog.Default().ErrorContext(workCtx, "drive user forge mirror", "mirror_id", row.ID, "error", err) + } + s.scheduleMirrorRecheck(workCtx, result, bodyFn, err != nil) + resultCh <- driveResult{row: result, err: err} + }() + select { + case <-ctx.Done(): + return fail(connect.CodeUnavailable, errors.New("mirror still delivering")) + case result := <-resultCh: + if ctx.Err() != nil { + return fail(connect.CodeUnavailable, errors.New("mirror still delivering")) + } + if result.row.State == 3 { + if refusal, ok := errors.AsType[*mirrorRefusalError](result.err); ok { + return fail(refusal.code, result.err) + } + if errors.Is(result.err, forge.ErrBodyTooLarge) { + return fail(connect.CodeInvalidArgument, result.err) + } + if errors.Is(result.err, store.ErrLeaseLost) { + return fail(connect.CodeUnavailable, result.err) + } + return fail(connect.CodeInternal, result.err) + } + if result.err != nil { + return "", connect.NewError(connect.CodeInternal, result.err) + } + if result.row.State != 2 { + return fail(connect.CodeUnavailable, errors.New("mirror row was not delivered")) + } + return result.row.CommentURL, nil + } +} + +func truncateBody(body string, limit int) string { + if limit <= 0 || len(body) <= limit { + return body + } + body = body[:limit] + for !utf8.ValidString(body) { + body = body[:len(body)-1] + } + return body +} + +func messageAuthors(messages []store.Message) map[store.AccountID]string { + authors := make(map[store.AccountID]string, len(messages)) + for _, message := range messages { + authors[message.AuthorAccountID] = message.AuthorHandle + } + return authors +} + +func parseConnectCode(code string) connect.Code { + switch code { + case connect.CodeInvalidArgument.String(): + return connect.CodeInvalidArgument + case connect.CodeNotFound.String(): + return connect.CodeNotFound + case connect.CodePermissionDenied.String(): + return connect.CodePermissionDenied + case connect.CodeFailedPrecondition.String(): + return connect.CodeFailedPrecondition + default: + return connect.CodeInternal + } +} + +func mirrorStoreError(err error) error { + switch { + case errors.Is(err, store.ErrNotFound): + return connect.NewError(connect.CodeNotFound, err) + case errors.Is(err, store.ErrInvalidArgument): + return connect.NewError(connect.CodeInvalidArgument, err) + case errors.Is(err, store.ErrPermissionDenied): + return connect.NewError(connect.CodePermissionDenied, err) + case errors.Is(err, store.ErrFailedPrecondition): + return connect.NewError(connect.CodeFailedPrecondition, err) + case errors.Is(err, store.ErrConflict): + return connect.NewError(connect.CodeAlreadyExists, err) + default: + return connect.NewError(connect.CodeInternal, err) + } +} diff --git a/go/server/forge_mirror_pgtest_test.go b/go/server/forge_mirror_pgtest_test.go new file mode 100644 index 000000000..2bf11a448 --- /dev/null +++ b/go/server/forge_mirror_pgtest_test.go @@ -0,0 +1,773 @@ +//go:build pgtest && unix + +package server + +import ( + "context" + "errors" + "fmt" + "strings" + "testing" + "time" + + "connectrpc.com/connect" + "github.com/jackc/pgx/v5" + + "github.com/RigelBuild/compass/go/events" + compassv1 "github.com/RigelBuild/compass/go/gen/compass/v1" + "github.com/RigelBuild/compass/go/internal/auth" + "github.com/RigelBuild/compass/go/internal/comms" + "github.com/RigelBuild/compass/go/internal/forge" + "github.com/RigelBuild/compass/go/internal/mirror" + "github.com/RigelBuild/compass/go/internal/refs" + "github.com/RigelBuild/compass/go/internal/store" +) + +type forgeMirrorLeaseLossPoster struct { + store *store.Store + rowID string + deleteRow bool +} + +func (*forgeMirrorLeaseLossPoster) PostMirror(context.Context, store.AccountID, store.ForgeMirrorMode, refs.Ref, string, string, string) (forge.Comment, error) { + return forge.Comment{}, errors.New("unexpected post during recheck") +} + +func (p *forgeMirrorLeaseLossPoster) ListMirrorComments(ctx context.Context, _ store.AccountID, _ refs.Ref, _ time.Time) ([]forge.Comment, error) { + if err := p.store.WithTx(ctx, func(tx pgx.Tx) error { + query := `UPDATE forge_mirrors SET lease_until = now() - interval '1 second' WHERE id = $1` + if p.deleteRow { + query = `DELETE FROM forge_mirrors WHERE id = $1` + } + _, err := tx.Exec(ctx, query, p.rowID) + return err + }); err != nil { + return nil, err + } + return []forge.Comment{{ID: 30, Key: "comment", Body: mirror.WithMarker("body", p.rowID)}}, nil +} + +func (*forgeMirrorLeaseLossPoster) DeleteMirrorComment(context.Context, store.AccountID, refs.Ref, string) error { + return nil +} + +func (*forgeMirrorLeaseLossPoster) BodyLimit(refs.Ref) int { return 4096 } + +func TestForgeMirrorRecheckLeaseLossReschedulesOnce(t *testing.T) { + delays, timers := runLeaseLossRecheck(t, "mirror-lease-loss", false) + if len(delays) != 2 || delays[1] != time.Minute || timers != 1 { + t.Fatalf("scheduled delays=%v pending timers=%d, want one lease-loss retry at 1m", delays, timers) + } +} + +func TestForgeMirrorRecheckLeaseLossRetriesWhenRereadFails(t *testing.T) { + delays, timers := runLeaseLossRecheck(t, "mirror-lease-reread", true) + if len(delays) != 2 || delays[1] != time.Minute || timers != 1 { + t.Fatalf("scheduled delays=%v pending timers=%d, want one retry at 1m after a failed reread", delays, timers) + } +} + +func runLeaseLossRecheck(t *testing.T, handle string, deleteRow bool) ([]time.Duration, int) { + t.Helper() + ctx := context.Background() + st := forgeTestStore(t) + owner, err := st.CreateUser(ctx, store.NewUser{Handle: handle, DisplayName: "Mirror Lease Loss"}) + if err != nil { + t.Fatalf("CreateUser: %v", err) + } + row, created, err := st.ClaimForgeMirror(ctx, store.ForgeMirrorRow{ + Source: 2, SourceKey: handle + "-recheck", Actor: owner.ID, TargetRef: "owner/repo#30", BodyRev: 1, State: 1, + }) + if err != nil || !created { + t.Fatalf("ClaimForgeMirror: created=%v err=%v", created, err) + } + if _, ok, err := st.LeaseForgeMirror(ctx, row.ID, "lease-loss-setup", time.Minute); err != nil || !ok { + t.Fatalf("LeaseForgeMirror: ok=%v err=%v", ok, err) + } + if err := st.MarkForgeMirror(ctx, row.ID, "lease-loss-setup", store.ForgeMirrorMark{PostedRev: 1, State: 2, CommentID: 30, CommentKey: "comment", Recheck: true}); err != nil { + t.Fatalf("MarkForgeMirror: %v", err) + } + if err := st.WithTx(ctx, func(tx pgx.Tx) error { + _, err := tx.Exec(ctx, `UPDATE forge_mirrors SET recheck_at = now() - interval '1 second' WHERE id = $1`, row.ID) + return err + }); err != nil { + t.Fatalf("expire recheck: %v", err) + } + row, err = st.ForgeMirrorByID(ctx, row.ID) + if err != nil { + t.Fatalf("ForgeMirrorByID: %v", err) + } + svc, _ := newForgeServiceForTest(t, forge.NewFakeProvider("gh-author"), forge.NewFakeProvider("gh-reviewer")) + svc.mirrorStoreDB = st + svc.mirrorPoster = &forgeMirrorLeaseLossPoster{store: st, rowID: row.ID, deleteRow: deleteRow} + delays := make([]time.Duration, 0, 2) + callbacks := make([]func(), 0, 2) + svc.scheduleMirrorAfter = func(delay time.Duration, fn func()) func() bool { + delays = append(delays, delay) + callbacks = append(callbacks, fn) + return func() bool { return true } + } + svc.SetMirrorLifetime(ctx) + t.Cleanup(svc.StopMirrorLifetime) + svc.scheduleMirrorRecheck(ctx, row, nil, false) + callbacks[0]() + return delays, len(svc.mirrorTimers) +} + +func TestForgeMirrorFailedRechecksKeepOnePendingTimer(t *testing.T) { + svc, _ := newForgeServiceForTest(t, forge.NewFakeProvider("gh-author"), forge.NewFakeProvider("gh-reviewer")) + delays := make([]time.Duration, 0, 3) + svc.scheduleMirrorAfter = func(delay time.Duration, _ func()) func() bool { + delays = append(delays, delay) + return func() bool { return true } + } + svc.SetMirrorLifetime(context.Background()) + t.Cleanup(svc.StopMirrorLifetime) + recheckAt := time.Now().Add(-time.Second) + row := store.ForgeMirrorRow{ID: "failed-recheck", RecheckAt: &recheckAt} + for range 3 { + svc.scheduleMirrorRecheck(context.Background(), row, nil, true) + } + if len(delays) != 1 || delays[0] != time.Minute || len(svc.mirrorTimers) != 1 { + t.Fatalf("scheduled delays = %v pending timers = %d, want one 1m retry", delays, len(svc.mirrorTimers)) + } +} + +func TestForgeMirrorFailedDueRecheckBacksOff(t *testing.T) { + ctx := context.Background() + storeDB := forgeTestStore(t) + user, err := storeDB.CreateUser(ctx, store.NewUser{Handle: "mirror-retry-user", DisplayName: "Mirror Retry User"}) + if err != nil { + t.Fatalf("CreateUser: %v", err) + } + if err := storeDB.SetForgeMirrorSetting(ctx, user.ID, store.ForgeMirrorSetting{Provider: store.ForgeProviderGitHub, Host: "github.com", Repo: "owner/repo", Mode: store.ForgeMirrorManual}); err != nil { + t.Fatalf("SetForgeMirrorSetting: %v", err) + } + row, created, err := storeDB.ClaimForgeMirror(ctx, store.ForgeMirrorRow{ + Source: 2, SourceKey: "retry-topic", Actor: user.ID, TargetRef: "owner/repo#13", BodyRev: 1, PostedRev: 0, State: 1, + }) + if err != nil || !created { + t.Fatalf("ClaimForgeMirror: created=%v err=%v", created, err) + } + if _, ok, err := storeDB.LeaseForgeMirror(ctx, row.ID, "retry-lease", time.Minute); err != nil || !ok { + t.Fatalf("LeaseForgeMirror: ok=%v err=%v", ok, err) + } + if err := storeDB.MarkForgeMirror(ctx, row.ID, "retry-lease", store.ForgeMirrorMark{PostedRev: 1, State: 2, CommentKey: "comment", Recheck: true}); err != nil { + t.Fatalf("MarkForgeMirror: %v", err) + } + if err := storeDB.WithTx(ctx, func(tx pgx.Tx) error { + _, err := tx.Exec(ctx, `UPDATE forge_mirrors SET recheck_at = now() - interval '1 second' WHERE id = $1`, row.ID) + return err + }); err != nil { + t.Fatalf("expire recheck: %v", err) + } + row, err = storeDB.ForgeMirrorByID(ctx, row.ID) + if err != nil { + t.Fatalf("ForgeMirrorByID: %v", err) + } + poster := forge.NewFakeProvider("gh-author") + poster.SetError("ListCommentsSince", errors.New("temporary comment scan failure")) + svc, _ := newForgeServiceForTest(t, poster, forge.NewFakeProvider("gh-reviewer")) + svc.providers.entries[forgeCoordinate{provider: compassv1.ForgeProvider_FORGE_PROVIDER_GITHUB, host: "github.com"}] = forgeProviderRoles{ + author: &mirrorIdentityProvider{Provider: poster, login: "gh-author"}, + reviewer: forge.NewFakeProvider("gh-reviewer"), + } + svc.store = storeDB + svc.mirrorStoreDB = storeDB + delays := make([]time.Duration, 0, 4) + callbacks := make([]func(), 0, 4) + svc.scheduleMirrorAfter = func(delay time.Duration, fn func()) func() bool { + delays = append(delays, delay) + callbacks = append(callbacks, fn) + return func() bool { return true } + } + svc.SetMirrorLifetime(ctx) + t.Cleanup(svc.StopMirrorLifetime) + svc.scheduleMirrorRecheck(ctx, row, nil, false) + for range 3 { + if err := storeDB.WithTx(ctx, func(tx pgx.Tx) error { + _, err := tx.Exec(ctx, `UPDATE forge_mirrors SET lease_until = now() - interval '1 second' WHERE id = $1`, row.ID) + return err + }); err != nil { + t.Fatalf("expire retry lease: %v", err) + } + callbacks[len(callbacks)-1]() + } + if len(delays) != 4 || delays[0] != 0 || delays[1] < time.Minute || delays[1] > 2*time.Minute || delays[2] != 5*time.Minute || delays[3] != 15*time.Minute { + t.Fatalf("scheduled delays = %v, want immediate due check, lease expiry, then 5m and 15m retries", delays) + } +} + +func TestForgeMirrorAsUserAuthorizesVisibleTopicAndEditsOneComment(t *testing.T) { + ctx := context.Background() + realStore := forgeTestStore(t) + user, err := realStore.CreateUser(ctx, store.NewUser{Handle: "mirror-clicker", DisplayName: "Mirror Clicker"}) + if err != nil { + t.Fatalf("CreateUser: %v", err) + } + channel, err := realStore.CreateChannel(ctx, user.ID, store.NewChannel{ + Name: "mirror-visible", Kind: store.ChannelKindChannel, MemberAccountIDs: []store.AccountID{user.ID}, + }) + if err != nil { + t.Fatalf("CreateChannel: %v", err) + } + text := "first source message" + message, _, err := realStore.AppendMessage(ctx, store.Message{ + AuthorAccountID: user.ID, Blocks: []store.MessageBlock{{Text: &text}}, + }, string(channel.ID), store.TopicRef{Name: "general", Create: true}, "") + if err != nil { + t.Fatalf("AppendMessage: %v", err) + } + author, svc, fakeStore := setupUserMirrorTest(t, realStore, user) + request := &compassv1.MirrorToForgeRequest{Ref: "owner/repo#12", TopicId: message.TopicID, ChannelId: string(channel.ID)} + if _, err := svc.MirrorAsUser(ctx, fakeStore.accounts[user.ID], request); err != nil { + t.Fatalf("MirrorAsUser(topic): %v", err) + } + calls := author.Calls() + if len(calls) != 1 || calls[0].Method != "CommentOnIssue" || !strings.Contains(calls[0].Body, "first source message") { + t.Fatalf("topic mirror calls = %+v, want one comment with source", calls) + } + text = "second source message" + if _, _, err := realStore.AppendMessage(ctx, store.Message{ + AuthorAccountID: user.ID, Blocks: []store.MessageBlock{{Text: &text}}, + }, string(channel.ID), store.TopicRef{ID: message.TopicID}, ""); err != nil { + t.Fatalf("AppendMessage(second): %v", err) + } + if _, err := svc.MirrorAsUser(ctx, fakeStore.accounts[user.ID], request); err != nil { + t.Fatalf("MirrorAsUser(second click): %v", err) + } + calls = author.Calls() + if len(calls) != 2 || calls[0].Method != "CommentOnIssue" || calls[1].Method != "EditComment" || calls[1].CommentKey != "comment-9" { + t.Fatalf("provider calls = %+v, want one comment followed by an edit of the same comment", calls) + } + for _, call := range calls { + clean, _, stamped := forge.StripOwner(call.Body) + if stamped || strings.Count(clean, "Mirrored by @mirror-clicker") != 1 || !strings.Contains(call.Body, "compass-mirror:") { + t.Errorf("manual body = %q, want one user attribution, a marker, and no owner header", call.Body) + } + } + if !strings.Contains(calls[1].Body, "second source message") { + t.Fatalf("edited body = %q, want current topic contents", calls[1].Body) + } +} + +func TestForgeMirrorSelectedMessageIDsCanonicalizeAndRejectDuplicates(t *testing.T) { + ctx := context.Background() + st := forgeTestStore(t) + user, err := st.CreateUser(ctx, store.NewUser{Handle: "mirror-selected-canonical", DisplayName: "Mirror Selected Canonical"}) + if err != nil { + t.Fatalf("CreateUser: %v", err) + } + channel, err := st.CreateChannel(ctx, user.ID, store.NewChannel{ + Name: "mirror-selected-canonical", Kind: store.ChannelKindChannel, MemberAccountIDs: []store.AccountID{user.ID}, + }) + if err != nil { + t.Fatalf("CreateChannel: %v", err) + } + firstText, secondText := "first selected message", "second selected message" + first, _, err := st.AppendMessage(ctx, store.Message{AuthorAccountID: user.ID, Blocks: []store.MessageBlock{{Text: &firstText}}}, string(channel.ID), store.TopicRef{Name: "first", Create: true}, "") + if err != nil { + t.Fatalf("AppendMessage(first): %v", err) + } + second, _, err := st.AppendMessage(ctx, store.Message{AuthorAccountID: user.ID, Blocks: []store.MessageBlock{{Text: &secondText}}}, string(channel.ID), store.TopicRef{Name: "second", Create: true}, "") + if err != nil { + t.Fatalf("AppendMessage(second): %v", err) + } + author, svc, fakeStore := setupUserMirrorTest(t, st, user) + if _, err := svc.MirrorAsUser(ctx, fakeStore.accounts[user.ID], &compassv1.MirrorToForgeRequest{ + Ref: "owner/repo#12", ChannelId: string(channel.ID), MessageIds: []string{string(first.ID), string(second.ID), string(first.ID)}, + }); connect.CodeOf(err) != connect.CodeInvalidArgument { + t.Fatalf("MirrorAsUser(duplicate IDs) error = %v, want invalid_argument", err) + } + if calls := author.Calls(); len(calls) != 0 { + t.Fatalf("provider calls after duplicate IDs = %+v, want none", calls) + } + if _, err := svc.MirrorAsUser(ctx, fakeStore.accounts[user.ID], &compassv1.MirrorToForgeRequest{ + Ref: "owner/repo#12", ChannelId: string(channel.ID), MessageIds: []string{string(second.ID), string(first.ID)}, + }); err != nil { + t.Fatalf("MirrorAsUser(reversed IDs): %v", err) + } + if _, err := svc.MirrorAsUser(ctx, fakeStore.accounts[user.ID], &compassv1.MirrorToForgeRequest{ + Ref: "owner/repo#12", ChannelId: string(channel.ID), MessageIds: []string{string(first.ID), string(second.ID)}, + }); err != nil { + t.Fatalf("MirrorAsUser(forward IDs): %v", err) + } + calls := author.Calls() + if len(calls) != 2 || calls[0].Method != "CommentOnIssue" || calls[1].Method != "EditComment" || calls[1].CommentKey != "comment-9" { + t.Fatalf("provider calls = %+v, want one comment followed by edit of the same comment", calls) + } + if !strings.Contains(calls[1].Body, firstText) || !strings.Contains(calls[1].Body, secondText) || strings.Count(calls[1].Body, "Mirrored by @mirror-selected-canonical") != 1 { + t.Fatalf("edited body = %q, want both selected messages and one user attribution", calls[1].Body) + } +} + +func TestForgeMirrorSelectedMessageRequiresCurrentMembership(t *testing.T) { + ctx := context.Background() + st := forgeTestStore(t) + user, err := st.CreateUser(ctx, store.NewUser{Handle: "mirror-membership", DisplayName: "Mirror Membership"}) + if err != nil { + t.Fatalf("CreateUser: %v", err) + } + channel, err := st.CreateChannel(ctx, user.ID, store.NewChannel{ + Name: "mirror-membership", Kind: store.ChannelKindChannel, MemberAccountIDs: []store.AccountID{user.ID}, + }) + if err != nil { + t.Fatalf("CreateChannel: %v", err) + } + text := "secret source" + posted, _, err := st.AppendMessage(ctx, store.Message{AuthorAccountID: user.ID, Blocks: []store.MessageBlock{{Text: &text}}}, string(channel.ID), store.TopicRef{Name: "membership-topic", Create: true}, "") + if err != nil { + t.Fatalf("AppendMessage: %v", err) + } + if _, _, err := st.UpdateChannelMembers(ctx, user.ID, channel.ID, []store.MemberUpdate{{AccountID: user.ID, Remove: true}}, store.MemberUpdatesOptions{}); err != nil { + t.Fatalf("UpdateChannelMembers(remove self): %v", err) + } + _, svc, fakeStore := setupUserMirrorTest(t, st, user) + request := &compassv1.MirrorToForgeRequest{ChannelId: string(channel.ID), MessageIds: []string{string(posted.ID)}} + _, _, err = svc.resolveUserMirrorSource(ctx, fakeStore.accounts[user.ID], request, channel.Name) + if connect.CodeOf(err) != connect.CodeNotFound { + t.Fatalf("resolveUserMirrorSource after membership removal = %v, want not_found", err) + } +} + +func TestForgeMirrorTopicKeepsNamesWithoutPublicURL(t *testing.T) { + ctx := context.Background() + st := forgeTestStore(t) + user, err := st.CreateUser(ctx, store.NewUser{Handle: "mirror-topic-names", DisplayName: "Mirror Topic Names"}) + if err != nil { + t.Fatalf("CreateUser: %v", err) + } + channel, err := st.CreateChannel(ctx, user.ID, store.NewChannel{ + Name: "topic-channel-name", Kind: store.ChannelKindChannel, MemberAccountIDs: []store.AccountID{user.ID}, + }) + if err != nil { + t.Fatalf("CreateChannel: %v", err) + } + text := "topic message" + message, _, err := st.AppendMessage(ctx, store.Message{ + AuthorAccountID: user.ID, Blocks: []store.MessageBlock{{Text: &text}}, + }, string(channel.ID), store.TopicRef{Name: "topic-name", Create: true}, "") + if err != nil { + t.Fatalf("AppendMessage: %v", err) + } + _, svc, fakeStore := setupUserMirrorTest(t, st, user) + svc.publicURL = "" + request := &compassv1.MirrorToForgeRequest{Ref: "owner/repo#12", TopicId: message.TopicID, ChannelId: string(channel.ID)} + _, body, err := svc.resolveUserMirrorSource(ctx, fakeStore.accounts[user.ID], request, channel.Name) + if err != nil { + t.Fatalf("resolveUserMirrorSource: %v", err) + } + rendered, err := body(ctx, store.ForgeMirrorRow{}, 4096) + if err != nil { + t.Fatalf("render topic body: %v", err) + } + if !strings.Contains(rendered, "#topic-channel-name › topic-name") || strings.Contains(rendered, string(channel.ID)) || strings.Contains(rendered, message.TopicID) { + t.Fatalf("topic body header = %q, want channel and topic names without IDs", rendered) + } +} + +func TestForgeMirrorTopicNotesNewestFiftyMessages(t *testing.T) { + ctx := context.Background() + st := forgeTestStore(t) + user, err := st.CreateUser(ctx, store.NewUser{Handle: "mirror-topic-limit", DisplayName: "Mirror Topic Limit"}) + if err != nil { + t.Fatalf("CreateUser: %v", err) + } + channel, err := st.CreateChannel(ctx, user.ID, store.NewChannel{ + Name: "topic-limit-channel", Kind: store.ChannelKindChannel, MemberAccountIDs: []store.AccountID{user.ID}, + }) + if err != nil { + t.Fatalf("CreateChannel: %v", err) + } + var topicID string + for i := range 51 { + text := fmt.Sprintf("topic message %02d", i) + topic := store.TopicRef{ID: topicID} + if topicID == "" { + topic = store.TopicRef{Name: "topic-limit", Create: true} + } + message, _, err := st.AppendMessage(ctx, store.Message{ + AuthorAccountID: user.ID, Blocks: []store.MessageBlock{{Text: &text}}, + }, string(channel.ID), topic, "") + if err != nil { + t.Fatalf("AppendMessage(%d): %v", i, err) + } + topicID = message.TopicID + } + _, svc, fakeStore := setupUserMirrorTest(t, st, user) + request := &compassv1.MirrorToForgeRequest{Ref: "owner/repo#12", TopicId: topicID, ChannelId: string(channel.ID)} + _, body, err := svc.resolveUserMirrorSource(ctx, fakeStore.accounts[user.ID], request, channel.Name) + if err != nil { + t.Fatalf("resolveUserMirrorSource: %v", err) + } + rendered, err := body(ctx, store.ForgeMirrorRow{}, 100_000) + if err != nil { + t.Fatalf("render topic body: %v", err) + } + if !strings.Contains(rendered, "Showing the newest 50 messages.") || !strings.Contains(rendered, "topic message 50") || strings.Contains(rendered, "topic message 00") { + t.Fatalf("topic mirror body omits cap note or wrong messages: %q", rendered) + } +} + +type forgeMirrorSignalDriver struct { + inner comms.MirrorDriver + done chan struct{} +} + +func (d forgeMirrorSignalDriver) DriveAsks(ctx context.Context, askIDs []string) { + d.inner.DriveAsks(ctx, askIDs) + d.done <- struct{}{} +} + +func waitForForgeMirrorDrive(t *testing.T, done <-chan struct{}) { + t.Helper() + select { + case <-done: + case <-time.After(5 * time.Second): + t.Fatal("ask mirror driver did not finish") + } +} + +func setupUserMirrorTest(t *testing.T, st *store.Store, user store.Account) (*forge.FakeProvider, *forgeService, *fakeForgeStore) { + t.Helper() + author := forge.NewFakeProvider("gh-author") + author.CommentResult = forge.Comment{ID: 9, Key: "comment-9", URL: "https://github.com/owner/repo/issues/12#issuecomment-9"} + author.EditCommentResult = forge.Comment{ID: 9, Key: "comment-9", URL: author.CommentResult.URL} + svc, fakeStore := newForgeServiceForTest(t, author, forge.NewFakeProvider("gh-reviewer")) + svc.mirrorStoreDB = st + fakeStore.accounts[user.ID] = store.Account{ID: user.ID, Handle: user.Handle, User: &store.UserAccount{Role: store.UserRoleMember}} + fakeStore.mirrorModes["1|github.com|owner/repo"] = store.ForgeMirrorManual + return author, svc, fakeStore +} + +func assertForgeMirrorPosted(t *testing.T, st *store.Store, ctx context.Context, askID string, postedRev int16) { + t.Helper() + rows, err := st.AskForgeMirrors(ctx, []string{askID}) + if err != nil { + t.Fatalf("AskForgeMirrors: %v", err) + } + if len(rows) != 1 || rows[0].State != 2 || rows[0].PostedRev != postedRev || rows[0].CommentKey == "" { + t.Fatalf("ask mirror rows = %+v, want one delivered row at revision %d", rows, postedRev) + } +} + +func TestCommittedAskMirrorsAndAnswerEditsOneComment(t *testing.T) { + ctx := context.Background() + st := forgeTestStore(t) + owner, err := st.CreateUser(ctx, store.NewUser{Handle: "mirror-ask-owner", DisplayName: "Mirror Ask Owner"}) + if err != nil { + t.Fatalf("CreateUser: %v", err) + } + agent, err := st.CreateAgent(ctx, owner.ID, store.NewAgent{Handle: "mirror-ask-agent", DisplayName: "Mirror Ask Agent"}) + if err != nil { + t.Fatalf("CreateAgent: %v", err) + } + channel, err := st.CreateChannel(ctx, owner.ID, store.NewChannel{ + Name: "mirror-auto-asks", Kind: store.ChannelKindChannel, MemberAccountIDs: []store.AccountID{owner.ID, agent.ID}, + }) + if err != nil { + t.Fatalf("CreateChannel: %v", err) + } + coord := store.ForgeMirrorSetting{Provider: store.ForgeProviderGitHub, Host: "github.com", Repo: "owner/auto", Mode: store.ForgeMirrorAuto} + if err := st.SetForgeMirrorSetting(ctx, owner.ID, coord); err != nil { + t.Fatalf("SetForgeMirrorSetting: %v", err) + } + if err := st.GrantForgeScope(ctx, store.ForgeScope{AccountID: owner.ID, Provider: coord.Provider, Host: coord.Host, Repo: coord.Repo}); err != nil { + t.Fatalf("GrantForgeScope: %v", err) + } + author := forge.NewFakeProvider("gh-author") + author.CommentResult = forge.Comment{ID: 15, Key: "auto-comment", URL: "https://github.com/owner/auto/issues/12#issuecomment-15"} + author.EditCommentResult = forge.Comment{ID: 15, Key: "auto-comment", URL: author.CommentResult.URL} + commsBus := events.NewBus[*compassv1.SubscribeCommsResponse]() + t.Cleanup(commsBus.Close) + cm := comms.NewComms(st, commsBus, nil, owner.ID) + forgeSvc, _ := newForgeServiceForTest(t, author, forge.NewFakeProvider("gh-reviewer")) + forgeSvc.store = st + forgeSvc.mirrorStoreDB = st + forgeSvc.enforceScopes = true + done := make(chan struct{}, 2) + cm.SetMirrorer(forgeSvc, forgeMirrorSignalDriver{inner: forgeSvc, done: done}) + posted, err := cm.PostMessage(comms.WithActor(ctx, agent.ID), connect.NewRequest(&compassv1.PostMessageRequest{ + Container: &compassv1.PostMessageRequest_ChannelId{ChannelId: string(channel.ID)}, + Topic: &compassv1.PostMessageRequest_TopicName{TopicName: "ask-thread"}, CreateTopic: true, + Blocks: []*compassv1.MessageBlock{{Block: &compassv1.MessageBlock_Ask{Ask: &compassv1.Ask{ + Ref: "owner/auto#12", Questions: []*compassv1.AskQuestion{{QuestionId: "q1", Question: "Ship this?", Options: []*compassv1.AskOption{{Id: "yes", Label: "Yes"}}}}, + }}}}, + })) + if err != nil { + t.Fatalf("PostMessage: %v", err) + } + waitForForgeMirrorDrive(t, done) + askID := posted.Msg.GetMessage().GetBlocks()[0].GetAsk().GetAskId() + assertForgeMirrorPosted(t, st, ctx, askID, 1) + clicker, err := st.GetAccount(ctx, owner.ID) + if err != nil { + t.Fatalf("GetAccount(owner): %v", err) + } + clickURL, err := forgeSvc.MirrorAsUser(ctx, clicker, &compassv1.MirrorToForgeRequest{Ref: "owner/auto#12", AskId: askID, ChannelId: string(channel.ID)}) + if err != nil || clickURL != author.CommentResult.URL { + t.Fatalf("MirrorAsUser(auto ask) = (%q, %v), want existing comment URL", clickURL, err) + } + calls := author.Calls() + if len(calls) != 1 || calls[0].Method != "CommentOnIssue" || !strings.Contains(calls[0].Body, "agent=mirror-ask-agent owner=mirror-ask-owner") || !strings.Contains(calls[0].Body, "compass-mirror:") { + t.Fatalf("automatic ask post calls = %+v, want one stamped comment with a marker", calls) + } + if !strings.Contains(calls[0].Body, "#mirror-auto-asks › ask-thread") || strings.Contains(calls[0].Body, string(channel.ID)) || strings.Contains(calls[0].Body, askID) { + t.Fatalf("automatic ask body = %q, want channel/topic names without ids", calls[0].Body) + } + if _, err := cm.RespondToAsk(comms.WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.RespondToAskRequest{ + AskId: askID, Answers: []*compassv1.AskQuestionAnswer{{QuestionId: "q1", ChosenOptionIds: []string{"yes"}}}, + })); err != nil { + t.Fatalf("RespondToAsk: %v", err) + } + waitForForgeMirrorDrive(t, done) + assertForgeMirrorPosted(t, st, ctx, askID, 2) + calls = author.Calls() + if len(calls) != 2 || calls[1].Method != "EditComment" || calls[1].CommentKey != "auto-comment" || !strings.Contains(calls[1].Body, "Yes") { + t.Fatalf("automatic ask answer calls = %+v, want edit of original comment containing answer", calls) + } +} + +func TestForgeMirrorModeOffBeforePostMarksRowFailed(t *testing.T) { + ctx := context.Background() + st := forgeTestStore(t) + owner, err := st.CreateUser(ctx, store.NewUser{Handle: "mirror-off-owner", DisplayName: "Mirror Off Owner"}) + if err != nil { + t.Fatalf("CreateUser: %v", err) + } + agent, err := st.CreateAgent(ctx, owner.ID, store.NewAgent{Handle: "mirror-off-agent", DisplayName: "Mirror Off Agent"}) + if err != nil { + t.Fatalf("CreateAgent: %v", err) + } + channel, err := st.CreateChannel(ctx, owner.ID, store.NewChannel{ + Name: "mirror-off-channel", Kind: store.ChannelKindChannel, MemberAccountIDs: []store.AccountID{owner.ID, agent.ID}, + }) + if err != nil { + t.Fatalf("CreateChannel: %v", err) + } + coord := store.ForgeMirrorSetting{Provider: store.ForgeProviderGitHub, Host: "github.com", Repo: "owner/auto", Mode: store.ForgeMirrorAuto} + if err := st.SetForgeMirrorSetting(ctx, owner.ID, coord); err != nil { + t.Fatalf("SetForgeMirrorSetting(auto): %v", err) + } + text := "Ship this?" + msg, _, err := st.AppendMessage(ctx, store.Message{AuthorAccountID: agent.ID, Blocks: []store.MessageBlock{{Ask: &store.Ask{ + Ref: "owner/auto#12", RefCoord: &store.ForgeCoord{Provider: coord.Provider, Host: coord.Host, Repo: coord.Repo, Number: 12}, + Questions: []store.AskQuestion{{QuestionID: "q1", Question: text, Options: []store.AskOption{{ID: "yes", Label: "Yes"}}}}, + }}}}, string(channel.ID), store.TopicRef{Name: "mirror-off-topic", Create: true}, "") + if err != nil { + t.Fatalf("AppendMessage: %v", err) + } + if err := st.SetForgeMirrorSetting(ctx, owner.ID, store.ForgeMirrorSetting{Provider: coord.Provider, Host: coord.Host, Repo: coord.Repo, Mode: store.ForgeMirrorOff}); err != nil { + t.Fatalf("SetForgeMirrorSetting(off): %v", err) + } + author := forge.NewFakeProvider("gh-author") + svc, _ := newForgeServiceForTest(t, author, forge.NewFakeProvider("gh-reviewer")) + svc.store = st + svc.mirrorStoreDB = st + svc.DriveAsks(ctx, []string{msg.Blocks[0].Ask.AskID}) + rows, err := st.AskForgeMirrors(ctx, []string{msg.Blocks[0].Ask.AskID}) + if err != nil || len(rows) != 1 || rows[0].State != 3 { + t.Fatalf("ask mirror rows = %+v, err=%v; want one failed row", rows, err) + } + if calls := author.Calls(); len(calls) != 0 { + t.Fatalf("forge provider calls = %+v, want no comment after disabling coordinate", calls) + } +} + +func TestForgeMirrorAsUserRefusesInvisibleAndInvalidSources(t *testing.T) { + ctx := context.Background() + st := forgeTestStore(t) + user, err := st.CreateUser(ctx, store.NewUser{Handle: "mirror-visible-user", DisplayName: "Mirror Visible User"}) + if err != nil { + t.Fatalf("CreateUser: %v", err) + } + channel, err := st.CreateChannel(ctx, user.ID, store.NewChannel{Name: "visible", Kind: store.ChannelKindChannel, MemberAccountIDs: []store.AccountID{user.ID}}) + if err != nil { + t.Fatalf("CreateChannel: %v", err) + } + other, err := st.CreateUser(ctx, store.NewUser{Handle: "mirror-invisible-user", DisplayName: "Mirror Invisible User"}) + if err != nil { + t.Fatalf("CreateUser(other): %v", err) + } + invisible, err := st.CreateChannel(ctx, other.ID, store.NewChannel{Name: "invisible", Kind: store.ChannelKindChannel, MemberAccountIDs: []store.AccountID{other.ID}}) + if err != nil { + t.Fatalf("CreateChannel(invisible): %v", err) + } + _, svc, fakeStore := setupUserMirrorTest(t, st, user) + if _, err := svc.MirrorAsUser(ctx, fakeStore.accounts[user.ID], &compassv1.MirrorToForgeRequest{Ref: "owner/repo#12", TopicId: "missing", ChannelId: string(invisible.ID)}); connect.CodeOf(err) != connect.CodeNotFound { + t.Fatalf("invisible channel error = %v, want not_found", err) + } + if _, err := svc.MirrorAsUser(ctx, fakeStore.accounts[user.ID], &compassv1.MirrorToForgeRequest{Ref: "owner/repo#12", AskId: "ask", TopicId: "topic", ChannelId: string(channel.ID)}); connect.CodeOf(err) != connect.CodeInvalidArgument { + t.Fatalf("invalid source error = %v, want invalid_argument", err) + } + if _, err := svc.MirrorAsUser(ctx, fakeStore.accounts[user.ID], &compassv1.MirrorToForgeRequest{Ref: "owner/disabled#13", TopicId: "topic", ChannelId: string(channel.ID)}); connect.CodeOf(err) != connect.CodeFailedPrecondition { + t.Fatalf("off target error = %v, want failed_precondition", err) + } +} + +func TestForgeMirrorAsUserMirrorsAskWithUserAttribution(t *testing.T) { + ctx := context.Background() + st := forgeTestStore(t) + user, err := st.CreateUser(ctx, store.NewUser{Handle: "mirror-ask-user", DisplayName: "Mirror Ask User"}) + if err != nil { + t.Fatalf("CreateUser: %v", err) + } + channel, err := st.CreateChannel(ctx, user.ID, store.NewChannel{Name: "ask-channel", Kind: store.ChannelKindChannel, MemberAccountIDs: []store.AccountID{user.ID}}) + if err != nil { + t.Fatalf("CreateChannel: %v", err) + } + ask := &store.Ask{Ref: "owner/repo#12", Questions: []store.AskQuestion{{QuestionID: "q", Question: "Ship this ask?", Options: []store.AskOption{{ID: "yes", Label: "Yes"}}}}} + msg, _, err := st.AppendMessage(ctx, store.Message{AuthorAccountID: user.ID, Blocks: []store.MessageBlock{{Ask: ask}}}, string(channel.ID), store.TopicRef{Name: "asks", Create: true}, "") + if err != nil { + t.Fatalf("AppendMessage(ask): %v", err) + } + askID := msg.Blocks[0].Ask.AskID + author, svc, fakeStore := setupUserMirrorTest(t, st, user) + if _, err := svc.MirrorAsUser(ctx, fakeStore.accounts[user.ID], &compassv1.MirrorToForgeRequest{Ref: "owner/repo#12", AskId: askID, ChannelId: string(channel.ID)}); err != nil { + t.Fatalf("MirrorAsUser(ask): %v", err) + } + calls := author.Calls() + if len(calls) != 1 || !strings.Contains(calls[0].Body, "Ship this ask?") || !strings.Contains(calls[0].Body, "Mirrored by @mirror-ask-user") { + t.Fatalf("ask mirror calls = %+v, want ask text and user attribution", calls) + } +} + +type blockingMirrorPoster struct { + entered chan struct{} + release chan struct{} +} + +func (p *blockingMirrorPoster) PostMirror(ctx context.Context, _ store.AccountID, _ store.ForgeMirrorMode, _ refs.Ref, _ string, _ string, _ string) (forge.Comment, error) { + select { + case p.entered <- struct{}{}: + default: + } + select { + case <-p.release: + return forge.Comment{ID: 1, Key: "blocked-comment", URL: "https://forge.example/comment"}, nil + case <-ctx.Done(): + return forge.Comment{}, ctx.Err() + } +} + +func (*blockingMirrorPoster) ListMirrorComments(context.Context, store.AccountID, refs.Ref, time.Time) ([]forge.Comment, error) { + return nil, nil +} + +func (*blockingMirrorPoster) DeleteMirrorComment(context.Context, store.AccountID, refs.Ref, string) error { + return nil +} + +func (*blockingMirrorPoster) BodyLimit(refs.Ref) int { return 4096 } + +func TestForgeMirrorClickWaitIsBoundedByRequestContext(t *testing.T) { + ctx := context.Background() + st := forgeTestStore(t) + user, err := st.CreateUser(ctx, store.NewUser{Handle: "mirror-blocked", DisplayName: "Mirror Blocked"}) + if err != nil { + t.Fatalf("CreateUser: %v", err) + } + if err := st.SetForgeMirrorSetting(ctx, user.ID, store.ForgeMirrorSetting{Provider: store.ForgeProviderGitHub, Host: "github.com", Repo: "owner/repo", Mode: store.ForgeMirrorManual}); err != nil { + t.Fatalf("SetForgeMirrorSetting: %v", err) + } + row, created, err := st.ClaimForgeMirror(ctx, store.ForgeMirrorRow{Source: 2, SourceKey: "topic", Actor: user.ID, TargetRef: "owner/repo#12", BodyRev: 1, State: 1}) + if err != nil || !created { + t.Fatalf("ClaimForgeMirror: row=%+v created=%v err=%v", row, created, err) + } + poster := &blockingMirrorPoster{entered: make(chan struct{}, 1), release: make(chan struct{})} + svc, _ := newForgeServiceForTest(t, forge.NewFakeProvider("gh-author"), forge.NewFakeProvider("gh-reviewer")) + svc.store = st + svc.mirrorStoreDB = st + svc.mirrorPoster = poster + requestCtx, cancel := context.WithCancel(ctx) + t.Cleanup(func() { + select { + case <-poster.release: + default: + close(poster.release) + } + svc.StopMirrorLifetime() + }) + result := make(chan error, 1) + go func() { + _, err := svc.clickDrive(requestCtx, row, func(context.Context, store.ForgeMirrorRow, int) (string, error) { + return "source", nil + }, func(code connect.Code, err error) (string, error) { + return "", connect.NewError(code, err) + }) + result <- err + }() + select { + case <-poster.entered: + case <-time.After(5 * time.Second): + close(poster.release) + t.Fatal("blocking poster did not start") + } + cancel() + select { + case err := <-result: + if connect.CodeOf(err) != connect.CodeUnavailable || !strings.Contains(err.Error(), "mirror still delivering") { + t.Fatalf("click result = %v; want unavailable while delivery continues", err) + } + case <-time.After(5 * time.Second): + close(poster.release) + t.Fatal("MirrorAsUser did not return after request cancellation") + } + close(poster.release) + svc.StopMirrorLifetime() +} + +func TestForgeMirrorSettingsHandlersEnforceTenantAdmin(t *testing.T) { + ctx := context.Background() + st, admin, member := newNetworkStore(t) + if err := st.EnsureForgeRepoSubscription(ctx, store.ForgeRepoSubscription{Provider: store.ForgeProviderGitHub, Host: "github.com", Repo: "owner/settings", Enabled: true}); err != nil { + t.Fatalf("EnsureForgeRepoSubscription: %v", err) + } + adminToken, err := auth.IssueAccountToken(ctx, st, admin) + if err != nil { + t.Fatalf("IssueAccountToken(admin): %v", err) + } + memberToken, err := auth.IssueAccountToken(ctx, st, member) + if err != nil { + t.Fatalf("IssueAccountToken(member): %v", err) + } + bus := events.NewBus[busPayload]() + t.Cleanup(bus.Close) + client := networkDoorHandler(t, newService("forge-mirror-settings", bus, st, nil, nil, nil, nil), st, admin) + setRequest := func(token string, mode compassv1.ForgeMirrorMode) *connect.Request[compassv1.SetForgeMirrorSettingRequest] { + req := connect.NewRequest(&compassv1.SetForgeMirrorSettingRequest{Setting: &compassv1.ForgeMirrorSetting{ + Forge: &compassv1.ForgeRef{Provider: compassv1.ForgeProvider_FORGE_PROVIDER_GITHUB, Host: "github.com"}, + Repo: "owner/settings", Mode: mode, + }}) + req.Header().Set("Authorization", "Bearer "+token) + return req + } + if _, err := client.SetForgeMirrorSetting(ctx, setRequest(memberToken, compassv1.ForgeMirrorMode_FORGE_MIRROR_MODE_AUTO)); connect.CodeOf(err) != connect.CodePermissionDenied { + t.Fatalf("member SetForgeMirrorSetting code = %v, err = %v; want permission_denied", connect.CodeOf(err), err) + } + if _, err := client.SetForgeMirrorSetting(ctx, setRequest(adminToken, compassv1.ForgeMirrorMode_FORGE_MIRROR_MODE_UNSPECIFIED)); connect.CodeOf(err) != connect.CodeInvalidArgument { + t.Fatalf("UNSPECIFIED SetForgeMirrorSetting code = %v, err = %v; want invalid_argument", connect.CodeOf(err), err) + } + if _, err := client.SetForgeMirrorSetting(ctx, setRequest(adminToken, compassv1.ForgeMirrorMode_FORGE_MIRROR_MODE_AUTO)); err != nil { + t.Fatalf("admin SetForgeMirrorSetting: %v", err) + } + listRequest := func(token string) *connect.Request[compassv1.ListForgeMirrorSettingsRequest] { + req := connect.NewRequest(&compassv1.ListForgeMirrorSettingsRequest{}) + req.Header().Set("Authorization", "Bearer "+token) + return req + } + memberList, err := client.ListForgeMirrorSettings(ctx, listRequest(memberToken)) + if err != nil { + t.Fatalf("member ListForgeMirrorSettings: %v", err) + } + if memberList.Msg.GetCanEdit() || len(memberList.Msg.GetSettings()) != 1 || memberList.Msg.GetSettings()[0].GetMode() != compassv1.ForgeMirrorMode_FORGE_MIRROR_MODE_AUTO { + t.Fatalf("member settings response = %+v, want auto setting and can_edit=false", memberList.Msg) + } + adminList, err := client.ListForgeMirrorSettings(ctx, listRequest(adminToken)) + if err != nil { + t.Fatalf("admin ListForgeMirrorSettings: %v", err) + } + if !adminList.Msg.GetCanEdit() || len(adminList.Msg.GetSettings()) != 1 { + t.Fatalf("admin settings response = %+v, want one setting and can_edit=true", adminList.Msg) + } +} diff --git a/go/server/forge_mirror_settings_service.go b/go/server/forge_mirror_settings_service.go new file mode 100644 index 000000000..de16a5842 --- /dev/null +++ b/go/server/forge_mirror_settings_service.go @@ -0,0 +1,140 @@ +//go:build unix + +package server + +import ( + "context" + "errors" + "fmt" + + "connectrpc.com/connect" + compassv1 "github.com/RigelBuild/compass/go/gen/compass/v1" + "github.com/RigelBuild/compass/go/internal/auth" + "github.com/RigelBuild/compass/go/internal/store" +) + +func (s *service) ListForgeMirrorSettings( + ctx context.Context, + _ *connect.Request[compassv1.ListForgeMirrorSettingsRequest], +) (*connect.Response[compassv1.ListForgeMirrorSettingsResponse], error) { + caller, ok := auth.CallerFrom(ctx) + if !ok { + return nil, connect.NewError(connect.CodeUnauthenticated, errNoCaller) + } + account, err := s.store.GetAccount(ctx, caller) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("resolve mirror settings caller: %w", err)) + } + settings, err := s.store.ListForgeMirrorSettings(ctx) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("list forge mirror settings: %w", err)) + } + out := make([]*compassv1.ForgeMirrorSetting, 0, len(settings)) + for _, setting := range settings { + mode, err := mirrorModeToProto(setting.Mode) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } + out = append(out, &compassv1.ForgeMirrorSetting{ + Forge: &compassv1.ForgeRef{Provider: compassv1.ForgeProvider(setting.Provider), Host: setting.Host}, + Repo: setting.Repo, + Mode: mode, + }) + } + canEdit := account.User != nil && account.User.Role == store.UserRoleAdmin + return connect.NewResponse(&compassv1.ListForgeMirrorSettingsResponse{Settings: out, CanEdit: canEdit}), nil +} + +func (s *service) SetForgeMirrorSetting( + ctx context.Context, + req *connect.Request[compassv1.SetForgeMirrorSettingRequest], +) (*connect.Response[compassv1.SetForgeMirrorSettingResponse], error) { + caller, ok := auth.CallerFrom(ctx) + if !ok { + return nil, connect.NewError(connect.CodeUnauthenticated, errNoCaller) + } + account, err := s.store.GetAccount(ctx, caller) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("resolve mirror settings caller: %w", err)) + } + if account.User == nil || account.User.Role != store.UserRoleAdmin { + return nil, connect.NewError(connect.CodePermissionDenied, errors.New("setting forge mirror mode requires a tenant admin")) + } + wire := req.Msg.GetSetting() + if wire == nil || wire.GetForge() == nil { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("forge and setting are required")) + } + mode, err := mirrorModeFromProto(wire.GetMode()) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, err) + } + provider := wire.GetForge().GetProvider() + if provider < compassv1.ForgeProvider_FORGE_PROVIDER_GITHUB || provider > compassv1.ForgeProvider_FORGE_PROVIDER_LINEAR { + return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("unknown forge provider %d", provider)) + } + coordinate := (store.ForgeCoord{Provider: store.ForgeProvider(provider), Host: wire.GetForge().GetHost(), Repo: wire.GetRepo()}).Normalized() + enabled, err := s.store.ListForgeMirrorSettings(ctx) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("list enabled forge repositories: %w", err)) + } + found := false + for _, candidate := range enabled { + if candidate.Provider == coordinate.Provider && candidate.Host == coordinate.Host && candidate.Repo == coordinate.Repo { + found = true + break + } + } + if !found { + return nil, connect.NewError(connect.CodeNotFound, store.ErrNotFound) + } + setting := store.ForgeMirrorSetting{Provider: coordinate.Provider, Host: coordinate.Host, Repo: coordinate.Repo, Mode: mode} + if err := s.store.SetForgeMirrorSetting(ctx, caller, setting); err != nil { + return nil, mapForgeMirrorStoreError(err) + } + protoMode, err := mirrorModeToProto(mode) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } + return connect.NewResponse(&compassv1.SetForgeMirrorSettingResponse{Setting: &compassv1.ForgeMirrorSetting{ + Forge: &compassv1.ForgeRef{Provider: compassv1.ForgeProvider(coordinate.Provider), Host: coordinate.Host}, + Repo: coordinate.Repo, + Mode: protoMode, + }}), nil +} + +func mirrorModeFromProto(mode compassv1.ForgeMirrorMode) (store.ForgeMirrorMode, error) { + switch mode { + case compassv1.ForgeMirrorMode_FORGE_MIRROR_MODE_OFF: + return store.ForgeMirrorOff, nil + case compassv1.ForgeMirrorMode_FORGE_MIRROR_MODE_MANUAL: + return store.ForgeMirrorManual, nil + case compassv1.ForgeMirrorMode_FORGE_MIRROR_MODE_AUTO: + return store.ForgeMirrorAuto, nil + default: + return store.ForgeMirrorOff, fmt.Errorf("unknown forge mirror mode %d", mode) + } +} + +func mirrorModeToProto(mode store.ForgeMirrorMode) (compassv1.ForgeMirrorMode, error) { + switch mode { + case store.ForgeMirrorOff: + return compassv1.ForgeMirrorMode_FORGE_MIRROR_MODE_OFF, nil + case store.ForgeMirrorManual: + return compassv1.ForgeMirrorMode_FORGE_MIRROR_MODE_MANUAL, nil + case store.ForgeMirrorAuto: + return compassv1.ForgeMirrorMode_FORGE_MIRROR_MODE_AUTO, nil + default: + return compassv1.ForgeMirrorMode_FORGE_MIRROR_MODE_UNSPECIFIED, fmt.Errorf("unknown stored forge mirror mode %d", mode) + } +} + +func mapForgeMirrorStoreError(err error) error { + switch { + case errors.Is(err, store.ErrInvalidArgument): + return connect.NewError(connect.CodeInvalidArgument, err) + case errors.Is(err, store.ErrNotFound): + return connect.NewError(connect.CodeNotFound, err) + default: + return connect.NewError(connect.CodeInternal, err) + } +} diff --git a/go/server/forge_test.go b/go/server/forge_test.go index f78260489..4613bc982 100644 --- a/go/server/forge_test.go +++ b/go/server/forge_test.go @@ -11,17 +11,17 @@ import ( "context" "errors" "fmt" - "math" - "strings" - "testing" - "time" - "github.com/RigelBuild/compass/go/events" compassv1 "github.com/RigelBuild/compass/go/gen/compass/v1" "github.com/RigelBuild/compass/go/internal/board" "github.com/RigelBuild/compass/go/internal/forge" compassv1internal "github.com/RigelBuild/compass/go/internal/gen/compass/v1" + "github.com/RigelBuild/compass/go/internal/refs" "github.com/RigelBuild/compass/go/internal/store" + "math" + "strings" + "testing" + "time" ) // ownerHeaderSentinel is the one literal owner-header marker StampOwner writes; @@ -53,15 +53,19 @@ type fakeForgeStore struct { // DL-053 subscriptions: subs is keyed by subscription id; subKey indexes the // UNIQUE (agent, coordinate) to its existing id, as the store upsert does. - subs map[string]store.AgentForgeSubscription - subKey map[string]string - nextSub int - subErr error - delErr error - scopes map[string]bool - scopeErr error - scopeCalls int - scopeArgs []string + subs map[string]store.AgentForgeSubscription + subKey map[string]string + nextSub int + subErr error + delErr error + scopes map[string]bool + scopeErr error + scopeCalls int + scopeArgs []string + mirrorModes map[string]store.ForgeMirrorMode + mirrorModeErr error + prRows []store.PullRequestRow + prErr error } // recordedTransition is one RecordStateTransition the fake saw: the full @@ -80,10 +84,11 @@ type recordedTransition struct { func newFakeForgeStore() *fakeForgeStore { return &fakeForgeStore{ - accounts: make(map[store.AccountID]store.Account), - memo: make(map[string]store.AuthoredArtifact), - subs: make(map[string]store.AgentForgeSubscription), - subKey: make(map[string]string), + accounts: make(map[store.AccountID]store.Account), + memo: make(map[string]store.AuthoredArtifact), + subs: make(map[string]store.AgentForgeSubscription), + subKey: make(map[string]string), + mirrorModes: make(map[string]store.ForgeMirrorMode), } } @@ -143,6 +148,13 @@ func (f *fakeForgeStore) HasForgeScope(_ context.Context, accountID store.Accoun return f.scopes[key] || f.scopes[fmt.Sprintf("%s|%d|%s|*", accountID, provider, host)], nil } +func (f *fakeForgeStore) ForgeMirrorMode(_ context.Context, provider store.ForgeProvider, host, repo string) (store.ForgeMirrorMode, error) { + if f.mirrorModeErr != nil { + return store.ForgeMirrorOff, f.mirrorModeErr + } + return f.mirrorModes[fmt.Sprintf("%d|%s|%s", provider, host, repo)], nil +} + func (f *fakeForgeStore) GetAccount(_ context.Context, id store.AccountID) (store.Account, error) { if f.getErr != nil { return store.Account{}, f.getErr @@ -203,11 +215,8 @@ func (f *fakeForgeStore) RecordStateTransition(_ context.Context, provider store return nil } -// seedAgent registers an agent account and its owning user so resolveIdentity -// finds both handles. -func (f *fakeForgeStore) seedAgent(agentID store.AccountID, agentHandle string, ownerID store.AccountID, ownerHandle string) { - f.accounts[agentID] = store.Account{ID: agentID, Handle: agentHandle, Agent: &store.AgentAccount{OwnerUserID: ownerID}} - f.accounts[ownerID] = store.Account{ID: ownerID, Handle: ownerHandle, User: &store.UserAccount{}} +func (f *fakeForgeStore) PullRequestsByCoords(_ context.Context, _ []store.ForgeCoord) ([]store.PullRequestRow, error) { + return f.prRows, f.prErr } // --- harness ---------------------------------------------------------------- @@ -1496,3 +1505,270 @@ func TestForgeCreatePullRequestMemoHitWritesNothing(t *testing.T) { t.Fatalf("provider calls=%d PR creates=%d, want 1 and 1", len(author.Calls()), len(st.prCreates)) } } + +func TestForgeMirrorAllowedModes(t *testing.T) { + ref := refs.Ref{Kind: refs.KindForge, Owner: "owner", Repo: "repo", Number: 12} + tests := []struct { + name string + mode store.ForgeMirrorMode + need store.ForgeMirrorMode + code string + }{ + {name: "off refuses manual", mode: store.ForgeMirrorOff, need: store.ForgeMirrorManual, code: "failed_precondition"}, + {name: "off refuses auto", mode: store.ForgeMirrorOff, need: store.ForgeMirrorAuto, code: "failed_precondition"}, + {name: "manual admits manual", mode: store.ForgeMirrorManual, need: store.ForgeMirrorManual}, + {name: "manual refuses auto", mode: store.ForgeMirrorManual, need: store.ForgeMirrorAuto, code: "failed_precondition"}, + {name: "auto admits manual", mode: store.ForgeMirrorAuto, need: store.ForgeMirrorManual}, + {name: "auto admits auto", mode: store.ForgeMirrorAuto, need: store.ForgeMirrorAuto}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + author := forge.NewFakeProvider("gh-author") + svc, st := newForgeServiceForTest(t, author, forge.NewFakeProvider("gh-reviewer")) + st.mirrorModes[fmt.Sprintf("%d|%s|%s", store.ForgeProviderGitHub, testHost, testRepo)] = tc.mode + _, _, _, fe := svc.mirrorAllowed(context.Background(), st.accounts[testOwnerID], tc.need, ref) + if tc.code == "" { + if fe != nil { + t.Fatalf("mirrorAllowed error = %v, want allowed", fe) + } + return + } + if fe == nil || fe.GetCode() != tc.code || !strings.Contains(fe.GetMessage(), testRepo) { + t.Fatalf("mirrorAllowed error = %v, want %s naming %s", fe, tc.code, testRepo) + } + }) + } + + t.Run("agent scope refusal", func(t *testing.T) { + author := forge.NewFakeProvider("gh-author") + svc, st := newForgeServiceForTest(t, author, forge.NewFakeProvider("gh-reviewer")) + svc.enforceScopes = true + st.mirrorModes[fmt.Sprintf("%d|%s|%s", store.ForgeProviderGitHub, testHost, testRepo)] = store.ForgeMirrorAuto + _, _, _, fe := svc.mirrorAllowed(context.Background(), st.accounts[testAgentID], store.ForgeMirrorAuto, ref) + if fe == nil || fe.GetCode() != "not_found" || len(author.Calls()) != 0 || st.scopeCalls != 1 { + t.Fatalf("mirrorAllowed = %v, provider calls=%d, scope calls=%d; want not_found, 0, 1", fe, len(author.Calls()), st.scopeCalls) + } + }) +} + +func TestResolveMirrorCoordinateLinear(t *testing.T) { + linear := forge.NewFakeProvider("linear") + svc, _ := newForgeServiceForTest(t, forge.NewFakeProvider("gh-author"), forge.NewFakeProvider("gh-reviewer")) + reg := newForgeProviderRegistry() + if err := reg.register(forgeCoordinate{provider: compassv1.ForgeProvider_FORGE_PROVIDER_LINEAR, host: "linear.app"}, linear, linear, false); err != nil { + t.Fatal(err) + } + svc.providers = reg + rf, repo, number, fe := svc.resolveMirrorCoordinate(refs.Ref{Kind: refs.KindTracker, Key: "RIG", Number: 12}) + if fe != nil || rf.provider != compassv1.ForgeProvider_FORGE_PROVIDER_LINEAR || rf.host != "linear.app" || repo != "RIG" || number != 12 { + t.Fatalf("Linear coordinate = (%+v, %q, %d, %v)", rf, repo, number, fe) + } +} + +func TestForgeMirrorPostUsesManualBodyAndLimit(t *testing.T) { + ctx := context.Background() + ref := refs.Ref{Kind: refs.KindForge, Owner: "owner", Repo: "repo", Number: 12} + author := forge.NewFakeProvider("gh-author") + author.BodyLimitResult = 1024 + author.CommentResult = forge.Comment{ID: 1, Key: "c-1", URL: "https://github.com/owner/repo/issues/12#issuecomment-1"} + svc, st := newForgeServiceForTest(t, author, forge.NewFakeProvider("gh-reviewer")) + st.accounts[testOwnerID] = store.Account{ID: testOwnerID, Handle: testOwnerHandle, User: &store.UserAccount{}} + st.mirrorModes[fmt.Sprintf("%d|%s|%s", store.ForgeProviderGitHub, testHost, testRepo)] = store.ForgeMirrorManual + + comment, err := svc.PostMirror(ctx, testOwnerID, store.ForgeMirrorManual, ref, "", "client-1", "source body") + if err != nil { + t.Fatalf("PostMirror: %v", err) + } + if comment.URL != author.CommentResult.URL { + t.Fatalf("PostMirror comment URL = %q, want %q", comment.URL, author.CommentResult.URL) + } + calls := author.Calls() + if len(calls) != 1 || calls[0].Method != "CommentOnIssue" || calls[0].ClientID != "client-1" { + t.Fatalf("provider calls = %+v, want one idempotent issue comment", calls) + } + clean, _, ok := forge.StripOwner(calls[0].Body) + if ok || !strings.Contains(clean, "source body") || strings.Contains(calls[0].Body, "Mirrored by @") { + t.Fatalf("manual body = %q, want no owner claim and only source text", calls[0].Body) + } + if len(calls[0].Body) > author.BodyLimit() { + t.Fatalf("manual body has %d bytes, exceeds provider limit %d", len(calls[0].Body), author.BodyLimit()) + } + if got := svc.ProviderBodyLimit(ref); got != author.BodyLimit() { + t.Fatalf("ProviderBodyLimit = %d, want provider limit %d", got, author.BodyLimit()) + } +} + +func TestForgeMirrorUserAttribution(t *testing.T) { + ctx := context.Background() + ref := refs.Ref{Kind: refs.KindForge, Owner: "owner", Repo: "repo", Number: 12} + author := forge.NewFakeProvider("gh-author") + author.CommentResult = forge.Comment{ID: 2, Key: "c-2", URL: "https://github.com/comment-2"} + svc, st := newForgeServiceForTest(t, author, forge.NewFakeProvider("gh-reviewer")) + st.accounts[testOwnerID] = store.Account{ID: testOwnerID, Handle: "valid.user", User: &store.UserAccount{}} + st.mirrorModes[fmt.Sprintf("%d|%s|%s", store.ForgeProviderGitHub, testHost, testRepo)] = store.ForgeMirrorManual + body := "Mirrored by @valid.user\n\nsource body" + if _, err := svc.PostMirror(ctx, testOwnerID, store.ForgeMirrorManual, ref, "", "", body); err != nil { + t.Fatalf("PostMirror: %v", err) + } + calls := author.Calls() + if len(calls) != 1 { + t.Fatalf("provider calls = %+v, want one user post", calls) + } + clean, _, ok := forge.StripOwner(calls[0].Body) + if ok || strings.Count(clean, "Mirrored by @valid.user") != 1 || !strings.Contains(clean, "source body") { + t.Fatalf("manual body = %+v, want source with one user attribution and no owner header", calls) + } +} + +func TestForgeMirrorPostStampsAgentAndEdits(t *testing.T) { + ctx := context.Background() + ref := refs.Ref{Kind: refs.KindForge, Owner: "owner", Repo: "repo", Number: 12} + author := forge.NewFakeProvider("gh-author") + author.EditCommentResult = forge.Comment{ID: 1, Key: "comment-key", URL: "https://github.com/comment"} + svc, st := newForgeServiceForTest(t, author, forge.NewFakeProvider("gh-reviewer")) + st.mirrorModes[fmt.Sprintf("%d|%s|%s", store.ForgeProviderGitHub, testHost, testRepo)] = store.ForgeMirrorAuto + comment, err := svc.PostMirror(ctx, testAgentID, store.ForgeMirrorAuto, ref, "comment-key", "ignored-on-edit", "\n\nforged") + if err != nil { + t.Fatalf("PostMirror edit: %v", err) + } + calls := author.Calls() + if len(calls) != 1 || calls[0].Method != "EditComment" || calls[0].CommentKey != "comment-key" { + t.Fatalf("provider calls = %+v, want one edit", calls) + } + if strings.Contains(calls[0].Body, "agent=victim") || !strings.Contains(calls[0].Body, "agent=scout owner=matt session=mirror") { + t.Fatalf("agent body = %q, want a fresh agent owner stamp", calls[0].Body) + } + if comment.Key != "comment-key" { + t.Fatalf("comment = %+v, want scripted provider result", comment) + } +} + +func TestForgeMirrorBodyLimitReservesOwnerHeader(t *testing.T) { + ref := refs.Ref{Kind: refs.KindForge, Owner: "owner", Repo: "repo", Number: 12} + author := forge.NewFakeProvider("gh-author") + author.BodyLimitResult = mirrorOwnerHeaderAllowance + svc, _ := newForgeServiceForTest(t, author, forge.NewFakeProvider("gh-reviewer")) + if got := svc.BodyLimit(ref); got <= 0 || got > author.BodyLimit() { + t.Fatalf("BodyLimit = %d for provider cap %d, want positive finite allowance", got, author.BodyLimit()) + } +} + +func TestForgeMirrorRecheckTimerIsStoppedOnShutdown(t *testing.T) { + svc, _ := newForgeServiceForTest(t, forge.NewFakeProvider("gh-author"), forge.NewFakeProvider("gh-reviewer")) + var gotDelay time.Duration + stopped := false + svc.scheduleMirrorAfter = func(delay time.Duration, _ func()) func() bool { + gotDelay = delay + return func() bool { + stopped = true + return true + } + } + svc.SetMirrorLifetime(context.Background()) + recheckAt := time.Now().Add(time.Hour) + svc.scheduleMirrorRecheck(context.Background(), store.ForgeMirrorRow{ID: "recheck-row", RecheckAt: &recheckAt}, nil, false) + if gotDelay <= 0 || gotDelay > time.Hour { + t.Fatalf("scheduled delay = %s, want a positive delay up to one hour", gotDelay) + } + svc.StopMirrorLifetime() + if !stopped { + t.Fatal("shutdown did not stop pending mirror recheck") + } +} + +func TestForgeMirrorRecheckKeepsOneEarlierPendingTimer(t *testing.T) { + svc, _ := newForgeServiceForTest(t, forge.NewFakeProvider("gh-author"), forge.NewFakeProvider("gh-reviewer")) + delays := make([]time.Duration, 0, 3) + stops := make([]bool, 0, 3) + callbacks := make([]func(), 0, 3) + svc.scheduleMirrorAfter = func(delay time.Duration, fn func()) func() bool { + delays = append(delays, delay) + callbacks = append(callbacks, fn) + stopped := false + stops = append(stops, false) + index := len(stops) - 1 + return func() bool { + stops[index] = true + stopped = true + return stopped + } + } + svc.SetMirrorLifetime(context.Background()) + t.Cleanup(svc.StopMirrorLifetime) + now := time.Now() + for _, due := range []time.Time{now.Add(time.Hour), now.Add(2 * time.Hour), now.Add(30 * time.Minute)} { + svc.scheduleMirrorRecheck(context.Background(), store.ForgeMirrorRow{ID: "coalesced", RecheckAt: &due}, nil, false) + } + if len(delays) != 2 || delays[0] < 59*time.Minute || delays[1] < 29*time.Minute || delays[1] > 30*time.Minute { + t.Fatalf("scheduled delays = %v, want original timer kept for later due time and replaced by earlier due time", delays) + } + if len(svc.mirrorTimers) != 1 || !stops[0] { + t.Fatalf("pending timers = %d, first stopped = %v; want one pending timer and replaced timer stopped", len(svc.mirrorTimers), stops[0]) + } + callbacks[0]() + if len(svc.mirrorTimers) != 1 { + t.Fatalf("pending timers after firing replaced callback = %d, want current timer retained", len(svc.mirrorTimers)) + } +} + +type mirrorIdentityProvider struct { + forge.Provider + login string + err error +} + +func (p *mirrorIdentityProvider) AppLogin(ctx context.Context) (string, error) { + return p.login, p.err +} + +func TestForgeMirrorCommentRecoveryRequiresAppIdentity(t *testing.T) { + t.Run("only app comments are returned", func(t *testing.T) { + author := forge.NewFakeProvider("gh-author") + reviewer := forge.NewFakeProvider("gh-reviewer") + identity := &mirrorIdentityProvider{Provider: author, login: "compass[bot]"} + svc, st := newForgeServiceForTest(t, author, reviewer) + svc.providers.entries[forgeCoordinate{provider: compassv1.ForgeProvider_FORGE_PROVIDER_GITHUB, host: testHost}] = forgeProviderRoles{author: identity, reviewer: reviewer} + st.mirrorModes[fmt.Sprintf("%d|%s|%s", store.ForgeProviderGitHub, testHost, testRepo)] = store.ForgeMirrorManual + author.ListCommentsResult = []forge.Comment{ + {Key: "app", ForgeAccount: "Compass[bot]"}, + {Key: "user", ForgeAccount: "alice"}, + } + + got, err := svc.ListMirrorComments(context.Background(), testOwnerID, + refs.Ref{Kind: refs.KindForge, Owner: "owner", Repo: "repo", Number: 12}, time.Time{}) + if err != nil { + t.Fatalf("ListMirrorComments: %v", err) + } + if len(got) != 1 || got[0].Key != "app" { + t.Fatalf("recovery comments = %+v, want only Compass App comment", got) + } + calls := author.Calls() + if len(calls) != 1 || calls[0].Method != "ListCommentsSince" { + t.Fatalf("provider calls = %+v, want one comment listing", calls) + } + }) + + t.Run("identity lookup failure prevents listing", func(t *testing.T) { + author := forge.NewFakeProvider("gh-author") + reviewer := forge.NewFakeProvider("gh-reviewer") + identity := &mirrorIdentityProvider{Provider: author, err: errors.New("identity unavailable")} + svc, st := newForgeServiceForTest(t, author, reviewer) + svc.providers.entries[forgeCoordinate{provider: compassv1.ForgeProvider_FORGE_PROVIDER_GITHUB, host: testHost}] = forgeProviderRoles{author: identity, reviewer: reviewer} + st.mirrorModes[fmt.Sprintf("%d|%s|%s", store.ForgeProviderGitHub, testHost, testRepo)] = store.ForgeMirrorManual + + if _, err := svc.ListMirrorComments(context.Background(), testOwnerID, + refs.Ref{Kind: refs.KindForge, Owner: "owner", Repo: "repo", Number: 12}, time.Time{}); err == nil { + t.Fatal("ListMirrorComments error = nil, want identity lookup failure") + } + if calls := author.Calls(); len(calls) != 0 { + t.Fatalf("provider calls = %+v, want no comment listing without a verified App identity", calls) + } + }) +} + +// seedAgent registers an agent account and its owning user so resolveIdentity +// finds both handles. +func (f *fakeForgeStore) seedAgent(agentID store.AccountID, agentHandle string, ownerID store.AccountID, ownerHandle string) { + f.accounts[agentID] = store.Account{ID: agentID, Handle: agentHandle, Agent: &store.AgentAccount{OwnerUserID: ownerID}} + f.accounts[ownerID] = store.Account{ID: ownerID, Handle: ownerHandle, User: &store.UserAccount{}} +} diff --git a/go/server/serve.go b/go/server/serve.go index 79d017037..4e2afccc5 100644 --- a/go/server/serve.go +++ b/go/server/serve.go @@ -880,6 +880,7 @@ func Serve(ctx context.Context, cfg ServeConfig) error { commsBus := events.NewBus[*compassv1.SubscribeCommsResponse]() defer commsBus.Close() commsSvc := comms.NewComms(st, commsBus, fab, admin.ID) + commsSvc.SetForgeHost(cfg.Forge.resolved().Host) // Register the coordination-channel reconcile as the store's in-tx hook, so // the two parent-edge writers auto-provision/reconcile a manager's // coordination channel atomically with the tree edge (RIG-1722 T5). Wired here @@ -947,15 +948,19 @@ func Serve(ctx context.Context, cfg ServeConfig) error { // configured (the 2-App cutover). The author leg REUSES the shared primary App // client so it rides the one budget gate; the reviewer leg gets its own; the // Linear coordinate rides the shared forgeWiring.linearTokens instance. - if err := wireForgeWriteCaller(ctx, cfg, st, issueBrd, serverResolver, hub, hubLog, forgeWiring.primaryClient, forgeWiring.linearTokens, udsListener, listeners); err != nil { + forgeMirrorService, err := wireForgeWriteCaller(ctx, cfg, st, issueBrd, serverResolver, hub, commsSvc, hubLog, forgeWiring.primaryClient, forgeWiring.linearTokens, udsListener, listeners) + if err != nil { return err } - // Run every door under one scoped group: scoped lifecycle, first-error-wins, // and sibling cancellation. gctx cancels on parent shutdown or a door's own // error and always ends by Wait, so the verifier's key refresh never outlives // Serve. The UDS door is primary; its error wins over the drain result below. g, gctx := newServeGroup(ctx, doors.runnerVerifier) + if forgeMirrorService != nil { + forgeMirrorService.SetMirrorLifetime(gctx) + defer forgeMirrorService.StopMirrorLifetime() + } startServeDoors(g, gctx, doors, udsListener, devListener, netListener, mergeQueueRunner) // The forge webhook-ingestion lanes (RIG-2883 board + RIG-2732 T7 notify): // each lane joins the SAME scoped group so it inherits the doors' lifecycle. @@ -970,23 +975,16 @@ func Serve(ctx context.Context, cfg ServeConfig) error { startCommsConsumers(gctx, g, commsBus, fab, st, hub, hubLog) // Background usage upkeep: the daily raw-log prune and the hourly orphan-interval close. startUsageSweepers(gctx, g, st, cfg.UsageEventRetention, hubLog) - // Drain member of the same group: wake on gctx cancellation, then hand off to - // drainDoors. A drain that overruns (a handler still wedged mid-replay) - // surfaces as the error rather than a false clean shutdown; a real serve - // error still wins because the group keeps the first error. - g.Go(func() error { - <-gctx.Done() - return drainDoors(gctx, drainSet{ - bus: bus, - commsBus: commsBus, - uds: doors.uds, - dev: doors.dev, - net: doors.net, - hub: hub, - log: hubLog, - cancelHooks: cancelSeed, - health: doors.health, - }) + startServeDrain(g, gctx, drainSet{ + bus: bus, + commsBus: commsBus, + uds: doors.uds, + dev: doors.dev, + net: doors.net, + hub: hub, + log: hubLog, + cancelHooks: cancelSeed, + health: doors.health, }) return g.Wait() @@ -1005,6 +1003,17 @@ func startServeDoors(g *errgroup.Group, ctx context.Context, doors serveDoors, u mergeQueue.start(g, ctx) } +// startServeDrain adds the drain member of the same group: wake on gctx cancellation, then hand off to +// drainDoors. A drain that overruns (a handler still wedged mid-replay) +// surfaces as the error rather than a false clean shutdown; a real serve +// error still wins because the group keeps the first error. +func startServeDrain(g *errgroup.Group, ctx context.Context, d drainSet) { + g.Go(func() error { + <-ctx.Done() + return drainDoors(ctx, d) + }) +} + // serveDoors holds the three compass.v1 doors Serve drives: the shipped Unix // socket (always built), the optional dev loopback browser door, and the // optional authenticated network door. dev and net are nil when their listener @@ -2146,34 +2155,25 @@ func validateForgeSecret(ctx context.Context, resolver secrets.Resolver, reason, // shared Linear token source the Linear coordinate rides (nil when Linear is not // configured). On any startup fault it unwinds the caller-bound listeners // (udsListener + listeners) before returning, the same teardown path the poll -// driver and Rehydrate faults use. -func wireForgeWriteCaller( - ctx context.Context, - cfg ServeConfig, - st *store.Store, - issueBrd *board.IssueProjection, - resolver secrets.Resolver, - hub *runnerhub.Hub, - log *slog.Logger, - primaryClient *forge.GitHub, - linearTokens *linearagent.TokenSource, - udsListener net.Listener, - listeners boundListeners, -) error { +// driver and Rehydrate faults use. It returns the wired service so Serve can +// bound mirror drives to its lifetime; nil means writes are off. +func wireForgeWriteCaller(ctx context.Context, cfg ServeConfig, st *store.Store, issueBrd *board.IssueProjection, resolver secrets.Resolver, hub *runnerhub.Hub, commsSvc *comms.Comms, log *slog.Logger, primaryClient *forge.GitHub, linearTokens *linearagent.TokenSource, udsListener net.Listener, listeners boundListeners) (*forgeService, error) { declaredSecrets, err := resolver.Resolve(ctx, "forge write") if err != nil { - return failStartup(udsListener, listeners, fmt.Errorf("forge secret resolve failed at startup: %w", err)) + return nil, failStartup(udsListener, listeners, fmt.Errorf("forge secret resolve failed at startup: %w", err)) } if !cfg.Forge.forgeWritesEnabled(declaredSecrets) { warnPartialForgeWriteSecrets(cfg.Forge, declaredSecrets, log) - return nil + return nil, nil //nolint:nilnil // nil service is the explicit disabled state. } forgeSvc, err := buildForgeWriteService(ctx, cfg, st, issueBrd, resolver, primaryClient, linearTokens, log) if err != nil { - return failStartup(udsListener, listeners, err) + return nil, failStartup(udsListener, listeners, err) } + forgeSvc.publicURL = cfg.PublicURL hub.SetForgeCaller(forgeSvc) - return nil + commsSvc.SetMirrorer(forgeSvc, forgeSvc) + return forgeSvc, nil } // buildForgeWriteService assembles the agent forge-WRITE chokepoint (the diff --git a/packages/compass-agent/src/gen/compass/v1/comms_pb.ts b/packages/compass-agent/src/gen/compass/v1/comms_pb.ts index 18fe2d057..3b37acd23 100644 --- a/packages/compass-agent/src/gen/compass/v1/comms_pb.ts +++ b/packages/compass-agent/src/gen/compass/v1/comms_pb.ts @@ -29,7 +29,7 @@ import type { Message as Message$1 } from "@bufbuild/protobuf"; * Describes the file compass/v1/comms.proto. */ export const file_compass_v1_comms: GenFile = /*@__PURE__*/ - fileDesc("ChZjb21wYXNzL3YxL2NvbW1zLnByb3RvEgpjb21wYXNzLnYxIsQBCgdBY2NvdW50EgoKAmlkGAEgASgJEg4KBmhhbmRsZRgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSJwoEdXNlchgKIAEoCzIXLmNvbXBhc3MudjEuVXNlckFjY291bnRIABIpCgVhZ2VudBgLIAEoCzIYLmNvbXBhc3MudjEuQWdlbnRBY2NvdW50SAASKwoGc3lzdGVtGAwgASgLMhkuY29tcGFzcy52MS5TeXN0ZW1BY2NvdW50SABCBgoEa2luZCIPCg1TeXN0ZW1BY2NvdW50IjEKC1VzZXJBY2NvdW50EiIKBHJvbGUYASABKA4yFC5jb21wYXNzLnYxLlVzZXJSb2xlIlcKDEFnZW50QWNjb3VudBIVCg1vd25lcl91c2VyX2lkGAEgASgJEhcKD2hvbWVfY2hhbm5lbF9pZBgCIAEoCRIXCg9wYXJlbnRfYWdlbnRfaWQYAyABKAkikAEKDENoYW5uZWxHcm91cBIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEhcKD3BhcmVudF9ncm91cF9pZBgDIAEoCRIVCg1vd25lcl91c2VyX2lkGAQgASgJEjYKCnZpc2liaWxpdHkYBSABKA4yIi5jb21wYXNzLnYxLkNoYW5uZWxHcm91cFZpc2liaWxpdHkijAMKB0NoYW5uZWwSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRIQCghncm91cF9pZBgDIAEoCRIlCgRraW5kGAQgASgOMhcuY29tcGFzcy52MS5DaGFubmVsS2luZBIaChJtZW1iZXJfYWNjb3VudF9pZHMYBSADKAkSHgoWc3Vic2NyaWJlcl9hY2NvdW50X2lkcxgGIAMoCRIyCgtwb3N0X3BvbGljeRgHIAEoDjIdLmNvbXBhc3MudjEuQ2hhbm5lbFBvc3RQb2xpY3kSGAoQb3duZXJfYWNjb3VudF9pZBgIIAEoCRIeChZtYW5kYXRvcnlfc3Vic2NyaXB0aW9uGAkgASgIEi8KDnBpbm5lZF9lbnRyaWVzGAogAygLMhcuY29tcGFzcy52MS5QaW5uZWRFbnRyeRIXCg9wYXJlbnRfYWdlbnRfaWQYCyABKAkSOgoPbWVtYmVyc2hpcF9tb2RlGAwgASgOMiEuY29tcGFzcy52MS5DaGFubmVsTWVtYmVyc2hpcE1vZGUibAoLUGlubmVkRW50cnkSEgoKbWVzc2FnZV9pZBgBIAEoCRIQCghwb3NpdGlvbhgCIAEoBRIZChFwaW5uZWRfYXRfdW5peF9tcxgDIAEoAxIcChRwaW5uZWRfYnlfYWNjb3VudF9pZBgEIAEoCSI2Cg5BZ2VudFdvcmtzcGFjZRIKCgJpZBgBIAEoCRIYChBhZ2VudF9hY2NvdW50X2lkGAIgASgJIvIBCgVUb3BpYxIKCgJpZBgBIAEoCRISCgpjaGFubmVsX2lkGAIgASgJEgwKBG5hbWUYAyABKAkSGgoSY3JlYXRlZF9hdF91bml4X21zGAQgASgDEh0KFWNyZWF0ZWRfYnlfYWNjb3VudF9pZBgFIAEoCRIQCghhcmNoaXZlZBgGIAEoCBIaCg1tZXNzYWdlX2NvdW50GAcgASgESACIAQESJAoXbGFzdF9tZXNzYWdlX2F0X3VuaXhfbXMYCCABKANIAYgBAUIQCg5fbWVzc2FnZV9jb3VudEIaChhfbGFzdF9tZXNzYWdlX2F0X3VuaXhfbXMiwgEKB01lc3NhZ2USCgoCaWQYASABKAkSEAoIdG9waWNfaWQYAiABKAkSGQoRYXV0aG9yX2FjY291bnRfaWQYAyABKAkSEgoKYXRfdW5peF9tcxgEIAEoAxIoCgZibG9ja3MYBSADKAsyGC5jb21wYXNzLnYxLk1lc3NhZ2VCbG9jaxIVCg1hdXRob3JfaGFuZGxlGAYgASgJEhIKCmNoYW5uZWxfaWQYByABKAkSFQoNdHVybl9zZXF1ZW5jZRgIIAEoBCJ5CgxNZXNzYWdlQmxvY2sSDgoEdGV4dBgBIAEoCUgAEh4KA2FzaxgCIAEoCzIPLmNvbXBhc3MudjEuQXNrSAASMAoKYXNrX2Fuc3dlchgDIAEoCzIaLmNvbXBhc3MudjEuQXNrQW5zd2VyQmxvY2tIAEIHCgVibG9jayJICg5Bc2tBbnN3ZXJCbG9jaxIcCgNhc2sYASABKAsyDy5jb21wYXNzLnYxLkFzaxIYChBhc2tlcl9hY2NvdW50X2lkGAIgASgJImAKA0FzaxIOCgZhc2tfaWQYASABKAkSKgoJcXVlc3Rpb25zGAIgAygLMhcuY29tcGFzcy52MS5Bc2tRdWVzdGlvbhIQCghhbnN3ZXJlZBgDIAEoCBILCgNyZWYYBCABKAki8QEKC0Fza1F1ZXN0aW9uEhMKC3F1ZXN0aW9uX2lkGAEgASgJEhAKCHF1ZXN0aW9uGAIgASgJEg4KBmhlYWRlchgDIAEoCRImCgdvcHRpb25zGAQgAygLMhUuY29tcGFzcy52MS5Bc2tPcHRpb24SFgoOYWxsb3dfbXVsdGlwbGUYBSABKAgSGAoLcmVjb21tZW5kZWQYBiABKAVIAIgBARIZChFjaG9zZW5fb3B0aW9uX2lkcxgHIAMoCRITCgtjdXN0b21fdGV4dBgIIAEoCRIRCgl0aW1lZF9vdXQYCSABKAhCDgoMX3JlY29tbWVuZGVkIkwKCUFza09wdGlvbhIKCgJpZBgBIAEoCRINCgVsYWJlbBgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIPCgdwcmV2aWV3GAQgASgJIscFChZTdWJzY3JpYmVDb21tc1Jlc3BvbnNlEgsKA3NlcRgBIAEoBBISCgphdF91bml4X21zGAIgASgDEhYKDmluc3RhbmNlX2Vwb2NoGAMgASgEEhQKDHNuYXBzaG90X3NlcRgEIAEoBBIzCg5tZXNzYWdlX3Bvc3RlZBgKIAEoCzIZLmNvbXBhc3MudjEuTWVzc2FnZVBvc3RlZEgAEjUKD21lc3NhZ2VfdXBkYXRlZBgLIAEoCzIaLmNvbXBhc3MudjEuTWVzc2FnZVVwZGF0ZWRIABI1Cg9jaGFubmVsX2NoYW5nZWQYDCABKAsyGi5jb21wYXNzLnYxLkNoYW5uZWxDaGFuZ2VkSAASQAoVY2hhbm5lbF9ncm91cF9jaGFuZ2VkGA0gASgLMh8uY29tcGFzcy52MS5DaGFubmVsR3JvdXBDaGFuZ2VkSAASNQoPYWNjb3VudF9jaGFuZ2VkGA4gASgLMhouY29tcGFzcy52MS5BY2NvdW50Q2hhbmdlZEgAEkQKF2FnZW50X3dvcmtzcGFjZV9jaGFuZ2VkGA8gASgLMiEuY29tcGFzcy52MS5BZ2VudFdvcmtzcGFjZUNoYW5nZWRIABI6Cg9yZXN5bmNfcmVxdWlyZWQYECABKAsyHy5jb21wYXNzLnYxLkNvbW1zUmVzeW5jUmVxdWlyZWRIABJCChZhZ2VudF9wcmVzZW5jZV9jaGFuZ2VkGBEgASgLMiAuY29tcGFzcy52MS5BZ2VudFByZXNlbmNlQ2hhbmdlZEgAEjMKDnRvcGljX3Vwc2VydGVkGBIgASgLMhkuY29tcGFzcy52MS5Ub3BpY1Vwc2VydGVkSAASPAoTYWdlbnRfYm9hcmRfY2hhbmdlZBgTIAEoCzIdLmNvbXBhc3MudjEuQWdlbnRCb2FyZENoYW5nZWRIAEIJCgdwYXlsb2FkIkwKDU1lc3NhZ2VQb3N0ZWQSJAoHbWVzc2FnZRgBIAEoCzITLmNvbXBhc3MudjEuTWVzc2FnZRIVCg10dXJuX3NlcXVlbmNlGAIgASgEIjYKDk1lc3NhZ2VVcGRhdGVkEiQKB21lc3NhZ2UYASABKAsyEy5jb21wYXNzLnYxLk1lc3NhZ2UiMQoNVG9waWNVcHNlcnRlZBIgCgV0b3BpYxgBIAEoCzIRLmNvbXBhc3MudjEuVG9waWMiUwoOQ2hhbm5lbENoYW5nZWQSJAoHY2hhbm5lbBgBIAEoCzITLmNvbXBhc3MudjEuQ2hhbm5lbBIbChNyZW1vdmVkX2FjY291bnRfaWRzGAIgAygJIj4KE0NoYW5uZWxHcm91cENoYW5nZWQSJwoFZ3JvdXAYASABKAsyGC5jb21wYXNzLnYxLkNoYW5uZWxHcm91cCI2Cg5BY2NvdW50Q2hhbmdlZBIkCgdhY2NvdW50GAEgASgLMhMuY29tcGFzcy52MS5BY2NvdW50IkYKFUFnZW50V29ya3NwYWNlQ2hhbmdlZBItCgl3b3Jrc3BhY2UYASABKAsyGi5jb21wYXNzLnYxLkFnZW50V29ya3NwYWNlIhUKE0NvbW1zUmVzeW5jUmVxdWlyZWQibwoUQWdlbnRQcmVzZW5jZUNoYW5nZWQSGAoQYWdlbnRfYWNjb3VudF9pZBgBIAEoCRIrCghwcmVzZW5jZRgCIAEoDjIZLmNvbXBhc3MudjEuQWdlbnRQcmVzZW5jZRIQCghhY3Rpdml0eRgDIAEoCSJ3CgxCb2FyZFdhaXRpbmcSJgoCb24YASABKA4yGi5jb21wYXNzLnYxLkJvYXJkV2FpdGluZ09uEhQKDGFnZW50X2hhbmRsZRgCIAEoCRIMCgR3aGF0GAMgASgJEgsKA3JlZhgEIAEoCRIOCgZibG9ja3MYBSADKAkihQEKDUJvYXJkU3ViYWdlbnQSCgoCaWQYASABKAkSDQoFYWdlbnQYAiABKAkSEwoLZGVzY3JpcHRpb24YAyABKAkSDgoGc3RhdHVzGAQgASgJEhoKEnN0YXJ0ZWRfYXRfdW5peF9tcxgFIAEoAxIYChBlbmRlZF9hdF91bml4X21zGAYgASgDIvgBCglCb2FyZFRhc2sSCwoDa2V5GAEgASgJEg0KBXRpdGxlGAIgASgJEhMKC3ByaW1hcnlfcmVmGAMgASgJEgwKBHJlZnMYBCADKAkSKQoFc3RhdGUYBSABKA4yGi5jb21wYXNzLnYxLkJvYXJkVGFza1N0YXRlEgwKBG5vdGUYBiABKAkSKQoHd2FpdGluZxgHIAMoCzIYLmNvbXBhc3MudjEuQm9hcmRXYWl0aW5nEiwKCXN1YmFnZW50cxgIIAMoCzIZLmNvbXBhc3MudjEuQm9hcmRTdWJhZ2VudBIaChJ3cml0dGVuX2F0X3VuaXhfbXMYCSABKAMiHQoLQm9hcmRSZWZzT3ASDgoGdmFsdWVzGAEgAygJIjoKDkJvYXJkV2FpdGluZ09wEigKBnZhbHVlcxgBIAMoCzIYLmNvbXBhc3MudjEuQm9hcmRXYWl0aW5nIsQCCg5Cb2FyZFRhc2tQYXRjaBILCgNrZXkYASABKAkSEgoFdGl0bGUYAiABKAlIAIgBARIYCgtwcmltYXJ5X3JlZhgDIAEoCUgBiAEBEi4KBXN0YXRlGAQgASgOMhouY29tcGFzcy52MS5Cb2FyZFRhc2tTdGF0ZUgCiAEBEhEKBG5vdGUYBSABKAlIA4gBARIlCgRyZWZzGAYgASgLMhcuY29tcGFzcy52MS5Cb2FyZFJlZnNPcBIrCgd3YWl0aW5nGAcgASgLMhouY29tcGFzcy52MS5Cb2FyZFdhaXRpbmdPcBIzChB1cHNlcnRfc3ViYWdlbnRzGAggAygLMhkuY29tcGFzcy52MS5Cb2FyZFN1YmFnZW50QggKBl90aXRsZUIOCgxfcHJpbWFyeV9yZWZCCAoGX3N0YXRlQgcKBV9ub3RlIlEKCUJvYXJkSGFsdBInCgRraW5kGAEgASgOMhkuY29tcGFzcy52MS5Cb2FyZEhhbHRLaW5kEgsKA3JlZhgCIAEoCRIOCgZyZWFzb24YAyABKAkijwMKCEJvYXJkUmVmEgsKA3JlZhgBIAEoCRImCgRraW5kGAIgASgOMhguY29tcGFzcy52MS5Cb2FyZFJlZktpbmQSDQoFdGl0bGUYAyABKAkSCwoDdXJsGAQgASgJEhMKC2ZvcmdlX3N0YXRlGAUgASgJEisKC2lzc3VlX3N0YXRlGAYgASgOMhYuY29tcGFzcy52MS5Jc3N1ZVN0YXRlEg0KBWRyYWZ0GAcgASgIEhQKDGNoZWNrc19zdGF0ZRgIIAEoCRIlCgRnYXRlGAkgASgOMhcuY29tcGFzcy52MS5Cb2FyZFByR2F0ZRIVCg1zdGFja190aXBfcmVmGAogASgJEhYKDnN0YWNrX3Bvc2l0aW9uGAsgASgNEg0KBW93bmVkGAwgASgIEhsKE29ic2VydmVkX2F0X3VuaXhfbXMYDSABKAMSGQoRbWVyZ2VfcXVldWVfc3RhdGUYDiABKAkSFgoOc3RhbGVfYXBwcm92YWwYDyABKAgSFgoOdHJhY2tlcl9zdGF0dXMYECABKAkijAEKCEJvYXJkQXNrEg4KBmFza19pZBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJEhIKCmNoYW5uZWxfaWQYAyABKAkSCwoDcmVmGAQgASgJEg4KBmhlYWRlchgFIAEoCRIQCghhbnN3ZXJlZBgGIAEoCBIZChFwb3N0ZWRfYXRfdW5peF9tcxgHIAEoAyLVAgoKQWdlbnRCb2FyZBIYChBhZ2VudF9hY2NvdW50X2lkGAEgASgJEg4KBmhhbmRsZRgCIAEoCRIWCg5mb2N1c190YXNrX2tleRgDIAEoCRIjCgRoYWx0GAQgASgLMhUuY29tcGFzcy52MS5Cb2FyZEhhbHQSJAoFdGFza3MYBSADKAsyFS5jb21wYXNzLnYxLkJvYXJkVGFzaxIiCgRyZWZzGAYgAygLMhQuY29tcGFzcy52MS5Cb2FyZFJlZhIWCg51bnRyYWNrZWRfcmVmcxgHIAMoCRIiCgRhc2tzGAggAygLMhQuY29tcGFzcy52MS5Cb2FyZEFzaxIUCgx3cml0dGVuX3R1cm4YCSABKAQSGgoSd3JpdHRlbl9hdF91bml4X21zGAogASgDEg0KBXN0YWxlGAsgASgIEhkKEW1hdGNoZWRfdGFza19rZXlzGAwgAygJIqMBChFBZ2VudEJvYXJkU3VtbWFyeRINCgVmb2N1cxgBIAEoCRISCgp0YXNrX2NvdW50GAIgASgNEhsKE3dhaXRpbmdfb25fb3BlcmF0b3IYAyABKA0SIwoEaGFsdBgEIAEoCzIVLmNvbXBhc3MudjEuQm9hcmRIYWx0EhoKEndyaXR0ZW5fYXRfdW5peF9tcxgFIAEoAxINCgVzdGFsZRgGIAEoCCL4AQoXVXBkYXRlQWdlbnRCb2FyZFJlcXVlc3QSMAoMdXBzZXJ0X3Rhc2tzGAEgAygLMhouY29tcGFzcy52MS5Cb2FyZFRhc2tQYXRjaBIYChByZW1vdmVfdGFza19rZXlzGAIgAygJEhsKDmZvY3VzX3Rhc2tfa2V5GAMgASgJSACIAQESIwoEaGFsdBgEIAEoCzIVLmNvbXBhc3MudjEuQm9hcmRIYWx0EhIKCmNsZWFyX2hhbHQYBSABKAgSFQoNdHVybl9zZXF1ZW5jZRgGIAEoBBIRCglhdXRvbWF0aWMYByABKAhCEQoPX2ZvY3VzX3Rhc2tfa2V5IkEKGFVwZGF0ZUFnZW50Qm9hcmRSZXNwb25zZRIlCgVib2FyZBgBIAEoCzIWLmNvbXBhc3MudjEuQWdlbnRCb2FyZCLeAQoWTGlzdEFnZW50Qm9hcmRzUmVxdWVzdBIVCg1hZ2VudF9oYW5kbGVzGAEgAygJEg0KBXF1ZXJ5GAIgASgJEgsKA3JlZhgDIAEoCRIuCgp3YWl0aW5nX29uGAQgASgOMhouY29tcGFzcy52MS5Cb2FyZFdhaXRpbmdPbhIpCgVzdGF0ZRgFIAEoDjIaLmNvbXBhc3MudjEuQm9hcmRUYXNrU3RhdGUSJwoEaGFsdBgGIAEoDjIZLmNvbXBhc3MudjEuQm9hcmRIYWx0S2luZBINCgVsaW1pdBgHIAEoDSJBChdMaXN0QWdlbnRCb2FyZHNSZXNwb25zZRImCgZib2FyZHMYASADKAsyFi5jb21wYXNzLnYxLkFnZW50Qm9hcmQiVAoRQWdlbnRCb2FyZENoYW5nZWQSGAoQYWdlbnRfYWNjb3VudF9pZBgBIAEoCRIlCgVib2FyZBgCIAEoCzIWLmNvbXBhc3MudjEuQWdlbnRCb2FyZCJbCgdQZWVyaW5nEhcKD3VzZXJfYWNjb3VudF9pZBgBIAEoCRIOCgZoYW5kbGUYAiABKAkSJwoFc3RhdGUYAyABKA4yGC5jb21wYXNzLnYxLlBlZXJpbmdTdGF0ZSIpChJBcHByb3ZlUGVlclJlcXVlc3QSEwoLcGVlcl9oYW5kbGUYASABKAkiOwoTQXBwcm92ZVBlZXJSZXNwb25zZRIkCgdwZWVyaW5nGAEgASgLMhMuY29tcGFzcy52MS5QZWVyaW5nIigKEVJldm9rZVBlZXJSZXF1ZXN0EhMKC3BlZXJfaGFuZGxlGAEgASgJIiUKElJldm9rZVBlZXJSZXNwb25zZRIPCgdkZWxldGVkGAEgASgIIhIKEExpc3RQZWVyc1JlcXVlc3QiOgoRTGlzdFBlZXJzUmVzcG9uc2USJQoIcGVlcmluZ3MYASADKAsyEy5jb21wYXNzLnYxLlBlZXJpbmciOQoRQ3JlYXRlVXNlclJlcXVlc3QSDgoGaGFuZGxlGAEgASgJEhQKDGRpc3BsYXlfbmFtZRgCIAEoCSI6ChJDcmVhdGVVc2VyUmVzcG9uc2USJAoHYWNjb3VudBgBIAEoCzITLmNvbXBhc3MudjEuQWNjb3VudCJwChJDcmVhdGVBZ2VudFJlcXVlc3QSDgoGaGFuZGxlGAEgASgJEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRIVCg1wYXJlbnRfaGFuZGxlGAMgASgJEgwKBHJvbGUYBCABKAkSDwoHcGVyc29uYRgFIAEoCSI7ChNDcmVhdGVBZ2VudFJlc3BvbnNlEiQKB2FjY291bnQYASABKAsyEy5jb21wYXNzLnYxLkFjY291bnQiKwoTTGlzdEFjY291bnRzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPQoUTGlzdEFjY291bnRzUmVzcG9uc2USJQoIYWNjb3VudHMYASADKAsyEy5jb21wYXNzLnYxLkFjY291bnQilQEKGUNyZWF0ZUNoYW5uZWxHcm91cFJlcXVlc3QSDAoEbmFtZRgBIAEoCRIXCg9wYXJlbnRfZ3JvdXBfaWQYAiABKAkSNgoKdmlzaWJpbGl0eRgDIAEoDjIiLmNvbXBhc3MudjEuQ2hhbm5lbEdyb3VwVmlzaWJpbGl0eRIZChFwYXJlbnRfZ3JvdXBfbmFtZRgEIAEoCSJFChpDcmVhdGVDaGFubmVsR3JvdXBSZXNwb25zZRInCgVncm91cBgBIAEoCzIYLmNvbXBhc3MudjEuQ2hhbm5lbEdyb3VwIjAKGExpc3RDaGFubmVsR3JvdXBzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiRQoZTGlzdENoYW5uZWxHcm91cHNSZXNwb25zZRIoCgZncm91cHMYASADKAsyGC5jb21wYXNzLnYxLkNoYW5uZWxHcm91cCIrChNMaXN0Q2hhbm5lbHNSZXF1ZXN0EhQKDHNuYXBzaG90X3NlcRgBIAEoBCI9ChRMaXN0Q2hhbm5lbHNSZXNwb25zZRIlCghjaGFubmVscxgBIAMoCzITLmNvbXBhc3MudjEuQ2hhbm5lbCLiAQoUQ3JlYXRlQ2hhbm5lbFJlcXVlc3QSDAoEbmFtZRgBIAEoCRIQCghncm91cF9pZBgCIAEoCRIlCgRraW5kGAMgASgOMhcuY29tcGFzcy52MS5DaGFubmVsS2luZBIWCg5tZW1iZXJfaGFuZGxlcxgEIAMoCRIbChNwYXJlbnRfYWdlbnRfaGFuZGxlGAUgASgJEjoKD21lbWJlcnNoaXBfbW9kZRgGIAEoDjIhLmNvbXBhc3MudjEuQ2hhbm5lbE1lbWJlcnNoaXBNb2RlEhIKCmdyb3VwX25hbWUYByABKAkiPQoVQ3JlYXRlQ2hhbm5lbFJlc3BvbnNlEiQKB2NoYW5uZWwYASABKAsyEy5jb21wYXNzLnYxLkNoYW5uZWwiwgEKG1VwZGF0ZUNoYW5uZWxNZW1iZXJzUmVxdWVzdBISCgpjaGFubmVsX2lkGAEgASgJEhoKEmFkZF9tZW1iZXJfaGFuZGxlcxgCIAMoCRIdChVyZW1vdmVfbWVtYmVyX2hhbmRsZXMYAyADKAkSGQoRc3Vic2NyaWJlX2hhbmRsZXMYBCADKAkSGwoTdW5zdWJzY3JpYmVfaGFuZGxlcxgFIAMoCRIcChRjb252ZXJ0X2NoYW5uZWxfbmFtZRgGIAEoCSJEChxVcGRhdGVDaGFubmVsTWVtYmVyc1Jlc3BvbnNlEiQKB2NoYW5uZWwYASABKAsyEy5jb21wYXNzLnYxLkNoYW5uZWwiJAoNT3BlbkRNUmVxdWVzdBITCgtwZWVyX2hhbmRsZRgBIAEoCSJHCg5PcGVuRE1SZXNwb25zZRIkCgdjaGFubmVsGAEgASgLMhMuY29tcGFzcy52MS5DaGFubmVsEg8KB2NyZWF0ZWQYAiABKAgiRwoUUmVwYXJlbnRBZ2VudFJlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJEhkKEW5ld19wYXJlbnRfaGFuZGxlGAIgASgJIj0KFVJlcGFyZW50QWdlbnRSZXNwb25zZRIkCgdhY2NvdW50GAEgASgLMhMuY29tcGFzcy52MS5BY2NvdW50Ik0KFlJlcGFyZW50Q2hhbm5lbFJlcXVlc3QSEgoKY2hhbm5lbF9pZBgBIAEoCRIfChduZXdfcGFyZW50X2FnZW50X2hhbmRsZRgCIAEoCSI/ChdSZXBhcmVudENoYW5uZWxSZXNwb25zZRIkCgdjaGFubmVsGAEgASgLMhMuY29tcGFzcy52MS5DaGFubmVsIpcBChdTZXRDaGFubmVsUG9saWN5UmVxdWVzdBISCgpjaGFubmVsX2lkGAEgASgJEjIKC3Bvc3RfcG9saWN5GAIgASgOMh0uY29tcGFzcy52MS5DaGFubmVsUG9zdFBvbGljeRIUCgxvd25lcl9oYW5kbGUYAyABKAkSHgoWbWFuZGF0b3J5X3N1YnNjcmlwdGlvbhgEIAEoCCJAChhTZXRDaGFubmVsUG9saWN5UmVzcG9uc2USJAoHY2hhbm5lbBgBIAEoCzITLmNvbXBhc3MudjEuQ2hhbm5lbCJSChBHZXRSb3N0ZXJSZXF1ZXN0EiYKBXNjb3BlGAEgASgOMhcuY29tcGFzcy52MS5Sb3N0ZXJTY29wZRIWCg52YW50YWdlX2hhbmRsZRgCIAEoCSI9ChFHZXRSb3N0ZXJSZXNwb25zZRIoCgdlbnRyaWVzGAEgAygLMhcuY29tcGFzcy52MS5Sb3N0ZXJFbnRyeSLwAQoLUm9zdGVyRW50cnkSGAoQYWdlbnRfYWNjb3VudF9pZBgBIAEoCRIOCgZoYW5kbGUYAiABKAkSFAoMZGlzcGxheV9uYW1lGAMgASgJEhcKD3BhcmVudF9hZ2VudF9pZBgEIAEoCRIrCghwcmVzZW5jZRgFIAEoDjIZLmNvbXBhc3MudjEuQWdlbnRQcmVzZW5jZRIQCghhY3Rpdml0eRgGIAEoCRIbChNhY3Rpdml0eV9hdF91bml4X21zGAcgASgDEiwKBWJvYXJkGAggASgLMh0uY29tcGFzcy52MS5BZ2VudEJvYXJkU3VtbWFyeSJ3ChhVcGRhdGVQaW5uZWRCb2FyZFJlcXVlc3QSEgoKY2hhbm5lbF9pZBgBIAEoCRIlCgNwaW4YAiABKAsyFi5jb21wYXNzLnYxLlBpbk1lc3NhZ2VIABIaChB1bnBpbl9tZXNzYWdlX2lkGAMgASgJSABCBAoCb3AiPAoKUGluTWVzc2FnZRISCgptZXNzYWdlX2lkGAEgASgJEhoKEnJlcGxhY2VfbWVzc2FnZV9pZBgCIAEoCSJBChlVcGRhdGVQaW5uZWRCb2FyZFJlc3BvbnNlEiQKB2NoYW5uZWwYASABKAsyEy5jb21wYXNzLnYxLkNoYW5uZWwiMQoZT3BlbkFnZW50V29ya3NwYWNlUmVxdWVzdBIUCgxhZ2VudF9oYW5kbGUYASABKAkiSwoaT3BlbkFnZW50V29ya3NwYWNlUmVzcG9uc2USLQoJd29ya3NwYWNlGAEgASgLMhouY29tcGFzcy52MS5BZ2VudFdvcmtzcGFjZSKKAQoTTGlzdE1lc3NhZ2VzUmVxdWVzdBIUCgpjaGFubmVsX2lkGAEgASgJSAASDQoFbGltaXQYAiABKA0SGQoRYmVmb3JlX21lc3NhZ2VfaWQYAyABKAkSFAoMc25hcHNob3Rfc2VxGAQgASgEEhAKCHRvcGljX2lkGAUgASgJQgsKCWNvbnRhaW5lciI9ChRMaXN0TWVzc2FnZXNSZXNwb25zZRIlCghtZXNzYWdlcxgBIAMoCzITLmNvbXBhc3MudjEuTWVzc2FnZSLcAQoSUG9zdE1lc3NhZ2VSZXF1ZXN0EhQKCmNoYW5uZWxfaWQYASABKAlIABIoCgZibG9ja3MYAiADKAsyGC5jb21wYXNzLnYxLk1lc3NhZ2VCbG9jaxISCgh0b3BpY19pZBgDIAEoCUgBEhQKCnRvcGljX25hbWUYBCABKAlIARIZChFjbGllbnRfcmVxdWVzdF9pZBgFIAEoCRIUCgxjcmVhdGVfdG9waWMYBiABKAgSFQoNdHVybl9zZXF1ZW5jZRgHIAEoBEILCgljb250YWluZXJCBwoFdG9waWMiOwoTUG9zdE1lc3NhZ2VSZXNwb25zZRIkCgdtZXNzYWdlGAEgASgLMhMuY29tcGFzcy52MS5NZXNzYWdlIkEKEUxpc3RUb3BpY3NSZXF1ZXN0EhIKCmNoYW5uZWxfaWQYASABKAkSGAoQaW5jbHVkZV9hcmNoaXZlZBgCIAEoCCI3ChJMaXN0VG9waWNzUmVzcG9uc2USIQoGdG9waWNzGAEgAygLMhEuY29tcGFzcy52MS5Ub3BpYyJmChJVcGRhdGVUb3BpY1JlcXVlc3QSEAoIdG9waWNfaWQYASABKAkSEQoEbmFtZRgCIAEoCUgAiAEBEhUKCGFyY2hpdmVkGAMgASgISAGIAQFCBwoFX25hbWVCCwoJX2FyY2hpdmVkIjcKE1VwZGF0ZVRvcGljUmVzcG9uc2USIAoFdG9waWMYASABKAsyES5jb21wYXNzLnYxLlRvcGljIlUKE1Jlc3BvbmRUb0Fza1JlcXVlc3QSDgoGYXNrX2lkGAEgASgJEi4KB2Fuc3dlcnMYAiADKAsyHS5jb21wYXNzLnYxLkFza1F1ZXN0aW9uQW5zd2VyIlgKEUFza1F1ZXN0aW9uQW5zd2VyEhMKC3F1ZXN0aW9uX2lkGAEgASgJEhkKEWNob3Nlbl9vcHRpb25faWRzGAIgAygJEhMKC2N1c3RvbV90ZXh0GAMgASgJIhYKFFJlc3BvbmRUb0Fza1Jlc3BvbnNlImoKFVNlYXJjaE1lc3NhZ2VzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIUCgpjaGFubmVsX2lkGAIgASgJSAASDQoFbGltaXQYAyABKA0SFAoMc25hcHNob3Rfc2VxGAQgASgEQgcKBXNjb3BlIj8KFlNlYXJjaE1lc3NhZ2VzUmVzcG9uc2USJQoIbWVzc2FnZXMYASADKAsyEy5jb21wYXNzLnYxLk1lc3NhZ2UiQgoVU3Vic2NyaWJlQ29tbXNSZXF1ZXN0EhEKCXNpbmNlX3NlcRgBIAEoBBIWCg5pbnN0YW5jZV9lcG9jaBgCIAEoBCo1CghVc2VyUm9sZRIUChBVU0VSX1JPTEVfTUVNQkVSEAASEwoPVVNFUl9ST0xFX0FETUlOEAEqYQoWQ2hhbm5lbEdyb3VwVmlzaWJpbGl0eRIiCh5DSEFOTkVMX0dST1VQX1ZJU0lCSUxJVFlfT1dORVIQABIjCh9DSEFOTkVMX0dST1VQX1ZJU0lCSUxJVFlfU0hBUkVEEAEqVQoRQ2hhbm5lbFBvc3RQb2xpY3kSHAoYQ0hBTk5FTF9QT1NUX1BPTElDWV9PUEVOEAASIgoeQ0hBTk5FTF9QT1NUX1BPTElDWV9PV05FUl9PTkxZEAEqXwoVQ2hhbm5lbE1lbWJlcnNoaXBNb2RlEiQKIENIQU5ORUxfTUVNQkVSU0hJUF9NT0RFX0VYUExJQ0lUEAASIAocQ0hBTk5FTF9NRU1CRVJTSElQX01PREVfVFJFRRABKlsKC0NoYW5uZWxLaW5kEhgKFENIQU5ORUxfS0lORF9DSEFOTkVMEAASEwoPQ0hBTk5FTF9LSU5EX0RNEAESHQoVQ0hBTk5FTF9LSU5EX0dST1VQX0RNEAIaAggBKpwBCg1BZ2VudFByZXNlbmNlEh4KGkFHRU5UX1BSRVNFTkNFX1VOU1BFQ0lGSUVEEAASFwoTQUdFTlRfUFJFU0VOQ0VfSURMRRABEhoKFkFHRU5UX1BSRVNFTkNFX1dPUktJTkcQAhIaChZBR0VOVF9QUkVTRU5DRV9XQUlUSU5HEAMSGgoWQUdFTlRfUFJFU0VOQ0VfT0ZGTElORRAEKsMBCg5Cb2FyZFRhc2tTdGF0ZRIgChxCT0FSRF9UQVNLX1NUQVRFX1VOU1BFQ0lGSUVEEAASGwoXQk9BUkRfVEFTS19TVEFURV9BQ1RJVkUQARIcChhCT0FSRF9UQVNLX1NUQVRFX0JMT0NLRUQQAhIcChhCT0FSRF9UQVNLX1NUQVRFX1dBSVRJTkcQAxIbChdCT0FSRF9UQVNLX1NUQVRFX1BBUktFRBAEEhkKFUJPQVJEX1RBU0tfU1RBVEVfRE9ORRAFKooBCg1Cb2FyZEhhbHRLaW5kEh8KG0JPQVJEX0hBTFRfS0lORF9VTlNQRUNJRklFRBAAEhgKFEJPQVJEX0hBTFRfS0lORF9ET05FEAESGwoXQk9BUkRfSEFMVF9LSU5EX0JMT0NLRUQQAhIhCh1CT0FSRF9IQUxUX0tJTkRfQVdBSVRJTkdfVVNFUhADKowBCg5Cb2FyZFdhaXRpbmdPbhIgChxCT0FSRF9XQUlUSU5HX09OX1VOU1BFQ0lGSUVEEAASHQoZQk9BUkRfV0FJVElOR19PTl9PUEVSQVRPUhABEhoKFkJPQVJEX1dBSVRJTkdfT05fQUdFTlQQAhIdChlCT0FSRF9XQUlUSU5HX09OX0VYVEVSTkFMEAMqaQoMQm9hcmRSZWZLaW5kEh4KGkJPQVJEX1JFRl9LSU5EX1VOU1BFQ0lGSUVEEAASGAoUQk9BUkRfUkVGX0tJTkRfSVNTVUUQARIfChtCT0FSRF9SRUZfS0lORF9QVUxMX1JFUVVFU1QQAiqgAwoLQm9hcmRQckdhdGUSHQoZQk9BUkRfUFJfR0FURV9VTlNQRUNJRklFRBAAEhcKE0JPQVJEX1BSX0dBVEVfRFJBRlQQARIgChxCT0FSRF9QUl9HQVRFX0NIRUNLU19QRU5ESU5HEAISHwobQk9BUkRfUFJfR0FURV9DSEVDS1NfRkFJTEVEEAMSIwofQk9BUkRfUFJfR0FURV9DSEFOR0VTX1JFUVVFU1RFRBAEEiEKHUJPQVJEX1BSX0dBVEVfQVdBSVRJTkdfUkVWSUVXEAUSIgoeQk9BUkRfUFJfR0FURV9BV0FJVElOR19FTlFVRVVFEAYSGAoUQk9BUkRfUFJfR0FURV9NRVJHRUQQBxIYChRCT0FSRF9QUl9HQVRFX0NMT1NFRBAIEhgKFEJPQVJEX1BSX0dBVEVfUVVFVUVEEAkSHgoaQk9BUkRfUFJfR0FURV9RVUVVRV9GQUlMRUQQChIfChtCT0FSRF9QUl9HQVRFX1FVRVVFX1JFTU9WRUQQCxIbChdCT0FSRF9QUl9HQVRFX0NPTkZMSUNUUxAMKpEBCgxQZWVyaW5nU3RhdGUSHQoZUEVFUklOR19TVEFURV9VTlNQRUNJRklFRBAAEiIKHlBFRVJJTkdfU1RBVEVfUEVORElOR19PVVRHT0lORxABEiIKHlBFRVJJTkdfU1RBVEVfUEVORElOR19JTkNPTUlORxACEhoKFlBFRVJJTkdfU1RBVEVfQVBQUk9WRUQQAypeCgtSb3N0ZXJTY29wZRIdChlST1NURVJfU0NPUEVfTkVJR0hCT1JIT09EEAASGAoUUk9TVEVSX1NDT1BFX1NVQlRSRUUQARIWChJST1NURVJfU0NPUEVfT1dORVIQAjLqEAoMQ29tbXNTZXJ2aWNlEksKCkNyZWF0ZVVzZXISHS5jb21wYXNzLnYxLkNyZWF0ZVVzZXJSZXF1ZXN0Gh4uY29tcGFzcy52MS5DcmVhdGVVc2VyUmVzcG9uc2USTgoLQ3JlYXRlQWdlbnQSHi5jb21wYXNzLnYxLkNyZWF0ZUFnZW50UmVxdWVzdBofLmNvbXBhc3MudjEuQ3JlYXRlQWdlbnRSZXNwb25zZRJRCgxMaXN0QWNjb3VudHMSHy5jb21wYXNzLnYxLkxpc3RBY2NvdW50c1JlcXVlc3QaIC5jb21wYXNzLnYxLkxpc3RBY2NvdW50c1Jlc3BvbnNlEmMKEkNyZWF0ZUNoYW5uZWxHcm91cBIlLmNvbXBhc3MudjEuQ3JlYXRlQ2hhbm5lbEdyb3VwUmVxdWVzdBomLmNvbXBhc3MudjEuQ3JlYXRlQ2hhbm5lbEdyb3VwUmVzcG9uc2USYAoRTGlzdENoYW5uZWxHcm91cHMSJC5jb21wYXNzLnYxLkxpc3RDaGFubmVsR3JvdXBzUmVxdWVzdBolLmNvbXBhc3MudjEuTGlzdENoYW5uZWxHcm91cHNSZXNwb25zZRJRCgxMaXN0Q2hhbm5lbHMSHy5jb21wYXNzLnYxLkxpc3RDaGFubmVsc1JlcXVlc3QaIC5jb21wYXNzLnYxLkxpc3RDaGFubmVsc1Jlc3BvbnNlElQKDUNyZWF0ZUNoYW5uZWwSIC5jb21wYXNzLnYxLkNyZWF0ZUNoYW5uZWxSZXF1ZXN0GiEuY29tcGFzcy52MS5DcmVhdGVDaGFubmVsUmVzcG9uc2USaQoUVXBkYXRlQ2hhbm5lbE1lbWJlcnMSJy5jb21wYXNzLnYxLlVwZGF0ZUNoYW5uZWxNZW1iZXJzUmVxdWVzdBooLmNvbXBhc3MudjEuVXBkYXRlQ2hhbm5lbE1lbWJlcnNSZXNwb25zZRI/CgZPcGVuRE0SGS5jb21wYXNzLnYxLk9wZW5ETVJlcXVlc3QaGi5jb21wYXNzLnYxLk9wZW5ETVJlc3BvbnNlElQKDVJlcGFyZW50QWdlbnQSIC5jb21wYXNzLnYxLlJlcGFyZW50QWdlbnRSZXF1ZXN0GiEuY29tcGFzcy52MS5SZXBhcmVudEFnZW50UmVzcG9uc2USWgoPUmVwYXJlbnRDaGFubmVsEiIuY29tcGFzcy52MS5SZXBhcmVudENoYW5uZWxSZXF1ZXN0GiMuY29tcGFzcy52MS5SZXBhcmVudENoYW5uZWxSZXNwb25zZRJjChJPcGVuQWdlbnRXb3Jrc3BhY2USJS5jb21wYXNzLnYxLk9wZW5BZ2VudFdvcmtzcGFjZVJlcXVlc3QaJi5jb21wYXNzLnYxLk9wZW5BZ2VudFdvcmtzcGFjZVJlc3BvbnNlElEKDExpc3RNZXNzYWdlcxIfLmNvbXBhc3MudjEuTGlzdE1lc3NhZ2VzUmVxdWVzdBogLmNvbXBhc3MudjEuTGlzdE1lc3NhZ2VzUmVzcG9uc2USTgoLUG9zdE1lc3NhZ2USHi5jb21wYXNzLnYxLlBvc3RNZXNzYWdlUmVxdWVzdBofLmNvbXBhc3MudjEuUG9zdE1lc3NhZ2VSZXNwb25zZRJLCgpMaXN0VG9waWNzEh0uY29tcGFzcy52MS5MaXN0VG9waWNzUmVxdWVzdBoeLmNvbXBhc3MudjEuTGlzdFRvcGljc1Jlc3BvbnNlEk4KC1VwZGF0ZVRvcGljEh4uY29tcGFzcy52MS5VcGRhdGVUb3BpY1JlcXVlc3QaHy5jb21wYXNzLnYxLlVwZGF0ZVRvcGljUmVzcG9uc2USUQoMUmVzcG9uZFRvQXNrEh8uY29tcGFzcy52MS5SZXNwb25kVG9Bc2tSZXF1ZXN0GiAuY29tcGFzcy52MS5SZXNwb25kVG9Bc2tSZXNwb25zZRJXCg5TZWFyY2hNZXNzYWdlcxIhLmNvbXBhc3MudjEuU2VhcmNoTWVzc2FnZXNSZXF1ZXN0GiIuY29tcGFzcy52MS5TZWFyY2hNZXNzYWdlc1Jlc3BvbnNlEl0KEFNldENoYW5uZWxQb2xpY3kSIy5jb21wYXNzLnYxLlNldENoYW5uZWxQb2xpY3lSZXF1ZXN0GiQuY29tcGFzcy52MS5TZXRDaGFubmVsUG9saWN5UmVzcG9uc2USSAoJR2V0Um9zdGVyEhwuY29tcGFzcy52MS5HZXRSb3N0ZXJSZXF1ZXN0Gh0uY29tcGFzcy52MS5HZXRSb3N0ZXJSZXNwb25zZRJgChFVcGRhdGVQaW5uZWRCb2FyZBIkLmNvbXBhc3MudjEuVXBkYXRlUGlubmVkQm9hcmRSZXF1ZXN0GiUuY29tcGFzcy52MS5VcGRhdGVQaW5uZWRCb2FyZFJlc3BvbnNlEk4KC0FwcHJvdmVQZWVyEh4uY29tcGFzcy52MS5BcHByb3ZlUGVlclJlcXVlc3QaHy5jb21wYXNzLnYxLkFwcHJvdmVQZWVyUmVzcG9uc2USSwoKUmV2b2tlUGVlchIdLmNvbXBhc3MudjEuUmV2b2tlUGVlclJlcXVlc3QaHi5jb21wYXNzLnYxLlJldm9rZVBlZXJSZXNwb25zZRJICglMaXN0UGVlcnMSHC5jb21wYXNzLnYxLkxpc3RQZWVyc1JlcXVlc3QaHS5jb21wYXNzLnYxLkxpc3RQZWVyc1Jlc3BvbnNlElkKDlN1YnNjcmliZUNvbW1zEiEuY29tcGFzcy52MS5TdWJzY3JpYmVDb21tc1JlcXVlc3QaIi5jb21wYXNzLnYxLlN1YnNjcmliZUNvbW1zUmVzcG9uc2UwAWIGcHJvdG8z", [file_compass_v1_compass]); + fileDesc("ChZjb21wYXNzL3YxL2NvbW1zLnByb3RvEgpjb21wYXNzLnYxIsQBCgdBY2NvdW50EgoKAmlkGAEgASgJEg4KBmhhbmRsZRgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSJwoEdXNlchgKIAEoCzIXLmNvbXBhc3MudjEuVXNlckFjY291bnRIABIpCgVhZ2VudBgLIAEoCzIYLmNvbXBhc3MudjEuQWdlbnRBY2NvdW50SAASKwoGc3lzdGVtGAwgASgLMhkuY29tcGFzcy52MS5TeXN0ZW1BY2NvdW50SABCBgoEa2luZCIPCg1TeXN0ZW1BY2NvdW50IjEKC1VzZXJBY2NvdW50EiIKBHJvbGUYASABKA4yFC5jb21wYXNzLnYxLlVzZXJSb2xlIlcKDEFnZW50QWNjb3VudBIVCg1vd25lcl91c2VyX2lkGAEgASgJEhcKD2hvbWVfY2hhbm5lbF9pZBgCIAEoCRIXCg9wYXJlbnRfYWdlbnRfaWQYAyABKAkikAEKDENoYW5uZWxHcm91cBIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEhcKD3BhcmVudF9ncm91cF9pZBgDIAEoCRIVCg1vd25lcl91c2VyX2lkGAQgASgJEjYKCnZpc2liaWxpdHkYBSABKA4yIi5jb21wYXNzLnYxLkNoYW5uZWxHcm91cFZpc2liaWxpdHkijAMKB0NoYW5uZWwSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRIQCghncm91cF9pZBgDIAEoCRIlCgRraW5kGAQgASgOMhcuY29tcGFzcy52MS5DaGFubmVsS2luZBIaChJtZW1iZXJfYWNjb3VudF9pZHMYBSADKAkSHgoWc3Vic2NyaWJlcl9hY2NvdW50X2lkcxgGIAMoCRIyCgtwb3N0X3BvbGljeRgHIAEoDjIdLmNvbXBhc3MudjEuQ2hhbm5lbFBvc3RQb2xpY3kSGAoQb3duZXJfYWNjb3VudF9pZBgIIAEoCRIeChZtYW5kYXRvcnlfc3Vic2NyaXB0aW9uGAkgASgIEi8KDnBpbm5lZF9lbnRyaWVzGAogAygLMhcuY29tcGFzcy52MS5QaW5uZWRFbnRyeRIXCg9wYXJlbnRfYWdlbnRfaWQYCyABKAkSOgoPbWVtYmVyc2hpcF9tb2RlGAwgASgOMiEuY29tcGFzcy52MS5DaGFubmVsTWVtYmVyc2hpcE1vZGUibAoLUGlubmVkRW50cnkSEgoKbWVzc2FnZV9pZBgBIAEoCRIQCghwb3NpdGlvbhgCIAEoBRIZChFwaW5uZWRfYXRfdW5peF9tcxgDIAEoAxIcChRwaW5uZWRfYnlfYWNjb3VudF9pZBgEIAEoCSI2Cg5BZ2VudFdvcmtzcGFjZRIKCgJpZBgBIAEoCRIYChBhZ2VudF9hY2NvdW50X2lkGAIgASgJIvIBCgVUb3BpYxIKCgJpZBgBIAEoCRISCgpjaGFubmVsX2lkGAIgASgJEgwKBG5hbWUYAyABKAkSGgoSY3JlYXRlZF9hdF91bml4X21zGAQgASgDEh0KFWNyZWF0ZWRfYnlfYWNjb3VudF9pZBgFIAEoCRIQCghhcmNoaXZlZBgGIAEoCBIaCg1tZXNzYWdlX2NvdW50GAcgASgESACIAQESJAoXbGFzdF9tZXNzYWdlX2F0X3VuaXhfbXMYCCABKANIAYgBAUIQCg5fbWVzc2FnZV9jb3VudEIaChhfbGFzdF9tZXNzYWdlX2F0X3VuaXhfbXMiwgEKB01lc3NhZ2USCgoCaWQYASABKAkSEAoIdG9waWNfaWQYAiABKAkSGQoRYXV0aG9yX2FjY291bnRfaWQYAyABKAkSEgoKYXRfdW5peF9tcxgEIAEoAxIoCgZibG9ja3MYBSADKAsyGC5jb21wYXNzLnYxLk1lc3NhZ2VCbG9jaxIVCg1hdXRob3JfaGFuZGxlGAYgASgJEhIKCmNoYW5uZWxfaWQYByABKAkSFQoNdHVybl9zZXF1ZW5jZRgIIAEoBCJ5CgxNZXNzYWdlQmxvY2sSDgoEdGV4dBgBIAEoCUgAEh4KA2FzaxgCIAEoCzIPLmNvbXBhc3MudjEuQXNrSAASMAoKYXNrX2Fuc3dlchgDIAEoCzIaLmNvbXBhc3MudjEuQXNrQW5zd2VyQmxvY2tIAEIHCgVibG9jayJICg5Bc2tBbnN3ZXJCbG9jaxIcCgNhc2sYASABKAsyDy5jb21wYXNzLnYxLkFzaxIYChBhc2tlcl9hY2NvdW50X2lkGAIgASgJImAKA0FzaxIOCgZhc2tfaWQYASABKAkSKgoJcXVlc3Rpb25zGAIgAygLMhcuY29tcGFzcy52MS5Bc2tRdWVzdGlvbhIQCghhbnN3ZXJlZBgDIAEoCBILCgNyZWYYBCABKAki8QEKC0Fza1F1ZXN0aW9uEhMKC3F1ZXN0aW9uX2lkGAEgASgJEhAKCHF1ZXN0aW9uGAIgASgJEg4KBmhlYWRlchgDIAEoCRImCgdvcHRpb25zGAQgAygLMhUuY29tcGFzcy52MS5Bc2tPcHRpb24SFgoOYWxsb3dfbXVsdGlwbGUYBSABKAgSGAoLcmVjb21tZW5kZWQYBiABKAVIAIgBARIZChFjaG9zZW5fb3B0aW9uX2lkcxgHIAMoCRITCgtjdXN0b21fdGV4dBgIIAEoCRIRCgl0aW1lZF9vdXQYCSABKAhCDgoMX3JlY29tbWVuZGVkIkwKCUFza09wdGlvbhIKCgJpZBgBIAEoCRINCgVsYWJlbBgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIPCgdwcmV2aWV3GAQgASgJIscFChZTdWJzY3JpYmVDb21tc1Jlc3BvbnNlEgsKA3NlcRgBIAEoBBISCgphdF91bml4X21zGAIgASgDEhYKDmluc3RhbmNlX2Vwb2NoGAMgASgEEhQKDHNuYXBzaG90X3NlcRgEIAEoBBIzCg5tZXNzYWdlX3Bvc3RlZBgKIAEoCzIZLmNvbXBhc3MudjEuTWVzc2FnZVBvc3RlZEgAEjUKD21lc3NhZ2VfdXBkYXRlZBgLIAEoCzIaLmNvbXBhc3MudjEuTWVzc2FnZVVwZGF0ZWRIABI1Cg9jaGFubmVsX2NoYW5nZWQYDCABKAsyGi5jb21wYXNzLnYxLkNoYW5uZWxDaGFuZ2VkSAASQAoVY2hhbm5lbF9ncm91cF9jaGFuZ2VkGA0gASgLMh8uY29tcGFzcy52MS5DaGFubmVsR3JvdXBDaGFuZ2VkSAASNQoPYWNjb3VudF9jaGFuZ2VkGA4gASgLMhouY29tcGFzcy52MS5BY2NvdW50Q2hhbmdlZEgAEkQKF2FnZW50X3dvcmtzcGFjZV9jaGFuZ2VkGA8gASgLMiEuY29tcGFzcy52MS5BZ2VudFdvcmtzcGFjZUNoYW5nZWRIABI6Cg9yZXN5bmNfcmVxdWlyZWQYECABKAsyHy5jb21wYXNzLnYxLkNvbW1zUmVzeW5jUmVxdWlyZWRIABJCChZhZ2VudF9wcmVzZW5jZV9jaGFuZ2VkGBEgASgLMiAuY29tcGFzcy52MS5BZ2VudFByZXNlbmNlQ2hhbmdlZEgAEjMKDnRvcGljX3Vwc2VydGVkGBIgASgLMhkuY29tcGFzcy52MS5Ub3BpY1Vwc2VydGVkSAASPAoTYWdlbnRfYm9hcmRfY2hhbmdlZBgTIAEoCzIdLmNvbXBhc3MudjEuQWdlbnRCb2FyZENoYW5nZWRIAEIJCgdwYXlsb2FkIkwKDU1lc3NhZ2VQb3N0ZWQSJAoHbWVzc2FnZRgBIAEoCzITLmNvbXBhc3MudjEuTWVzc2FnZRIVCg10dXJuX3NlcXVlbmNlGAIgASgEIjYKDk1lc3NhZ2VVcGRhdGVkEiQKB21lc3NhZ2UYASABKAsyEy5jb21wYXNzLnYxLk1lc3NhZ2UiMQoNVG9waWNVcHNlcnRlZBIgCgV0b3BpYxgBIAEoCzIRLmNvbXBhc3MudjEuVG9waWMiUwoOQ2hhbm5lbENoYW5nZWQSJAoHY2hhbm5lbBgBIAEoCzITLmNvbXBhc3MudjEuQ2hhbm5lbBIbChNyZW1vdmVkX2FjY291bnRfaWRzGAIgAygJIj4KE0NoYW5uZWxHcm91cENoYW5nZWQSJwoFZ3JvdXAYASABKAsyGC5jb21wYXNzLnYxLkNoYW5uZWxHcm91cCI2Cg5BY2NvdW50Q2hhbmdlZBIkCgdhY2NvdW50GAEgASgLMhMuY29tcGFzcy52MS5BY2NvdW50IkYKFUFnZW50V29ya3NwYWNlQ2hhbmdlZBItCgl3b3Jrc3BhY2UYASABKAsyGi5jb21wYXNzLnYxLkFnZW50V29ya3NwYWNlIhUKE0NvbW1zUmVzeW5jUmVxdWlyZWQibwoUQWdlbnRQcmVzZW5jZUNoYW5nZWQSGAoQYWdlbnRfYWNjb3VudF9pZBgBIAEoCRIrCghwcmVzZW5jZRgCIAEoDjIZLmNvbXBhc3MudjEuQWdlbnRQcmVzZW5jZRIQCghhY3Rpdml0eRgDIAEoCSJ3CgxCb2FyZFdhaXRpbmcSJgoCb24YASABKA4yGi5jb21wYXNzLnYxLkJvYXJkV2FpdGluZ09uEhQKDGFnZW50X2hhbmRsZRgCIAEoCRIMCgR3aGF0GAMgASgJEgsKA3JlZhgEIAEoCRIOCgZibG9ja3MYBSADKAkihQEKDUJvYXJkU3ViYWdlbnQSCgoCaWQYASABKAkSDQoFYWdlbnQYAiABKAkSEwoLZGVzY3JpcHRpb24YAyABKAkSDgoGc3RhdHVzGAQgASgJEhoKEnN0YXJ0ZWRfYXRfdW5peF9tcxgFIAEoAxIYChBlbmRlZF9hdF91bml4X21zGAYgASgDIvgBCglCb2FyZFRhc2sSCwoDa2V5GAEgASgJEg0KBXRpdGxlGAIgASgJEhMKC3ByaW1hcnlfcmVmGAMgASgJEgwKBHJlZnMYBCADKAkSKQoFc3RhdGUYBSABKA4yGi5jb21wYXNzLnYxLkJvYXJkVGFza1N0YXRlEgwKBG5vdGUYBiABKAkSKQoHd2FpdGluZxgHIAMoCzIYLmNvbXBhc3MudjEuQm9hcmRXYWl0aW5nEiwKCXN1YmFnZW50cxgIIAMoCzIZLmNvbXBhc3MudjEuQm9hcmRTdWJhZ2VudBIaChJ3cml0dGVuX2F0X3VuaXhfbXMYCSABKAMiHQoLQm9hcmRSZWZzT3ASDgoGdmFsdWVzGAEgAygJIjoKDkJvYXJkV2FpdGluZ09wEigKBnZhbHVlcxgBIAMoCzIYLmNvbXBhc3MudjEuQm9hcmRXYWl0aW5nIsQCCg5Cb2FyZFRhc2tQYXRjaBILCgNrZXkYASABKAkSEgoFdGl0bGUYAiABKAlIAIgBARIYCgtwcmltYXJ5X3JlZhgDIAEoCUgBiAEBEi4KBXN0YXRlGAQgASgOMhouY29tcGFzcy52MS5Cb2FyZFRhc2tTdGF0ZUgCiAEBEhEKBG5vdGUYBSABKAlIA4gBARIlCgRyZWZzGAYgASgLMhcuY29tcGFzcy52MS5Cb2FyZFJlZnNPcBIrCgd3YWl0aW5nGAcgASgLMhouY29tcGFzcy52MS5Cb2FyZFdhaXRpbmdPcBIzChB1cHNlcnRfc3ViYWdlbnRzGAggAygLMhkuY29tcGFzcy52MS5Cb2FyZFN1YmFnZW50QggKBl90aXRsZUIOCgxfcHJpbWFyeV9yZWZCCAoGX3N0YXRlQgcKBV9ub3RlIlEKCUJvYXJkSGFsdBInCgRraW5kGAEgASgOMhkuY29tcGFzcy52MS5Cb2FyZEhhbHRLaW5kEgsKA3JlZhgCIAEoCRIOCgZyZWFzb24YAyABKAkijwMKCEJvYXJkUmVmEgsKA3JlZhgBIAEoCRImCgRraW5kGAIgASgOMhguY29tcGFzcy52MS5Cb2FyZFJlZktpbmQSDQoFdGl0bGUYAyABKAkSCwoDdXJsGAQgASgJEhMKC2ZvcmdlX3N0YXRlGAUgASgJEisKC2lzc3VlX3N0YXRlGAYgASgOMhYuY29tcGFzcy52MS5Jc3N1ZVN0YXRlEg0KBWRyYWZ0GAcgASgIEhQKDGNoZWNrc19zdGF0ZRgIIAEoCRIlCgRnYXRlGAkgASgOMhcuY29tcGFzcy52MS5Cb2FyZFByR2F0ZRIVCg1zdGFja190aXBfcmVmGAogASgJEhYKDnN0YWNrX3Bvc2l0aW9uGAsgASgNEg0KBW93bmVkGAwgASgIEhsKE29ic2VydmVkX2F0X3VuaXhfbXMYDSABKAMSGQoRbWVyZ2VfcXVldWVfc3RhdGUYDiABKAkSFgoOc3RhbGVfYXBwcm92YWwYDyABKAgSFgoOdHJhY2tlcl9zdGF0dXMYECABKAki1AEKCEJvYXJkQXNrEg4KBmFza19pZBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJEhIKCmNoYW5uZWxfaWQYAyABKAkSCwoDcmVmGAQgASgJEg4KBmhlYWRlchgFIAEoCRIQCghhbnN3ZXJlZBgGIAEoCBIZChFwb3N0ZWRfYXRfdW5peF9tcxgHIAEoAxIyCgxtaXJyb3Jfc3RhdGUYCCABKA4yHC5jb21wYXNzLnYxLkJvYXJkTWlycm9yU3RhdGUSEgoKbWlycm9yX3VybBgJIAEoCSJuChRNaXJyb3JUb0ZvcmdlUmVxdWVzdBILCgNyZWYYASABKAkSDgoGYXNrX2lkGAIgASgJEhAKCHRvcGljX2lkGAMgASgJEhMKC21lc3NhZ2VfaWRzGAQgAygJEhIKCmNoYW5uZWxfaWQYBSABKAkiLAoVTWlycm9yVG9Gb3JnZVJlc3BvbnNlEhMKC2NvbW1lbnRfdXJsGAEgASgJItUCCgpBZ2VudEJvYXJkEhgKEGFnZW50X2FjY291bnRfaWQYASABKAkSDgoGaGFuZGxlGAIgASgJEhYKDmZvY3VzX3Rhc2tfa2V5GAMgASgJEiMKBGhhbHQYBCABKAsyFS5jb21wYXNzLnYxLkJvYXJkSGFsdBIkCgV0YXNrcxgFIAMoCzIVLmNvbXBhc3MudjEuQm9hcmRUYXNrEiIKBHJlZnMYBiADKAsyFC5jb21wYXNzLnYxLkJvYXJkUmVmEhYKDnVudHJhY2tlZF9yZWZzGAcgAygJEiIKBGFza3MYCCADKAsyFC5jb21wYXNzLnYxLkJvYXJkQXNrEhQKDHdyaXR0ZW5fdHVybhgJIAEoBBIaChJ3cml0dGVuX2F0X3VuaXhfbXMYCiABKAMSDQoFc3RhbGUYCyABKAgSGQoRbWF0Y2hlZF90YXNrX2tleXMYDCADKAkiowEKEUFnZW50Qm9hcmRTdW1tYXJ5Eg0KBWZvY3VzGAEgASgJEhIKCnRhc2tfY291bnQYAiABKA0SGwoTd2FpdGluZ19vbl9vcGVyYXRvchgDIAEoDRIjCgRoYWx0GAQgASgLMhUuY29tcGFzcy52MS5Cb2FyZEhhbHQSGgoSd3JpdHRlbl9hdF91bml4X21zGAUgASgDEg0KBXN0YWxlGAYgASgIIvgBChdVcGRhdGVBZ2VudEJvYXJkUmVxdWVzdBIwCgx1cHNlcnRfdGFza3MYASADKAsyGi5jb21wYXNzLnYxLkJvYXJkVGFza1BhdGNoEhgKEHJlbW92ZV90YXNrX2tleXMYAiADKAkSGwoOZm9jdXNfdGFza19rZXkYAyABKAlIAIgBARIjCgRoYWx0GAQgASgLMhUuY29tcGFzcy52MS5Cb2FyZEhhbHQSEgoKY2xlYXJfaGFsdBgFIAEoCBIVCg10dXJuX3NlcXVlbmNlGAYgASgEEhEKCWF1dG9tYXRpYxgHIAEoCEIRCg9fZm9jdXNfdGFza19rZXkiQQoYVXBkYXRlQWdlbnRCb2FyZFJlc3BvbnNlEiUKBWJvYXJkGAEgASgLMhYuY29tcGFzcy52MS5BZ2VudEJvYXJkIt4BChZMaXN0QWdlbnRCb2FyZHNSZXF1ZXN0EhUKDWFnZW50X2hhbmRsZXMYASADKAkSDQoFcXVlcnkYAiABKAkSCwoDcmVmGAMgASgJEi4KCndhaXRpbmdfb24YBCABKA4yGi5jb21wYXNzLnYxLkJvYXJkV2FpdGluZ09uEikKBXN0YXRlGAUgASgOMhouY29tcGFzcy52MS5Cb2FyZFRhc2tTdGF0ZRInCgRoYWx0GAYgASgOMhkuY29tcGFzcy52MS5Cb2FyZEhhbHRLaW5kEg0KBWxpbWl0GAcgASgNIkEKF0xpc3RBZ2VudEJvYXJkc1Jlc3BvbnNlEiYKBmJvYXJkcxgBIAMoCzIWLmNvbXBhc3MudjEuQWdlbnRCb2FyZCJUChFBZ2VudEJvYXJkQ2hhbmdlZBIYChBhZ2VudF9hY2NvdW50X2lkGAEgASgJEiUKBWJvYXJkGAIgASgLMhYuY29tcGFzcy52MS5BZ2VudEJvYXJkIlsKB1BlZXJpbmcSFwoPdXNlcl9hY2NvdW50X2lkGAEgASgJEg4KBmhhbmRsZRgCIAEoCRInCgVzdGF0ZRgDIAEoDjIYLmNvbXBhc3MudjEuUGVlcmluZ1N0YXRlIikKEkFwcHJvdmVQZWVyUmVxdWVzdBITCgtwZWVyX2hhbmRsZRgBIAEoCSI7ChNBcHByb3ZlUGVlclJlc3BvbnNlEiQKB3BlZXJpbmcYASABKAsyEy5jb21wYXNzLnYxLlBlZXJpbmciKAoRUmV2b2tlUGVlclJlcXVlc3QSEwoLcGVlcl9oYW5kbGUYASABKAkiJQoSUmV2b2tlUGVlclJlc3BvbnNlEg8KB2RlbGV0ZWQYASABKAgiEgoQTGlzdFBlZXJzUmVxdWVzdCI6ChFMaXN0UGVlcnNSZXNwb25zZRIlCghwZWVyaW5ncxgBIAMoCzITLmNvbXBhc3MudjEuUGVlcmluZyI5ChFDcmVhdGVVc2VyUmVxdWVzdBIOCgZoYW5kbGUYASABKAkSFAoMZGlzcGxheV9uYW1lGAIgASgJIjoKEkNyZWF0ZVVzZXJSZXNwb25zZRIkCgdhY2NvdW50GAEgASgLMhMuY29tcGFzcy52MS5BY2NvdW50InAKEkNyZWF0ZUFnZW50UmVxdWVzdBIOCgZoYW5kbGUYASABKAkSFAoMZGlzcGxheV9uYW1lGAIgASgJEhUKDXBhcmVudF9oYW5kbGUYAyABKAkSDAoEcm9sZRgEIAEoCRIPCgdwZXJzb25hGAUgASgJIjsKE0NyZWF0ZUFnZW50UmVzcG9uc2USJAoHYWNjb3VudBgBIAEoCzITLmNvbXBhc3MudjEuQWNjb3VudCIrChNMaXN0QWNjb3VudHNSZXF1ZXN0EhQKDHNuYXBzaG90X3NlcRgBIAEoBCI9ChRMaXN0QWNjb3VudHNSZXNwb25zZRIlCghhY2NvdW50cxgBIAMoCzITLmNvbXBhc3MudjEuQWNjb3VudCKVAQoZQ3JlYXRlQ2hhbm5lbEdyb3VwUmVxdWVzdBIMCgRuYW1lGAEgASgJEhcKD3BhcmVudF9ncm91cF9pZBgCIAEoCRI2Cgp2aXNpYmlsaXR5GAMgASgOMiIuY29tcGFzcy52MS5DaGFubmVsR3JvdXBWaXNpYmlsaXR5EhkKEXBhcmVudF9ncm91cF9uYW1lGAQgASgJIkUKGkNyZWF0ZUNoYW5uZWxHcm91cFJlc3BvbnNlEicKBWdyb3VwGAEgASgLMhguY29tcGFzcy52MS5DaGFubmVsR3JvdXAiMAoYTGlzdENoYW5uZWxHcm91cHNSZXF1ZXN0EhQKDHNuYXBzaG90X3NlcRgBIAEoBCJFChlMaXN0Q2hhbm5lbEdyb3Vwc1Jlc3BvbnNlEigKBmdyb3VwcxgBIAMoCzIYLmNvbXBhc3MudjEuQ2hhbm5lbEdyb3VwIisKE0xpc3RDaGFubmVsc1JlcXVlc3QSFAoMc25hcHNob3Rfc2VxGAEgASgEIj0KFExpc3RDaGFubmVsc1Jlc3BvbnNlEiUKCGNoYW5uZWxzGAEgAygLMhMuY29tcGFzcy52MS5DaGFubmVsIuIBChRDcmVhdGVDaGFubmVsUmVxdWVzdBIMCgRuYW1lGAEgASgJEhAKCGdyb3VwX2lkGAIgASgJEiUKBGtpbmQYAyABKA4yFy5jb21wYXNzLnYxLkNoYW5uZWxLaW5kEhYKDm1lbWJlcl9oYW5kbGVzGAQgAygJEhsKE3BhcmVudF9hZ2VudF9oYW5kbGUYBSABKAkSOgoPbWVtYmVyc2hpcF9tb2RlGAYgASgOMiEuY29tcGFzcy52MS5DaGFubmVsTWVtYmVyc2hpcE1vZGUSEgoKZ3JvdXBfbmFtZRgHIAEoCSI9ChVDcmVhdGVDaGFubmVsUmVzcG9uc2USJAoHY2hhbm5lbBgBIAEoCzITLmNvbXBhc3MudjEuQ2hhbm5lbCLCAQobVXBkYXRlQ2hhbm5lbE1lbWJlcnNSZXF1ZXN0EhIKCmNoYW5uZWxfaWQYASABKAkSGgoSYWRkX21lbWJlcl9oYW5kbGVzGAIgAygJEh0KFXJlbW92ZV9tZW1iZXJfaGFuZGxlcxgDIAMoCRIZChFzdWJzY3JpYmVfaGFuZGxlcxgEIAMoCRIbChN1bnN1YnNjcmliZV9oYW5kbGVzGAUgAygJEhwKFGNvbnZlcnRfY2hhbm5lbF9uYW1lGAYgASgJIkQKHFVwZGF0ZUNoYW5uZWxNZW1iZXJzUmVzcG9uc2USJAoHY2hhbm5lbBgBIAEoCzITLmNvbXBhc3MudjEuQ2hhbm5lbCIkCg1PcGVuRE1SZXF1ZXN0EhMKC3BlZXJfaGFuZGxlGAEgASgJIkcKDk9wZW5ETVJlc3BvbnNlEiQKB2NoYW5uZWwYASABKAsyEy5jb21wYXNzLnYxLkNoYW5uZWwSDwoHY3JlYXRlZBgCIAEoCCJHChRSZXBhcmVudEFnZW50UmVxdWVzdBIUCgxhZ2VudF9oYW5kbGUYASABKAkSGQoRbmV3X3BhcmVudF9oYW5kbGUYAiABKAkiPQoVUmVwYXJlbnRBZ2VudFJlc3BvbnNlEiQKB2FjY291bnQYASABKAsyEy5jb21wYXNzLnYxLkFjY291bnQiTQoWUmVwYXJlbnRDaGFubmVsUmVxdWVzdBISCgpjaGFubmVsX2lkGAEgASgJEh8KF25ld19wYXJlbnRfYWdlbnRfaGFuZGxlGAIgASgJIj8KF1JlcGFyZW50Q2hhbm5lbFJlc3BvbnNlEiQKB2NoYW5uZWwYASABKAsyEy5jb21wYXNzLnYxLkNoYW5uZWwilwEKF1NldENoYW5uZWxQb2xpY3lSZXF1ZXN0EhIKCmNoYW5uZWxfaWQYASABKAkSMgoLcG9zdF9wb2xpY3kYAiABKA4yHS5jb21wYXNzLnYxLkNoYW5uZWxQb3N0UG9saWN5EhQKDG93bmVyX2hhbmRsZRgDIAEoCRIeChZtYW5kYXRvcnlfc3Vic2NyaXB0aW9uGAQgASgIIkAKGFNldENoYW5uZWxQb2xpY3lSZXNwb25zZRIkCgdjaGFubmVsGAEgASgLMhMuY29tcGFzcy52MS5DaGFubmVsIlIKEEdldFJvc3RlclJlcXVlc3QSJgoFc2NvcGUYASABKA4yFy5jb21wYXNzLnYxLlJvc3RlclNjb3BlEhYKDnZhbnRhZ2VfaGFuZGxlGAIgASgJIj0KEUdldFJvc3RlclJlc3BvbnNlEigKB2VudHJpZXMYASADKAsyFy5jb21wYXNzLnYxLlJvc3RlckVudHJ5IvABCgtSb3N0ZXJFbnRyeRIYChBhZ2VudF9hY2NvdW50X2lkGAEgASgJEg4KBmhhbmRsZRgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSFwoPcGFyZW50X2FnZW50X2lkGAQgASgJEisKCHByZXNlbmNlGAUgASgOMhkuY29tcGFzcy52MS5BZ2VudFByZXNlbmNlEhAKCGFjdGl2aXR5GAYgASgJEhsKE2FjdGl2aXR5X2F0X3VuaXhfbXMYByABKAMSLAoFYm9hcmQYCCABKAsyHS5jb21wYXNzLnYxLkFnZW50Qm9hcmRTdW1tYXJ5IncKGFVwZGF0ZVBpbm5lZEJvYXJkUmVxdWVzdBISCgpjaGFubmVsX2lkGAEgASgJEiUKA3BpbhgCIAEoCzIWLmNvbXBhc3MudjEuUGluTWVzc2FnZUgAEhoKEHVucGluX21lc3NhZ2VfaWQYAyABKAlIAEIECgJvcCI8CgpQaW5NZXNzYWdlEhIKCm1lc3NhZ2VfaWQYASABKAkSGgoScmVwbGFjZV9tZXNzYWdlX2lkGAIgASgJIkEKGVVwZGF0ZVBpbm5lZEJvYXJkUmVzcG9uc2USJAoHY2hhbm5lbBgBIAEoCzITLmNvbXBhc3MudjEuQ2hhbm5lbCIxChlPcGVuQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCSJLChpPcGVuQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRItCgl3b3Jrc3BhY2UYASABKAsyGi5jb21wYXNzLnYxLkFnZW50V29ya3NwYWNlIooBChNMaXN0TWVzc2FnZXNSZXF1ZXN0EhQKCmNoYW5uZWxfaWQYASABKAlIABINCgVsaW1pdBgCIAEoDRIZChFiZWZvcmVfbWVzc2FnZV9pZBgDIAEoCRIUCgxzbmFwc2hvdF9zZXEYBCABKAQSEAoIdG9waWNfaWQYBSABKAlCCwoJY29udGFpbmVyIj0KFExpc3RNZXNzYWdlc1Jlc3BvbnNlEiUKCG1lc3NhZ2VzGAEgAygLMhMuY29tcGFzcy52MS5NZXNzYWdlItwBChJQb3N0TWVzc2FnZVJlcXVlc3QSFAoKY2hhbm5lbF9pZBgBIAEoCUgAEigKBmJsb2NrcxgCIAMoCzIYLmNvbXBhc3MudjEuTWVzc2FnZUJsb2NrEhIKCHRvcGljX2lkGAMgASgJSAESFAoKdG9waWNfbmFtZRgEIAEoCUgBEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAUgASgJEhQKDGNyZWF0ZV90b3BpYxgGIAEoCBIVCg10dXJuX3NlcXVlbmNlGAcgASgEQgsKCWNvbnRhaW5lckIHCgV0b3BpYyI7ChNQb3N0TWVzc2FnZVJlc3BvbnNlEiQKB21lc3NhZ2UYASABKAsyEy5jb21wYXNzLnYxLk1lc3NhZ2UiQQoRTGlzdFRvcGljc1JlcXVlc3QSEgoKY2hhbm5lbF9pZBgBIAEoCRIYChBpbmNsdWRlX2FyY2hpdmVkGAIgASgIIjcKEkxpc3RUb3BpY3NSZXNwb25zZRIhCgZ0b3BpY3MYASADKAsyES5jb21wYXNzLnYxLlRvcGljImYKElVwZGF0ZVRvcGljUmVxdWVzdBIQCgh0b3BpY19pZBgBIAEoCRIRCgRuYW1lGAIgASgJSACIAQESFQoIYXJjaGl2ZWQYAyABKAhIAYgBAUIHCgVfbmFtZUILCglfYXJjaGl2ZWQiNwoTVXBkYXRlVG9waWNSZXNwb25zZRIgCgV0b3BpYxgBIAEoCzIRLmNvbXBhc3MudjEuVG9waWMiVQoTUmVzcG9uZFRvQXNrUmVxdWVzdBIOCgZhc2tfaWQYASABKAkSLgoHYW5zd2VycxgCIAMoCzIdLmNvbXBhc3MudjEuQXNrUXVlc3Rpb25BbnN3ZXIiWAoRQXNrUXVlc3Rpb25BbnN3ZXISEwoLcXVlc3Rpb25faWQYASABKAkSGQoRY2hvc2VuX29wdGlvbl9pZHMYAiADKAkSEwoLY3VzdG9tX3RleHQYAyABKAkiFgoUUmVzcG9uZFRvQXNrUmVzcG9uc2UiagoVU2VhcmNoTWVzc2FnZXNSZXF1ZXN0Eg0KBXF1ZXJ5GAEgASgJEhQKCmNoYW5uZWxfaWQYAiABKAlIABINCgVsaW1pdBgDIAEoDRIUCgxzbmFwc2hvdF9zZXEYBCABKARCBwoFc2NvcGUiPwoWU2VhcmNoTWVzc2FnZXNSZXNwb25zZRIlCghtZXNzYWdlcxgBIAMoCzITLmNvbXBhc3MudjEuTWVzc2FnZSJCChVTdWJzY3JpYmVDb21tc1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEKjUKCFVzZXJSb2xlEhQKEFVTRVJfUk9MRV9NRU1CRVIQABITCg9VU0VSX1JPTEVfQURNSU4QASphChZDaGFubmVsR3JvdXBWaXNpYmlsaXR5EiIKHkNIQU5ORUxfR1JPVVBfVklTSUJJTElUWV9PV05FUhAAEiMKH0NIQU5ORUxfR1JPVVBfVklTSUJJTElUWV9TSEFSRUQQASpVChFDaGFubmVsUG9zdFBvbGljeRIcChhDSEFOTkVMX1BPU1RfUE9MSUNZX09QRU4QABIiCh5DSEFOTkVMX1BPU1RfUE9MSUNZX09XTkVSX09OTFkQASpfChVDaGFubmVsTWVtYmVyc2hpcE1vZGUSJAogQ0hBTk5FTF9NRU1CRVJTSElQX01PREVfRVhQTElDSVQQABIgChxDSEFOTkVMX01FTUJFUlNISVBfTU9ERV9UUkVFEAEqWwoLQ2hhbm5lbEtpbmQSGAoUQ0hBTk5FTF9LSU5EX0NIQU5ORUwQABITCg9DSEFOTkVMX0tJTkRfRE0QARIdChVDSEFOTkVMX0tJTkRfR1JPVVBfRE0QAhoCCAEqnAEKDUFnZW50UHJlc2VuY2USHgoaQUdFTlRfUFJFU0VOQ0VfVU5TUEVDSUZJRUQQABIXChNBR0VOVF9QUkVTRU5DRV9JRExFEAESGgoWQUdFTlRfUFJFU0VOQ0VfV09SS0lORxACEhoKFkFHRU5UX1BSRVNFTkNFX1dBSVRJTkcQAxIaChZBR0VOVF9QUkVTRU5DRV9PRkZMSU5FEAQqwwEKDkJvYXJkVGFza1N0YXRlEiAKHEJPQVJEX1RBU0tfU1RBVEVfVU5TUEVDSUZJRUQQABIbChdCT0FSRF9UQVNLX1NUQVRFX0FDVElWRRABEhwKGEJPQVJEX1RBU0tfU1RBVEVfQkxPQ0tFRBACEhwKGEJPQVJEX1RBU0tfU1RBVEVfV0FJVElORxADEhsKF0JPQVJEX1RBU0tfU1RBVEVfUEFSS0VEEAQSGQoVQk9BUkRfVEFTS19TVEFURV9ET05FEAUqigEKDUJvYXJkSGFsdEtpbmQSHwobQk9BUkRfSEFMVF9LSU5EX1VOU1BFQ0lGSUVEEAASGAoUQk9BUkRfSEFMVF9LSU5EX0RPTkUQARIbChdCT0FSRF9IQUxUX0tJTkRfQkxPQ0tFRBACEiEKHUJPQVJEX0hBTFRfS0lORF9BV0FJVElOR19VU0VSEAMqjAEKDkJvYXJkV2FpdGluZ09uEiAKHEJPQVJEX1dBSVRJTkdfT05fVU5TUEVDSUZJRUQQABIdChlCT0FSRF9XQUlUSU5HX09OX09QRVJBVE9SEAESGgoWQk9BUkRfV0FJVElOR19PTl9BR0VOVBACEh0KGUJPQVJEX1dBSVRJTkdfT05fRVhURVJOQUwQAyppCgxCb2FyZFJlZktpbmQSHgoaQk9BUkRfUkVGX0tJTkRfVU5TUEVDSUZJRUQQABIYChRCT0FSRF9SRUZfS0lORF9JU1NVRRABEh8KG0JPQVJEX1JFRl9LSU5EX1BVTExfUkVRVUVTVBACKqADCgtCb2FyZFByR2F0ZRIdChlCT0FSRF9QUl9HQVRFX1VOU1BFQ0lGSUVEEAASFwoTQk9BUkRfUFJfR0FURV9EUkFGVBABEiAKHEJPQVJEX1BSX0dBVEVfQ0hFQ0tTX1BFTkRJTkcQAhIfChtCT0FSRF9QUl9HQVRFX0NIRUNLU19GQUlMRUQQAxIjCh9CT0FSRF9QUl9HQVRFX0NIQU5HRVNfUkVRVUVTVEVEEAQSIQodQk9BUkRfUFJfR0FURV9BV0FJVElOR19SRVZJRVcQBRIiCh5CT0FSRF9QUl9HQVRFX0FXQUlUSU5HX0VOUVVFVUUQBhIYChRCT0FSRF9QUl9HQVRFX01FUkdFRBAHEhgKFEJPQVJEX1BSX0dBVEVfQ0xPU0VEEAgSGAoUQk9BUkRfUFJfR0FURV9RVUVVRUQQCRIeChpCT0FSRF9QUl9HQVRFX1FVRVVFX0ZBSUxFRBAKEh8KG0JPQVJEX1BSX0dBVEVfUVVFVUVfUkVNT1ZFRBALEhsKF0JPQVJEX1BSX0dBVEVfQ09ORkxJQ1RTEAwqkgEKEEJvYXJkTWlycm9yU3RhdGUSIgoeQk9BUkRfTUlSUk9SX1NUQVRFX1VOU1BFQ0lGSUVEEAASHgoaQk9BUkRfTUlSUk9SX1NUQVRFX1BFTkRJTkcQARIbChdCT0FSRF9NSVJST1JfU1RBVEVfRE9ORRACEh0KGUJPQVJEX01JUlJPUl9TVEFURV9GQUlMRUQQAyqRAQoMUGVlcmluZ1N0YXRlEh0KGVBFRVJJTkdfU1RBVEVfVU5TUEVDSUZJRUQQABIiCh5QRUVSSU5HX1NUQVRFX1BFTkRJTkdfT1VUR09JTkcQARIiCh5QRUVSSU5HX1NUQVRFX1BFTkRJTkdfSU5DT01JTkcQAhIaChZQRUVSSU5HX1NUQVRFX0FQUFJPVkVEEAMqXgoLUm9zdGVyU2NvcGUSHQoZUk9TVEVSX1NDT1BFX05FSUdIQk9SSE9PRBAAEhgKFFJPU1RFUl9TQ09QRV9TVUJUUkVFEAESFgoSUk9TVEVSX1NDT1BFX09XTkVSEAIywBEKDENvbW1zU2VydmljZRJLCgpDcmVhdGVVc2VyEh0uY29tcGFzcy52MS5DcmVhdGVVc2VyUmVxdWVzdBoeLmNvbXBhc3MudjEuQ3JlYXRlVXNlclJlc3BvbnNlEk4KC0NyZWF0ZUFnZW50Eh4uY29tcGFzcy52MS5DcmVhdGVBZ2VudFJlcXVlc3QaHy5jb21wYXNzLnYxLkNyZWF0ZUFnZW50UmVzcG9uc2USUQoMTGlzdEFjY291bnRzEh8uY29tcGFzcy52MS5MaXN0QWNjb3VudHNSZXF1ZXN0GiAuY29tcGFzcy52MS5MaXN0QWNjb3VudHNSZXNwb25zZRJjChJDcmVhdGVDaGFubmVsR3JvdXASJS5jb21wYXNzLnYxLkNyZWF0ZUNoYW5uZWxHcm91cFJlcXVlc3QaJi5jb21wYXNzLnYxLkNyZWF0ZUNoYW5uZWxHcm91cFJlc3BvbnNlEmAKEUxpc3RDaGFubmVsR3JvdXBzEiQuY29tcGFzcy52MS5MaXN0Q2hhbm5lbEdyb3Vwc1JlcXVlc3QaJS5jb21wYXNzLnYxLkxpc3RDaGFubmVsR3JvdXBzUmVzcG9uc2USUQoMTGlzdENoYW5uZWxzEh8uY29tcGFzcy52MS5MaXN0Q2hhbm5lbHNSZXF1ZXN0GiAuY29tcGFzcy52MS5MaXN0Q2hhbm5lbHNSZXNwb25zZRJUCg1DcmVhdGVDaGFubmVsEiAuY29tcGFzcy52MS5DcmVhdGVDaGFubmVsUmVxdWVzdBohLmNvbXBhc3MudjEuQ3JlYXRlQ2hhbm5lbFJlc3BvbnNlEmkKFFVwZGF0ZUNoYW5uZWxNZW1iZXJzEicuY29tcGFzcy52MS5VcGRhdGVDaGFubmVsTWVtYmVyc1JlcXVlc3QaKC5jb21wYXNzLnYxLlVwZGF0ZUNoYW5uZWxNZW1iZXJzUmVzcG9uc2USPwoGT3BlbkRNEhkuY29tcGFzcy52MS5PcGVuRE1SZXF1ZXN0GhouY29tcGFzcy52MS5PcGVuRE1SZXNwb25zZRJUCg1SZXBhcmVudEFnZW50EiAuY29tcGFzcy52MS5SZXBhcmVudEFnZW50UmVxdWVzdBohLmNvbXBhc3MudjEuUmVwYXJlbnRBZ2VudFJlc3BvbnNlEloKD1JlcGFyZW50Q2hhbm5lbBIiLmNvbXBhc3MudjEuUmVwYXJlbnRDaGFubmVsUmVxdWVzdBojLmNvbXBhc3MudjEuUmVwYXJlbnRDaGFubmVsUmVzcG9uc2USYwoST3BlbkFnZW50V29ya3NwYWNlEiUuY29tcGFzcy52MS5PcGVuQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0GiYuY29tcGFzcy52MS5PcGVuQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJRCgxMaXN0TWVzc2FnZXMSHy5jb21wYXNzLnYxLkxpc3RNZXNzYWdlc1JlcXVlc3QaIC5jb21wYXNzLnYxLkxpc3RNZXNzYWdlc1Jlc3BvbnNlEk4KC1Bvc3RNZXNzYWdlEh4uY29tcGFzcy52MS5Qb3N0TWVzc2FnZVJlcXVlc3QaHy5jb21wYXNzLnYxLlBvc3RNZXNzYWdlUmVzcG9uc2USSwoKTGlzdFRvcGljcxIdLmNvbXBhc3MudjEuTGlzdFRvcGljc1JlcXVlc3QaHi5jb21wYXNzLnYxLkxpc3RUb3BpY3NSZXNwb25zZRJOCgtVcGRhdGVUb3BpYxIeLmNvbXBhc3MudjEuVXBkYXRlVG9waWNSZXF1ZXN0Gh8uY29tcGFzcy52MS5VcGRhdGVUb3BpY1Jlc3BvbnNlElEKDFJlc3BvbmRUb0FzaxIfLmNvbXBhc3MudjEuUmVzcG9uZFRvQXNrUmVxdWVzdBogLmNvbXBhc3MudjEuUmVzcG9uZFRvQXNrUmVzcG9uc2USVAoNTWlycm9yVG9Gb3JnZRIgLmNvbXBhc3MudjEuTWlycm9yVG9Gb3JnZVJlcXVlc3QaIS5jb21wYXNzLnYxLk1pcnJvclRvRm9yZ2VSZXNwb25zZRJXCg5TZWFyY2hNZXNzYWdlcxIhLmNvbXBhc3MudjEuU2VhcmNoTWVzc2FnZXNSZXF1ZXN0GiIuY29tcGFzcy52MS5TZWFyY2hNZXNzYWdlc1Jlc3BvbnNlEl0KEFNldENoYW5uZWxQb2xpY3kSIy5jb21wYXNzLnYxLlNldENoYW5uZWxQb2xpY3lSZXF1ZXN0GiQuY29tcGFzcy52MS5TZXRDaGFubmVsUG9saWN5UmVzcG9uc2USSAoJR2V0Um9zdGVyEhwuY29tcGFzcy52MS5HZXRSb3N0ZXJSZXF1ZXN0Gh0uY29tcGFzcy52MS5HZXRSb3N0ZXJSZXNwb25zZRJgChFVcGRhdGVQaW5uZWRCb2FyZBIkLmNvbXBhc3MudjEuVXBkYXRlUGlubmVkQm9hcmRSZXF1ZXN0GiUuY29tcGFzcy52MS5VcGRhdGVQaW5uZWRCb2FyZFJlc3BvbnNlEk4KC0FwcHJvdmVQZWVyEh4uY29tcGFzcy52MS5BcHByb3ZlUGVlclJlcXVlc3QaHy5jb21wYXNzLnYxLkFwcHJvdmVQZWVyUmVzcG9uc2USSwoKUmV2b2tlUGVlchIdLmNvbXBhc3MudjEuUmV2b2tlUGVlclJlcXVlc3QaHi5jb21wYXNzLnYxLlJldm9rZVBlZXJSZXNwb25zZRJICglMaXN0UGVlcnMSHC5jb21wYXNzLnYxLkxpc3RQZWVyc1JlcXVlc3QaHS5jb21wYXNzLnYxLkxpc3RQZWVyc1Jlc3BvbnNlElkKDlN1YnNjcmliZUNvbW1zEiEuY29tcGFzcy52MS5TdWJzY3JpYmVDb21tc1JlcXVlc3QaIi5jb21wYXNzLnYxLlN1YnNjcmliZUNvbW1zUmVzcG9uc2UwAWIGcHJvdG8z", [file_compass_v1_compass]); /** * An account in the communication layer: a human user or an owned agent. Both @@ -1690,6 +1690,20 @@ export type BoardAsk = Message$1<"compass.v1.BoardAsk"> & { * @generated from field: int64 posted_at_unix_ms = 7; */ postedAtUnixMs: bigint; + + /** + * Set by the board read to report this ask's current forge delivery state. + * + * @generated from field: compass.v1.BoardMirrorState mirror_state = 8; + */ + mirrorState: BoardMirrorState; + + /** + * Set by the board read when the forge comment was delivered. + * + * @generated from field: string mirror_url = 9; + */ + mirrorUrl: string; }; /** @@ -1699,6 +1713,76 @@ export type BoardAsk = Message$1<"compass.v1.BoardAsk"> & { export const BoardAskSchema: GenMessage = /*@__PURE__*/ messageDesc(file_compass_v1_comms, 33); +/** + * Request to mirror a visible ask, topic, or message set to an allowed forge ref on the calling user's behalf. + * + * @generated from message compass.v1.MirrorToForgeRequest + */ +export type MirrorToForgeRequest = Message$1<"compass.v1.MirrorToForgeRequest"> & { + /** + * The issue or pull request destination. + * + * @generated from field: string ref = 1; + */ + ref: string; + + /** + * The ask to mirror; mutually exclusive with the other source fields. + * + * @generated from field: string ask_id = 2; + */ + askId: string; + + /** + * The topic to mirror; mutually exclusive with the other source fields. + * + * @generated from field: string topic_id = 3; + */ + topicId: string; + + /** + * One to fifty message IDs from channel_id, mutually exclusive with other sources. + * + * @generated from field: repeated string message_ids = 4; + */ + messageIds: string[]; + + /** + * The visible channel containing the chosen source. + * + * @generated from field: string channel_id = 5; + */ + channelId: string; +}; + +/** + * Describes the message compass.v1.MirrorToForgeRequest. + * Use `create(MirrorToForgeRequestSchema)` to create a new message. + */ +export const MirrorToForgeRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_comms, 34); + +/** + * The forge comment created or updated for the requested source. + * + * @generated from message compass.v1.MirrorToForgeResponse + */ +export type MirrorToForgeResponse = Message$1<"compass.v1.MirrorToForgeResponse"> & { + /** + * The URL of the resulting forge comment. + * + * @generated from field: string comment_url = 1; + */ + commentUrl: string; +}; + +/** + * Describes the message compass.v1.MirrorToForgeResponse. + * Use `create(MirrorToForgeResponseSchema)` to create a new message. + */ +export const MirrorToForgeResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_comms, 35); + /** * The current board state for one agent. * @@ -1795,7 +1879,7 @@ export type AgentBoard = Message$1<"compass.v1.AgentBoard"> & { * Use `create(AgentBoardSchema)` to create a new message. */ export const AgentBoardSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 34); + messageDesc(file_compass_v1_comms, 36); /** * The compact board summary attached to a roster entry. @@ -1851,7 +1935,7 @@ export type AgentBoardSummary = Message$1<"compass.v1.AgentBoardSummary"> & { * Use `create(AgentBoardSummarySchema)` to create a new message. */ export const AgentBoardSummarySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 35); + messageDesc(file_compass_v1_comms, 37); /** * A request to apply task changes and board metadata for the caller's agent. @@ -1914,7 +1998,7 @@ export type UpdateAgentBoardRequest = Message$1<"compass.v1.UpdateAgentBoardRequ * Use `create(UpdateAgentBoardRequestSchema)` to create a new message. */ export const UpdateAgentBoardRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 36); + messageDesc(file_compass_v1_comms, 38); /** * The board after an update. @@ -1935,7 +2019,7 @@ export type UpdateAgentBoardResponse = Message$1<"compass.v1.UpdateAgentBoardRes * Use `create(UpdateAgentBoardResponseSchema)` to create a new message. */ export const UpdateAgentBoardResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 37); + messageDesc(file_compass_v1_comms, 39); /** * Filters for a visible agent board listing. @@ -1998,7 +2082,7 @@ export type ListAgentBoardsRequest = Message$1<"compass.v1.ListAgentBoardsReques * Use `create(ListAgentBoardsRequestSchema)` to create a new message. */ export const ListAgentBoardsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 38); + messageDesc(file_compass_v1_comms, 40); /** * Agent boards matching a listing request. @@ -2019,7 +2103,7 @@ export type ListAgentBoardsResponse = Message$1<"compass.v1.ListAgentBoardsRespo * Use `create(ListAgentBoardsResponseSchema)` to create a new message. */ export const ListAgentBoardsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 39); + messageDesc(file_compass_v1_comms, 41); /** * A live agent-board update; the event's board has no asks. @@ -2047,7 +2131,7 @@ export type AgentBoardChanged = Message$1<"compass.v1.AgentBoardChanged"> & { * Use `create(AgentBoardChangedSchema)` to create a new message. */ export const AgentBoardChangedSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 40); + messageDesc(file_compass_v1_comms, 42); /** * @generated from message compass.v1.Peering @@ -2074,7 +2158,7 @@ export type Peering = Message$1<"compass.v1.Peering"> & { * Use `create(PeeringSchema)` to create a new message. */ export const PeeringSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 41); + messageDesc(file_compass_v1_comms, 43); /** * @generated from message compass.v1.ApprovePeerRequest @@ -2091,7 +2175,7 @@ export type ApprovePeerRequest = Message$1<"compass.v1.ApprovePeerRequest"> & { * Use `create(ApprovePeerRequestSchema)` to create a new message. */ export const ApprovePeerRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 42); + messageDesc(file_compass_v1_comms, 44); /** * @generated from message compass.v1.ApprovePeerResponse @@ -2108,7 +2192,7 @@ export type ApprovePeerResponse = Message$1<"compass.v1.ApprovePeerResponse"> & * Use `create(ApprovePeerResponseSchema)` to create a new message. */ export const ApprovePeerResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 43); + messageDesc(file_compass_v1_comms, 45); /** * @generated from message compass.v1.RevokePeerRequest @@ -2125,7 +2209,7 @@ export type RevokePeerRequest = Message$1<"compass.v1.RevokePeerRequest"> & { * Use `create(RevokePeerRequestSchema)` to create a new message. */ export const RevokePeerRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 44); + messageDesc(file_compass_v1_comms, 46); /** * @generated from message compass.v1.RevokePeerResponse @@ -2142,7 +2226,7 @@ export type RevokePeerResponse = Message$1<"compass.v1.RevokePeerResponse"> & { * Use `create(RevokePeerResponseSchema)` to create a new message. */ export const RevokePeerResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 45); + messageDesc(file_compass_v1_comms, 47); /** * @generated from message compass.v1.ListPeersRequest @@ -2155,7 +2239,7 @@ export type ListPeersRequest = Message$1<"compass.v1.ListPeersRequest"> & { * Use `create(ListPeersRequestSchema)` to create a new message. */ export const ListPeersRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 46); + messageDesc(file_compass_v1_comms, 48); /** * @generated from message compass.v1.ListPeersResponse @@ -2172,7 +2256,7 @@ export type ListPeersResponse = Message$1<"compass.v1.ListPeersResponse"> & { * Use `create(ListPeersResponseSchema)` to create a new message. */ export const ListPeersResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 47); + messageDesc(file_compass_v1_comms, 49); /** * @generated from message compass.v1.CreateUserRequest @@ -2194,7 +2278,7 @@ export type CreateUserRequest = Message$1<"compass.v1.CreateUserRequest"> & { * Use `create(CreateUserRequestSchema)` to create a new message. */ export const CreateUserRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 48); + messageDesc(file_compass_v1_comms, 50); /** * @generated from message compass.v1.CreateUserResponse @@ -2211,7 +2295,7 @@ export type CreateUserResponse = Message$1<"compass.v1.CreateUserResponse"> & { * Use `create(CreateUserResponseSchema)` to create a new message. */ export const CreateUserResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 49); + messageDesc(file_compass_v1_comms, 51); /** * @generated from message compass.v1.CreateAgentRequest @@ -2257,7 +2341,7 @@ export type CreateAgentRequest = Message$1<"compass.v1.CreateAgentRequest"> & { * Use `create(CreateAgentRequestSchema)` to create a new message. */ export const CreateAgentRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 50); + messageDesc(file_compass_v1_comms, 52); /** * @generated from message compass.v1.CreateAgentResponse @@ -2274,7 +2358,7 @@ export type CreateAgentResponse = Message$1<"compass.v1.CreateAgentResponse"> & * Use `create(CreateAgentResponseSchema)` to create a new message. */ export const CreateAgentResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 51); + messageDesc(file_compass_v1_comms, 53); /** * @generated from message compass.v1.ListAccountsRequest @@ -2294,7 +2378,7 @@ export type ListAccountsRequest = Message$1<"compass.v1.ListAccountsRequest"> & * Use `create(ListAccountsRequestSchema)` to create a new message. */ export const ListAccountsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 52); + messageDesc(file_compass_v1_comms, 54); /** * @generated from message compass.v1.ListAccountsResponse @@ -2311,7 +2395,7 @@ export type ListAccountsResponse = Message$1<"compass.v1.ListAccountsResponse"> * Use `create(ListAccountsResponseSchema)` to create a new message. */ export const ListAccountsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 53); + messageDesc(file_compass_v1_comms, 55); /** * @generated from message compass.v1.CreateChannelGroupRequest @@ -2356,7 +2440,7 @@ export type CreateChannelGroupRequest = Message$1<"compass.v1.CreateChannelGroup * Use `create(CreateChannelGroupRequestSchema)` to create a new message. */ export const CreateChannelGroupRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 54); + messageDesc(file_compass_v1_comms, 56); /** * @generated from message compass.v1.CreateChannelGroupResponse @@ -2373,7 +2457,7 @@ export type CreateChannelGroupResponse = Message$1<"compass.v1.CreateChannelGrou * Use `create(CreateChannelGroupResponseSchema)` to create a new message. */ export const CreateChannelGroupResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 55); + messageDesc(file_compass_v1_comms, 57); /** * @generated from message compass.v1.ListChannelGroupsRequest @@ -2393,7 +2477,7 @@ export type ListChannelGroupsRequest = Message$1<"compass.v1.ListChannelGroupsRe * Use `create(ListChannelGroupsRequestSchema)` to create a new message. */ export const ListChannelGroupsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 56); + messageDesc(file_compass_v1_comms, 58); /** * @generated from message compass.v1.ListChannelGroupsResponse @@ -2410,7 +2494,7 @@ export type ListChannelGroupsResponse = Message$1<"compass.v1.ListChannelGroupsR * Use `create(ListChannelGroupsResponseSchema)` to create a new message. */ export const ListChannelGroupsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 57); + messageDesc(file_compass_v1_comms, 59); /** * @generated from message compass.v1.ListChannelsRequest @@ -2430,7 +2514,7 @@ export type ListChannelsRequest = Message$1<"compass.v1.ListChannelsRequest"> & * Use `create(ListChannelsRequestSchema)` to create a new message. */ export const ListChannelsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 58); + messageDesc(file_compass_v1_comms, 60); /** * @generated from message compass.v1.ListChannelsResponse @@ -2447,7 +2531,7 @@ export type ListChannelsResponse = Message$1<"compass.v1.ListChannelsResponse"> * Use `create(ListChannelsResponseSchema)` to create a new message. */ export const ListChannelsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 59); + messageDesc(file_compass_v1_comms, 61); /** * @generated from message compass.v1.CreateChannelRequest @@ -2515,7 +2599,7 @@ export type CreateChannelRequest = Message$1<"compass.v1.CreateChannelRequest"> * Use `create(CreateChannelRequestSchema)` to create a new message. */ export const CreateChannelRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 60); + messageDesc(file_compass_v1_comms, 62); /** * @generated from message compass.v1.CreateChannelResponse @@ -2532,7 +2616,7 @@ export type CreateChannelResponse = Message$1<"compass.v1.CreateChannelResponse" * Use `create(CreateChannelResponseSchema)` to create a new message. */ export const CreateChannelResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 61); + messageDesc(file_compass_v1_comms, 63); /** * @generated from message compass.v1.UpdateChannelMembersRequest @@ -2596,7 +2680,7 @@ export type UpdateChannelMembersRequest = Message$1<"compass.v1.UpdateChannelMem * Use `create(UpdateChannelMembersRequestSchema)` to create a new message. */ export const UpdateChannelMembersRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 62); + messageDesc(file_compass_v1_comms, 64); /** * @generated from message compass.v1.UpdateChannelMembersResponse @@ -2613,7 +2697,7 @@ export type UpdateChannelMembersResponse = Message$1<"compass.v1.UpdateChannelMe * Use `create(UpdateChannelMembersResponseSchema)` to create a new message. */ export const UpdateChannelMembersResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 63); + messageDesc(file_compass_v1_comms, 65); /** * @generated from message compass.v1.OpenDMRequest @@ -2634,7 +2718,7 @@ export type OpenDMRequest = Message$1<"compass.v1.OpenDMRequest"> & { * Use `create(OpenDMRequestSchema)` to create a new message. */ export const OpenDMRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 64); + messageDesc(file_compass_v1_comms, 66); /** * @generated from message compass.v1.OpenDMResponse @@ -2660,7 +2744,7 @@ export type OpenDMResponse = Message$1<"compass.v1.OpenDMResponse"> & { * Use `create(OpenDMResponseSchema)` to create a new message. */ export const OpenDMResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 65); + messageDesc(file_compass_v1_comms, 67); /** * @generated from message compass.v1.ReparentAgentRequest @@ -2688,7 +2772,7 @@ export type ReparentAgentRequest = Message$1<"compass.v1.ReparentAgentRequest"> * Use `create(ReparentAgentRequestSchema)` to create a new message. */ export const ReparentAgentRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 66); + messageDesc(file_compass_v1_comms, 68); /** * @generated from message compass.v1.ReparentAgentResponse @@ -2705,7 +2789,7 @@ export type ReparentAgentResponse = Message$1<"compass.v1.ReparentAgentResponse" * Use `create(ReparentAgentResponseSchema)` to create a new message. */ export const ReparentAgentResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 67); + messageDesc(file_compass_v1_comms, 69); /** * @generated from message compass.v1.ReparentChannelRequest @@ -2730,7 +2814,7 @@ export type ReparentChannelRequest = Message$1<"compass.v1.ReparentChannelReques * Use `create(ReparentChannelRequestSchema)` to create a new message. */ export const ReparentChannelRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 68); + messageDesc(file_compass_v1_comms, 70); /** * @generated from message compass.v1.ReparentChannelResponse @@ -2747,7 +2831,7 @@ export type ReparentChannelResponse = Message$1<"compass.v1.ReparentChannelRespo * Use `create(ReparentChannelResponseSchema)` to create a new message. */ export const ReparentChannelResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 69); + messageDesc(file_compass_v1_comms, 71); /** * @generated from message compass.v1.SetChannelPolicyRequest @@ -2788,7 +2872,7 @@ export type SetChannelPolicyRequest = Message$1<"compass.v1.SetChannelPolicyRequ * Use `create(SetChannelPolicyRequestSchema)` to create a new message. */ export const SetChannelPolicyRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 70); + messageDesc(file_compass_v1_comms, 72); /** * @generated from message compass.v1.SetChannelPolicyResponse @@ -2807,7 +2891,7 @@ export type SetChannelPolicyResponse = Message$1<"compass.v1.SetChannelPolicyRes * Use `create(SetChannelPolicyResponseSchema)` to create a new message. */ export const SetChannelPolicyResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 71); + messageDesc(file_compass_v1_comms, 73); /** * @generated from message compass.v1.GetRosterRequest @@ -2835,7 +2919,7 @@ export type GetRosterRequest = Message$1<"compass.v1.GetRosterRequest"> & { * Use `create(GetRosterRequestSchema)` to create a new message. */ export const GetRosterRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 72); + messageDesc(file_compass_v1_comms, 74); /** * @generated from message compass.v1.GetRosterResponse @@ -2852,7 +2936,7 @@ export type GetRosterResponse = Message$1<"compass.v1.GetRosterResponse"> & { * Use `create(GetRosterResponseSchema)` to create a new message. */ export const GetRosterResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 73); + messageDesc(file_compass_v1_comms, 75); /** * One agent in a roster: its identity, tree position, and joined presence + @@ -2909,7 +2993,7 @@ export type RosterEntry = Message$1<"compass.v1.RosterEntry"> & { * Use `create(RosterEntrySchema)` to create a new message. */ export const RosterEntrySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 74); + messageDesc(file_compass_v1_comms, 76); /** * @generated from message compass.v1.UpdatePinnedBoardRequest @@ -2949,7 +3033,7 @@ export type UpdatePinnedBoardRequest = Message$1<"compass.v1.UpdatePinnedBoardRe * Use `create(UpdatePinnedBoardRequestSchema)` to create a new message. */ export const UpdatePinnedBoardRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 75); + messageDesc(file_compass_v1_comms, 77); /** * Pin an existing message. message_id must already live in a topic of the @@ -2975,7 +3059,7 @@ export type PinMessage = Message$1<"compass.v1.PinMessage"> & { * Use `create(PinMessageSchema)` to create a new message. */ export const PinMessageSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 76); + messageDesc(file_compass_v1_comms, 78); /** * @generated from message compass.v1.UpdatePinnedBoardResponse @@ -2994,7 +3078,7 @@ export type UpdatePinnedBoardResponse = Message$1<"compass.v1.UpdatePinnedBoardR * Use `create(UpdatePinnedBoardResponseSchema)` to create a new message. */ export const UpdatePinnedBoardResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 77); + messageDesc(file_compass_v1_comms, 79); /** * @generated from message compass.v1.OpenAgentWorkspaceRequest @@ -3014,7 +3098,7 @@ export type OpenAgentWorkspaceRequest = Message$1<"compass.v1.OpenAgentWorkspace * Use `create(OpenAgentWorkspaceRequestSchema)` to create a new message. */ export const OpenAgentWorkspaceRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 78); + messageDesc(file_compass_v1_comms, 80); /** * @generated from message compass.v1.OpenAgentWorkspaceResponse @@ -3031,7 +3115,7 @@ export type OpenAgentWorkspaceResponse = Message$1<"compass.v1.OpenAgentWorkspac * Use `create(OpenAgentWorkspaceResponseSchema)` to create a new message. */ export const OpenAgentWorkspaceResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 79); + messageDesc(file_compass_v1_comms, 81); /** * @generated from message compass.v1.ListMessagesRequest @@ -3087,7 +3171,7 @@ export type ListMessagesRequest = Message$1<"compass.v1.ListMessagesRequest"> & * Use `create(ListMessagesRequestSchema)` to create a new message. */ export const ListMessagesRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 80); + messageDesc(file_compass_v1_comms, 82); /** * @generated from message compass.v1.ListMessagesResponse @@ -3106,7 +3190,7 @@ export type ListMessagesResponse = Message$1<"compass.v1.ListMessagesResponse"> * Use `create(ListMessagesResponseSchema)` to create a new message. */ export const ListMessagesResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 81); + messageDesc(file_compass_v1_comms, 83); /** * @generated from message compass.v1.PostMessageRequest @@ -3189,7 +3273,7 @@ export type PostMessageRequest = Message$1<"compass.v1.PostMessageRequest"> & { * Use `create(PostMessageRequestSchema)` to create a new message. */ export const PostMessageRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 82); + messageDesc(file_compass_v1_comms, 84); /** * @generated from message compass.v1.PostMessageResponse @@ -3206,7 +3290,7 @@ export type PostMessageResponse = Message$1<"compass.v1.PostMessageResponse"> & * Use `create(PostMessageResponseSchema)` to create a new message. */ export const PostMessageResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 83); + messageDesc(file_compass_v1_comms, 85); /** * @generated from message compass.v1.ListTopicsRequest @@ -3232,7 +3316,7 @@ export type ListTopicsRequest = Message$1<"compass.v1.ListTopicsRequest"> & { * Use `create(ListTopicsRequestSchema)` to create a new message. */ export const ListTopicsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 84); + messageDesc(file_compass_v1_comms, 86); /** * @generated from message compass.v1.ListTopicsResponse @@ -3249,7 +3333,7 @@ export type ListTopicsResponse = Message$1<"compass.v1.ListTopicsResponse"> & { * Use `create(ListTopicsResponseSchema)` to create a new message. */ export const ListTopicsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 85); + messageDesc(file_compass_v1_comms, 87); /** * @generated from message compass.v1.UpdateTopicRequest @@ -3283,7 +3367,7 @@ export type UpdateTopicRequest = Message$1<"compass.v1.UpdateTopicRequest"> & { * Use `create(UpdateTopicRequestSchema)` to create a new message. */ export const UpdateTopicRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 86); + messageDesc(file_compass_v1_comms, 88); /** * @generated from message compass.v1.UpdateTopicResponse @@ -3300,7 +3384,7 @@ export type UpdateTopicResponse = Message$1<"compass.v1.UpdateTopicResponse"> & * Use `create(UpdateTopicResponseSchema)` to create a new message. */ export const UpdateTopicResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 87); + messageDesc(file_compass_v1_comms, 89); /** * @generated from message compass.v1.RespondToAskRequest @@ -3326,7 +3410,7 @@ export type RespondToAskRequest = Message$1<"compass.v1.RespondToAskRequest"> & * Use `create(RespondToAskRequestSchema)` to create a new message. */ export const RespondToAskRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 88); + messageDesc(file_compass_v1_comms, 90); /** * The answer to one AskQuestion. @@ -3360,7 +3444,7 @@ export type AskQuestionAnswer = Message$1<"compass.v1.AskQuestionAnswer"> & { * Use `create(AskQuestionAnswerSchema)` to create a new message. */ export const AskQuestionAnswerSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 89); + messageDesc(file_compass_v1_comms, 91); /** * @generated from message compass.v1.RespondToAskResponse @@ -3373,7 +3457,7 @@ export type RespondToAskResponse = Message$1<"compass.v1.RespondToAskResponse"> * Use `create(RespondToAskResponseSchema)` to create a new message. */ export const RespondToAskResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 90); + messageDesc(file_compass_v1_comms, 92); /** * @generated from message compass.v1.SearchMessagesRequest @@ -3419,7 +3503,7 @@ export type SearchMessagesRequest = Message$1<"compass.v1.SearchMessagesRequest" * Use `create(SearchMessagesRequestSchema)` to create a new message. */ export const SearchMessagesRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 91); + messageDesc(file_compass_v1_comms, 93); /** * @generated from message compass.v1.SearchMessagesResponse @@ -3438,7 +3522,7 @@ export type SearchMessagesResponse = Message$1<"compass.v1.SearchMessagesRespons * Use `create(SearchMessagesResponseSchema)` to create a new message. */ export const SearchMessagesResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 92); + messageDesc(file_compass_v1_comms, 94); /** * @generated from message compass.v1.SubscribeCommsRequest @@ -3466,7 +3550,7 @@ export type SubscribeCommsRequest = Message$1<"compass.v1.SubscribeCommsRequest" * Use `create(SubscribeCommsRequestSchema)` to create a new message. */ export const SubscribeCommsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 93); + messageDesc(file_compass_v1_comms, 95); /** * The user permission roles, expandable later. A regular member is the default @@ -3926,6 +4010,47 @@ export enum BoardPrGate { export const BoardPrGateSchema: GenEnum = /*@__PURE__*/ enumDesc(file_compass_v1_comms, 10); +/** + * Delivery state of an ask mirror outbox row. + * + * @generated from enum compass.v1.BoardMirrorState + */ +export enum BoardMirrorState { + /** + * No known mirror state was supplied. + * + * @generated from enum value: BOARD_MIRROR_STATE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * The comment is waiting for delivery or retry. + * + * @generated from enum value: BOARD_MIRROR_STATE_PENDING = 1; + */ + PENDING = 1, + + /** + * The forge comment was delivered. + * + * @generated from enum value: BOARD_MIRROR_STATE_DONE = 2; + */ + DONE = 2, + + /** + * Delivery was refused or exhausted. + * + * @generated from enum value: BOARD_MIRROR_STATE_FAILED = 3; + */ + FAILED = 3, +} + +/** + * Describes the enum compass.v1.BoardMirrorState. + */ +export const BoardMirrorStateSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_compass_v1_comms, 11); + /** * @generated from enum compass.v1.PeeringState */ @@ -3955,7 +4080,7 @@ export enum PeeringState { * Describes the enum compass.v1.PeeringState. */ export const PeeringStateSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_comms, 11); + enumDesc(file_compass_v1_comms, 12); /** * The scope of a roster read, relative to a vantage agent. @@ -3989,7 +4114,7 @@ export enum RosterScope { * Describes the enum compass.v1.RosterScope. */ export const RosterScopeSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_comms, 12); + enumDesc(file_compass_v1_comms, 13); /** * The communication-layer service: accounts, channel groups + channels, @@ -4205,6 +4330,17 @@ export const CommsService: GenService<{ input: typeof RespondToAskRequestSchema; output: typeof RespondToAskResponseSchema; }, + /** + * Mirror a visible ask, topic, or message set to an allowed forge ref on the + * calling user's behalf. + * + * @generated from rpc compass.v1.CommsService.MirrorToForge + */ + mirrorToForge: { + methodKind: "unary"; + input: typeof MirrorToForgeRequestSchema; + output: typeof MirrorToForgeResponseSchema; + }, /** * Full-text search across the messages the caller may see — the audit/search * property served from the store of record (D1, D12). Results are scoped to diff --git a/packages/compass-agent/src/gen/compass/v1/compass_pb.ts b/packages/compass-agent/src/gen/compass/v1/compass_pb.ts index 61258647a..110ef11e7 100644 --- a/packages/compass-agent/src/gen/compass/v1/compass_pb.ts +++ b/packages/compass-agent/src/gen/compass/v1/compass_pb.ts @@ -20,7 +20,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file compass/v1/compass.proto. */ export const file_compass_v1_compass: GenFile = /*@__PURE__*/ - fileDesc("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEiRwobR3JhbnRBZ2VudFJlcG9zaXRvcnlSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCRISCgpyZXBvc2l0b3J5GAIgASgJIi0KHEdyYW50QWdlbnRSZXBvc2l0b3J5UmVzcG9uc2USDQoFYWRkZWQYASABKAgiSAocUmV2b2tlQWdlbnRSZXBvc2l0b3J5UmVxdWVzdBIUCgxhZ2VudF9oYW5kbGUYASABKAkSEgoKcmVwb3NpdG9yeRgCIAEoCSIwCh1SZXZva2VBZ2VudFJlcG9zaXRvcnlSZXNwb25zZRIPCgdyZW1vdmVkGAEgASgIIjQKHExpc3RBZ2VudFJlcG9zaXRvcmllc1JlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJIjUKHUxpc3RBZ2VudFJlcG9zaXRvcmllc1Jlc3BvbnNlEhQKDHJlcG9zaXRvcmllcxgBIAMoCSLQAQoQU2V0U2VjcmV0UmVxdWVzdBIMCgRuYW1lGAEgASgJEhIKBXZhbHVlGAIgASgJQgOAAQESLAoIZGVsaXZlcnkYAyABKA4yGi5jb21wYXNzLnYxLlNlY3JldERlbGl2ZXJ5EiQKBGtpbmQYBCABKA4yFi5jb21wYXNzLnYxLlNlY3JldEtpbmQSEAoIcHJvdmlkZXIYBSABKAkSDAoEaG9zdBgGIAEoCRImCgVzY29wZRgHIAEoDjIXLmNvbXBhc3MudjEuU2VjcmV0U2NvcGUiEwoRU2V0U2VjcmV0UmVzcG9uc2UiFAoSTGlzdFNlY3JldHNSZXF1ZXN0IkAKE0xpc3RTZWNyZXRzUmVzcG9uc2USKQoHc2VjcmV0cxgBIAMoCzIYLmNvbXBhc3MudjEuU2VjcmV0U3RhdHVzIqABCgxTZWNyZXRTdGF0dXMSDAoEbmFtZRgBIAEoCRIOCgZpc19zZXQYAiABKAgSLAoIZGVsaXZlcnkYAyABKA4yGi5jb21wYXNzLnYxLlNlY3JldERlbGl2ZXJ5EiQKBGtpbmQYBCABKA4yFi5jb21wYXNzLnYxLlNlY3JldEtpbmQSEAoIcHJvdmlkZXIYBSABKAkSDAoEaG9zdBgGIAEoCSJLChNEZWxldGVTZWNyZXRSZXF1ZXN0EgwKBG5hbWUYASABKAkSJgoFc2NvcGUYAiABKA4yFy5jb21wYXNzLnYxLlNlY3JldFNjb3BlIhYKFERlbGV0ZVNlY3JldFJlc3BvbnNlIhoKGExpc3RTZXJ2ZXJTZWNyZXRzUmVxdWVzdCJTChlMaXN0U2VydmVyU2VjcmV0c1Jlc3BvbnNlEjYKDnNlcnZlcl9zZWNyZXRzGAEgAygLMh4uY29tcGFzcy52MS5TZXJ2ZXJTZWNyZXRTdGF0dXMiMgoSU2VydmVyU2VjcmV0U3RhdHVzEgwKBG5hbWUYASABKAkSDgoGaXNfc2V0GAIgASgIIhgKFkdldFJlZkxpbmtSdWxlc1JlcXVlc3QiQQoLUmVmTGlua1J1bGUSDAoEa2luZBgBIAEoCRIOCgZwcmVmaXgYAiABKAkSFAoMdXJsX3RlbXBsYXRlGAMgASgJIkEKF0dldFJlZkxpbmtSdWxlc1Jlc3BvbnNlEiYKBXJ1bGVzGAEgAygLMhcuY29tcGFzcy52MS5SZWZMaW5rUnVsZSIWChRHZXRTZXJ2ZXJJbmZvUmVxdWVzdCKAAQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSEwoLYXBpX3ZlcnNpb24YAiABKAkSCwoDcmV2GAMgASgJEjQKEGVucm9sbGVkX3J1bm5lcnMYBCADKAsyGi5jb21wYXNzLnYxLkVucm9sbGVkUnVubmVyIjAKDkVucm9sbGVkUnVubmVyEgoKAmlkGAEgASgJEhIKCmVucm9sbG1lbnQYAiABKAQiDwoNV2hvQW1JUmVxdWVzdCIkCg5XaG9BbUlSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJIhUKE0dldFRvdXJTdGF0ZVJlcXVlc3QiUQoUR2V0VG91clN0YXRlUmVzcG9uc2USKAoHb3V0Y29tZRgBIAEoDjIXLmNvbXBhc3MudjEuVG91ck91dGNvbWUSDwoHc3RlcF9pZBgCIAEoCSIoChVDbGFpbVRvdXJTdGFydFJlcXVlc3QSDwoHc3RlcF9pZBgBIAEoCSIpChZDbGFpbVRvdXJTdGFydFJlc3BvbnNlEg8KB2NsYWltZWQYASABKAgiUAoTU2V0VG91clN0YXRlUmVxdWVzdBIoCgdvdXRjb21lGAEgASgOMhcuY29tcGFzcy52MS5Ub3VyT3V0Y29tZRIPCgdzdGVwX2lkGAIgASgJIhYKFFNldFRvdXJTdGF0ZVJlc3BvbnNlIkMKFlN1YnNjcmliZUV2ZW50c1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEIuIDChdTdWJzY3JpYmVFdmVudHNSZXNwb25zZRILCgNzZXEYASABKAQSEgoKYXRfdW5peF9tcxgCIAEoAxIWCg5pbnN0YW5jZV9lcG9jaBgDIAEoBBIUCgxzbmFwc2hvdF9zZXEYBCABKAQSMQoNc2VydmVyX3N0YXR1cxgKIAEoCzIYLmNvbXBhc3MudjEuU2VydmVyU3RhdHVzSAASNQoPcmVzeW5jX3JlcXVpcmVkGAsgASgLMhouY29tcGFzcy52MS5SZXN5bmNSZXF1aXJlZEgAEj4KFGFnZW50X3Nlc3Npb25fc3RhdHVzGAwgASgLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXNIABI8ChNhZ2VudF9tZXNzYWdlX2NodW5rGA0gASgLMh0uY29tcGFzcy52MS5BZ2VudE1lc3NhZ2VDaHVua0gAEjQKD2FnZW50X3Rvb2xfY2FsbBgOIAEoCzIZLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbEgAEisKCmFnZW50X3BsYW4YDyABKAsyFS5jb21wYXNzLnYxLkFnZW50UGxhbkgAEiIKBWlzc3VlGBAgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAQgkKB3BheWxvYWQiLgoWTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPAoXTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSJJChNTZWFyY2hJc3N1ZXNSZXF1ZXN0Eg0KBXF1ZXJ5GAEgASgJEg0KBWxpbWl0GAIgASgNEhQKDHNuYXBzaG90X3NlcRgDIAEoBCI5ChRTZWFyY2hJc3N1ZXNSZXNwb25zZRIhCgZpc3N1ZXMYASADKAsyES5jb21wYXNzLnYxLklzc3VlIjYKDFNlcnZlclN0YXR1cxImCgVzdGF0ZRgBIAEoDjIXLmNvbXBhc3MudjEuU2VydmVyU3RhdGUiEAoOUmVzeW5jUmVxdWlyZWQi0gEKEkFnZW50U2Vzc2lvblN0YXR1cxISCgpzZXNzaW9uX2lkGAEgASgJEiwKBXN0YXRlGAIgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZRIYChBhZ2VudF9hY2NvdW50X2lkGAMgASgJEi0KDHJ1bnRpbWVfdGllchgEIAEoDjIXLmNvbXBhc3MudjEuUnVudGltZVRpZXISMQoOZWdyZXNzX3Bvc3R1cmUYBSABKA4yGS5jb21wYXNzLnYxLkVncmVzc1Bvc3R1cmUiSQoRQWdlbnRNZXNzYWdlQ2h1bmsSEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgR0ZXh0GAIgASgJEhIKCmlzX3Rob3VnaHQYAyABKAgieQoNQWdlbnRUb29sQ2FsbBISCgpzZXNzaW9uX2lkGAEgASgJEhQKDHRvb2xfY2FsbF9pZBgCIAEoCRINCgV0aXRsZRgDIAEoCRIvCgZzdGF0dXMYBCABKA4yHy5jb21wYXNzLnYxLkFnZW50VG9vbENhbGxTdGF0dXMiTAoJQWdlbnRQbGFuEhIKCnNlc3Npb25faWQYASABKAkSKwoHZW50cmllcxgCIAMoCzIaLmNvbXBhc3MudjEuQWdlbnRQbGFuRW50cnkiUwoOQWdlbnRQbGFuRW50cnkSDwoHY29udGVudBgBIAEoCRIwCgZzdGF0dXMYAiABKA4yIC5jb21wYXNzLnYxLkFnZW50UGxhbkVudHJ5U3RhdHVzIpkECgxTZXNzaW9uRXZlbnQSEAoIZXZlbnRfaWQYASABKAkSEgoKYXRfdW5peF9tcxgCIAEoAxI6Cg5hc3Npc3RhbnRfdGV4dBgDIAEoCzIgLmNvbXBhc3MudjEuU2Vzc2lvbkFzc2lzdGFudFRleHRIABIvCgh0aGlua2luZxgEIAEoCzIbLmNvbXBhc3MudjEuU2Vzc2lvblRoaW5raW5nSAASMAoJdG9vbF9jYWxsGAUgASgLMhsuY29tcGFzcy52MS5TZXNzaW9uVG9vbENhbGxIABI9ChB0b29sX2NhbGxfdXBkYXRlGAYgASgLMiEuY29tcGFzcy52MS5TZXNzaW9uVG9vbENhbGxVcGRhdGVIABInCgRwbGFuGAcgASgLMhcuY29tcGFzcy52MS5TZXNzaW9uUGxhbkgAEisKBm5vdGljZRgIIAEoCzIZLmNvbXBhc3MudjEuU2Vzc2lvbk5vdGljZUgAEjkKEXNlc3Npb25faW5qZWN0aW9uGAkgASgLMhwuY29tcGFzcy52MS5TZXNzaW9uSW5qZWN0aW9uSAASMQoNc2Vzc2lvbl9lcnJvchgKIAEoCzIYLmNvbXBhc3MudjEuU2Vzc2lvbkVycm9ySAASOAoNYmF0Y2hfcGVuZGluZxgLIAEoCzIfLmNvbXBhc3MudjEuU2Vzc2lvbkJhdGNoUGVuZGluZ0gAQgcKBWV2ZW50IjgKFFNlc3Npb25Bc3Npc3RhbnRUZXh0EgwKBHRleHQYASABKAkSEgoKbWVzc2FnZV9pZBgCIAEoCSIzCg9TZXNzaW9uVGhpbmtpbmcSDAoEdGV4dBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJImcKD1Nlc3Npb25Ub29sQ2FsbBIUCgx0b29sX2NhbGxfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSLwoGc3RhdHVzGAMgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIpoBChVTZXNzaW9uVG9vbENhbGxVcGRhdGUSFAoMdG9vbF9jYWxsX2lkGAEgASgJEi8KBnN0YXR1cxgCIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cxIOCgZvdXRwdXQYAyABKAkSKgoFZGlmZnMYBCADKAsyGy5jb21wYXNzLnYxLlNlc3Npb25GaWxlRGlmZiJVCg9TZXNzaW9uRmlsZURpZmYSDAoEcGF0aBgBIAEoCRIVCghvbGRfdGV4dBgCIAEoCUgAiAEBEhAKCG5ld190ZXh0GAMgASgJQgsKCV9vbGRfdGV4dCI6CgtTZXNzaW9uUGxhbhIrCgdlbnRyaWVzGAEgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSI5Cg1TZXNzaW9uTm90aWNlEgwKBHRleHQYASABKAkSEQoEbGluaxgCIAEoCUgAiAEBQgcKBV9saW5rIoMBChBTZXNzaW9uSW5qZWN0aW9uEjEKB29wX2tpbmQYASABKA4yIC5jb21wYXNzLnYxLlNlc3Npb25JbmplY3Rpb25LaW5kEhIKCm1lc3NhZ2VfaWQYAiABKAkSEwoLZnJvbV9oYW5kbGUYAyABKAkSEwoLdHJhY2VwYXJlbnQYBCABKAkiawoMU2Vzc2lvbkVycm9yEioKBGtpbmQYASABKA4yHC5jb21wYXNzLnYxLlNlc3Npb25FcnJvcktpbmQSDwoHbWVzc2FnZRgCIAEoCRITCgZzdGF0dXMYAyABKAVIAIgBAUIJCgdfc3RhdHVzIj4KE1Nlc3Npb25CYXRjaFBlbmRpbmcSDQoFY291bnQYASABKA0SGAoQZmlyZXNfYXRfdW5peF9tcxgCIAEoAyIyChxTdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkilQEKEUFnZW50U2Vzc2lvbkZyYW1lEhIKCnNlc3Npb25faWQYASABKAkSJwoFZXZlbnQYAiABKAsyGC5jb21wYXNzLnYxLlNlc3Npb25FdmVudBIsCgVzdGF0ZRgDIAEoDjIdLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdGUSFQoNdHVybl9zZXF1ZW5jZRgEIAEoBCJwCh5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJEg8KB3BlcnNvbmEYAyABKAkSDAoEcm9sZRgEIAEoCSI5Ch9Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEhYKDmNvbnRhaW5lcl9uYW1lGAEgASgJIlAKG1JlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgCIAEoCSIeChxSZW1vdmVBZ2VudFdvcmtzcGFjZVJlc3BvbnNlImMKGFN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFyZXN1bWVfc2Vzc2lvbl9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiLwoZU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIloKEVNwYXduQWdlbnRSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiQAoSU3Bhd25BZ2VudFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSFgoOY29udGFpbmVyX25hbWUYAiABKAkiLQoXU3RvcEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIaChhTdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2UiLAoWU2tpcEJhdGNoV2luZG93UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIhkKF1NraXBCYXRjaFdpbmRvd1Jlc3BvbnNlIi8KGVJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIwChpSZWxvYWRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIisKFUdldEFnZW50U3RhdHVzUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJInwKFkdldEFnZW50U3RhdHVzUmVzcG9uc2USMAoIc3RhdHVzZXMYASADKAsyHi5jb21wYXNzLnYxLkFnZW50U2Vzc2lvblN0YXR1cxIcCg9ydW5uZXJfZW5yb2xsZWQYAiABKAhIAIgBAUISChBfcnVubmVyX2Vucm9sbGVkIisKEUlzc3VlVG9rZW5SZXF1ZXN0EhYKDmFjY291bnRfaGFuZGxlGAEgASgJIiMKEklzc3VlVG9rZW5SZXNwb25zZRINCgV0b2tlbhgBIAEoCSJCChRSZW5hbWVBY2NvdW50UmVxdWVzdBIWCg5hY2NvdW50X2hhbmRsZRgBIAEoCRISCgpuZXdfaGFuZGxlGAIgASgJIjsKFVJlbmFtZUFjY291bnRSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJEg4KBmhhbmRsZRgCIAEoCSIoChJSZXZva2VUb2tlblJlcXVlc3QSEgoFdG9rZW4YASABKAlCA4ABASIVChNSZXZva2VUb2tlblJlc3BvbnNlIicKFVB1dEFnZW50Q29uZmlnUmVxdWVzdBIOCgZidW5kbGUYASABKAwiKQoWUHV0QWdlbnRDb25maWdSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJIhsKGUdldEFnZW50Q29uZmlnSW5mb1JlcXVlc3Qi2gEKGkdldEFnZW50Q29uZmlnSW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSDgoGc2tpbGxzGAIgAygJEhIKCmV4dGVuc2lvbnMYAyADKAkSEwoLbWNwX3NlcnZlcnMYBCADKAkSFAoMaGFzX3NldHRpbmdzGAUgASgIEhUKDWhhc19hZ2VudHNfbWQYBiABKAgSDQoFcnVsZXMYByADKAkSEQoJc3ViYWdlbnRzGAggAygJEhIKCmhhc19tb2RlbHMYCSABKAgSDwoHcHJvbXB0cxgKIAMoCSIaChhEZWxldGVBZ2VudENvbmZpZ1JlcXVlc3QiGwoZRGVsZXRlQWdlbnRDb25maWdSZXNwb25zZSI0Cg5Nb2RlbENhbmRpZGF0ZRIQCghwcm92aWRlchgBIAEoCRIQCghtb2RlbF9pZBgCIAEoCSJxCg1Nb2RlbE1ldGFkYXRhEhYKDmNvbnRleHRfd2luZG93GAEgASgDEhwKFGlucHV0X2Nvc3RfbWljcm9fdXNkGAIgASgDEh0KFW91dHB1dF9jb3N0X21pY3JvX3VzZBgDIAEoAxILCgNhcGkYBCABKAkihwEKEk1vZGVsUmVnaXN0cnlFbnRyeRIUCgxkaXNwbGF5X25hbWUYASABKAkSLgoKY2FuZGlkYXRlcxgCIAMoCzIaLmNvbXBhc3MudjEuTW9kZWxDYW5kaWRhdGUSKwoIbWV0YWRhdGEYAyABKAsyGS5jb21wYXNzLnYxLk1vZGVsTWV0YWRhdGEimAEKDU1vZGVsUmVnaXN0cnkSNwoHZW50cmllcxgBIAMoCzImLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeS5FbnRyaWVzRW50cnkaTgoMRW50cmllc0VudHJ5EgsKA2tleRgBIAEoCRItCgV2YWx1ZRgCIAEoCzIeLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeUVudHJ5OgI4ASJgChdQdXRNb2RlbFJlZ2lzdHJ5UmVxdWVzdBIrCghyZWdpc3RyeRgBIAEoCzIZLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeRIYChBleHBlY3RlZF92ZXJzaW9uGAIgASgDIisKGFB1dE1vZGVsUmVnaXN0cnlSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgDIhkKF0dldE1vZGVsUmVnaXN0cnlSZXF1ZXN0IlgKGEdldE1vZGVsUmVnaXN0cnlSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgDEisKCHJlZ2lzdHJ5GAIgASgLMhkuY29tcGFzcy52MS5Nb2RlbFJlZ2lzdHJ5IhwKGkRlbGV0ZU1vZGVsUmVnaXN0cnlSZXF1ZXN0Ih0KG0RlbGV0ZU1vZGVsUmVnaXN0cnlSZXNwb25zZSI+ChBBZ2VudEF0dHJpYnV0aW9uEhQKDGFnZW50X2hhbmRsZRgBIAEoCRIUCgxvd25lcl9oYW5kbGUYAiABKAkiRQoIRm9yZ2VSZWYSKwoIcHJvdmlkZXIYASABKA4yGS5jb21wYXNzLnYxLkZvcmdlUHJvdmlkZXISDAoEaG9zdBgCIAEoCSLUAwoFSXNzdWUSCgoCaWQYASABKAkSIwoFZm9yZ2UYAiABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAyABKAkSDgoGbnVtYmVyGAQgASgNEg0KBXRpdGxlGAUgASgJEgwKBGJvZHkYBiABKAkSEwoLZm9yZ2Vfc3RhdGUYByABKAkSCwoDdXJsGAggASgJEisKBWFnZW50GAkgASgLMhwuY29tcGFzcy52MS5BZ2VudEF0dHJpYnV0aW9uEhUKDWZvcmdlX2FjY291bnQYCiABKAkSDgoGbGFiZWxzGAsgAygJEi4KCnVwZGF0ZWRfYXQYEyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEiUKBXN0YXRlGAwgASgOMhYuY29tcGFzcy52MS5Jc3N1ZVN0YXRlEhAKCHByaW9yaXR5GA0gASgJEhAKCGFzc2lnbmVlGA4gASgJEg8KB3N1bW1hcnkYDyABKAkSDgoGYnJhbmNoGBAgASgJEiQKA3BycxgRIAMoCzIXLmNvbXBhc3MudjEuUHVsbFJlcXVlc3QSJwoHdHJhY2tlchgSIAEoCzIWLmNvbXBhc3MudjEuVHJhY2tlclJlZiL7AwoLUHVsbFJlcXVlc3QSIwoFZm9yZ2UYASABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAiABKAkSDgoGbnVtYmVyGAMgASgNEg0KBXRpdGxlGAQgASgJEhMKC2ZvcmdlX3N0YXRlGAUgASgJEgsKA3VybBgGIAEoCRIQCghoZWFkX3JlZhgHIAEoCRIQCghiYXNlX3JlZhgIIAEoCRIrCgVhZ2VudBgJIAEoCzIcLmNvbXBhc3MudjEuQWdlbnRBdHRyaWJ1dGlvbhIVCg1mb3JnZV9hY2NvdW50GAogASgJEg0KBWRyYWZ0GAsgASgIEikKB2NoYW5nZWQYDCABKAsyGC5jb21wYXNzLnYxLkNoYW5nZWRTdGF0cxIpCgZjaGVja3MYDSABKAsyGS5jb21wYXNzLnYxLkNoZWNrc1N1bW1hcnkSIwoHcmV2aWV3cxgOIAMoCzISLmNvbXBhc3MudjEuUmV2aWV3EikKB3RocmVhZHMYDyADKAsyGC5jb21wYXNzLnYxLlJldmlld1RocmVhZBIQCghoZWFkX3NoYRgQIAEoCRIXCg9tZXJnZWFibGVfc3RhdGUYESABKAkSMAoLbWVyZ2VfcXVldWUYEiABKAsyGy5jb21wYXNzLnYxLk1lcmdlUXVldWVFbnRyeSJTCg1DaGVja3NTdW1tYXJ5EhAKCGhlYWRfc2hhGAEgASgJEg0KBXN0YXRlGAIgASgJEiEKBmNoZWNrcxgDIAMoCzIRLmNvbXBhc3MudjEuQ2hlY2siQwoFQ2hlY2sSDAoEbmFtZRgBIAEoCRINCgVzdGF0ZRgCIAEoCRILCgN1cmwYAyABKAkSEAoIcmVxdWlyZWQYBCABKAgiQwoMQ2hhbmdlZFN0YXRzEg0KBWZpbGVzGAEgASgNEhEKCWFkZGl0aW9ucxgCIAEoDRIRCglkZWxldGlvbnMYAyABKA0iaQoKVHJhY2tlclJlZhIMCgRraW5kGAEgASgJEgoKAmlkGAIgASgJEg4KBnN0YXR1cxgDIAEoCRILCgN1cmwYBCABKAkSEAoIc3RhdGVfaWQYBSABKAkSEgoKc3RhdGVfdHlwZRgGIAEoCSJaCgZSZXZpZXcSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIPCgd2ZXJkaWN0GAMgASgJEgwKBGJvZHkYBCABKAkSEQoJY29tbWl0X2lkGAUgASgJIlUKDFJldmlld1RocmVhZBIMCgRwYXRoGAEgASgJEhAKCHJlc29sdmVkGAIgASgIEiUKCGNvbW1lbnRzGAMgAygLMhMuY29tcGFzcy52MS5Db21tZW50IjcKB0NvbW1lbnQSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIMCgRib2R5GAMgASgJImIKD01lcmdlUXVldWVFbnRyeRIQCghwcm92aWRlchgBIAEoCRINCgVzdGF0ZRgCIAEoCRIuCgpjaGFuZ2VkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCpkCg5TZWNyZXREZWxpdmVyeRIfChtTRUNSRVRfREVMSVZFUllfVU5TUEVDSUZJRUQQABIYChRTRUNSRVRfREVMSVZFUllfRklMRRABEhcKE1NFQ1JFVF9ERUxJVkVSWV9FTlYQAipwCgpTZWNyZXRLaW5kEhsKF1NFQ1JFVF9LSU5EX1VOU1BFQ0lGSUVEEAASFwoTU0VDUkVUX0tJTkRfR0VORVJJQxABEhgKFFNFQ1JFVF9LSU5EX1BST1ZJREVSEAISEgoOU0VDUkVUX0tJTkRfR0gQAypbCgtTZWNyZXRTY29wZRIcChhTRUNSRVRfU0NPUEVfVU5TUEVDSUZJRUQQABIVChFTRUNSRVRfU0NPUEVfVVNFUhABEhcKE1NFQ1JFVF9TQ09QRV9URU5BTlQQAip9CgtUb3VyT3V0Y29tZRIcChhUT1VSX09VVENPTUVfVU5TUEVDSUZJRUQQABIYChRUT1VSX09VVENPTUVfU1RBUlRFRBABEhoKFlRPVVJfT1VUQ09NRV9ESVNNSVNTRUQQAhIaChZUT1VSX09VVENPTUVfQ09NUExFVEVEEAMqQwoLU2VydmVyU3RhdGUSHAoYU0VSVkVSX1NUQVRFX1VOU1BFQ0lGSUVEEAASFgoSU0VSVkVSX1NUQVRFX1JFQURZEAEqlwEKC1J1bnRpbWVUaWVyEhwKGFJVTlRJTUVfVElFUl9VTlNQRUNJRklFRBAAEhcKE1JVTlRJTUVfVElFUl9QT0RNQU4QARIYChRSVU5USU1FX1RJRVJfTUlDUk9WTRACEiAKHFJVTlRJTUVfVElFUl9BUFBMRV9DT05UQUlORVIQAxIVChFSVU5USU1FX1RJRVJfSE9TVBAEKmgKDUVncmVzc1Bvc3R1cmUSHgoaRUdSRVNTX1BPU1RVUkVfVU5TUEVDSUZJRUQQABIYChRFR1JFU1NfUE9TVFVSRV9BUk1FRBABEh0KGUVHUkVTU19QT1NUVVJFX1VORU5GT1JDRUQQAiqCAgoRQWdlbnRTZXNzaW9uU3RhdGUSIwofQUdFTlRfU0VTU0lPTl9TVEFURV9VTlNQRUNJRklFRBAAEiAKHEFHRU5UX1NFU1NJT05fU1RBVEVfU1RBUlRJTkcQARIdChlBR0VOVF9TRVNTSU9OX1NUQVRFX1JFQURZEAISHwobQUdFTlRfU0VTU0lPTl9TVEFURV9XT1JLSU5HEAMSHwobQUdFTlRfU0VTU0lPTl9TVEFURV9TVE9QUEVEEAQSHwobQUdFTlRfU0VTU0lPTl9TVEFURV9FUlJPUkVEEAUSJAogQUdFTlRfU0VTU0lPTl9TVEFURV9ESVNDT05ORUNURUQQBirSAQoTQWdlbnRUb29sQ2FsbFN0YXR1cxImCiJBR0VOVF9UT09MX0NBTExfU1RBVFVTX1VOU1BFQ0lGSUVEEAASIgoeQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19QRU5ESU5HEAESJgoiQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19JTl9QUk9HUkVTUxACEiQKIEFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfQ09NUExFVEVEEAMSIQodQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19GQUlMRUQQBCq0AQoUQWdlbnRQbGFuRW50cnlTdGF0dXMSJwojQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfVU5TUEVDSUZJRUQQABIjCh9BR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19QRU5ESU5HEAESJwojQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfSU5fUFJPR1JFU1MQAhIlCiFBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19DT01QTEVURUQQAyqEAQoUU2Vzc2lvbkluamVjdGlvbktpbmQSJgoiU0VTU0lPTl9JTkpFQ1RJT05fS0lORF9VTlNQRUNJRklFRBAAEiAKHFNFU1NJT05fSU5KRUNUSU9OX0tJTkRfU1RFRVIQARIiCh5TRVNTSU9OX0lOSkVDVElPTl9LSU5EX0RFTElWRVIQAip0ChBTZXNzaW9uRXJyb3JLaW5kEiIKHlNFU1NJT05fRVJST1JfS0lORF9VTlNQRUNJRklFRBAAEhwKGFNFU1NJT05fRVJST1JfS0lORF9FUlJPUhABEh4KGlNFU1NJT05fRVJST1JfS0lORF9BQk9SVEVEEAIq8QEKCklzc3VlU3RhdGUSGwoXSVNTVUVfU1RBVEVfVU5TUEVDSUZJRUQQABIXChNJU1NVRV9TVEFURV9CQUNLTE9HEAESFAoQSVNTVUVfU1RBVEVfVE9ETxACEhYKEklTU1VFX1NUQVRFX1FVRVVFRBADEhcKE0lTU1VFX1NUQVRFX0JMT0NLRUQQBBIbChdJU1NVRV9TVEFURV9JTl9QUk9HUkVTUxAFEhkKFUlTU1VFX1NUQVRFX0lOX1JFVklFVxAGEhQKEElTU1VFX1NUQVRFX0RPTkUQBxIYChRJU1NVRV9TVEFURV9BUkNISVZFRBAIKpwBCg1Gb3JnZVByb3ZpZGVyEh4KGkZPUkdFX1BST1ZJREVSX1VOU1BFQ0lGSUVEEAASGQoVRk9SR0VfUFJPVklERVJfR0lUSFVCEAESGQoVRk9SR0VfUFJPVklERVJfR0lUTEFCEAISGgoWRk9SR0VfUFJPVklERVJfRk9SR0VKTxADEhkKFUZPUkdFX1BST1ZJREVSX0xJTkVBUhAEMrMTCg5Db21wYXNzU2VydmljZRJUCg1HZXRTZXJ2ZXJJbmZvEiAuY29tcGFzcy52MS5HZXRTZXJ2ZXJJbmZvUmVxdWVzdBohLmNvbXBhc3MudjEuR2V0U2VydmVySW5mb1Jlc3BvbnNlEj8KBldob0FtSRIZLmNvbXBhc3MudjEuV2hvQW1JUmVxdWVzdBoaLmNvbXBhc3MudjEuV2hvQW1JUmVzcG9uc2USWgoPR2V0UmVmTGlua1J1bGVzEiIuY29tcGFzcy52MS5HZXRSZWZMaW5rUnVsZXNSZXF1ZXN0GiMuY29tcGFzcy52MS5HZXRSZWZMaW5rUnVsZXNSZXNwb25zZRJRCgxHZXRUb3VyU3RhdGUSHy5jb21wYXNzLnYxLkdldFRvdXJTdGF0ZVJlcXVlc3QaIC5jb21wYXNzLnYxLkdldFRvdXJTdGF0ZVJlc3BvbnNlElcKDkNsYWltVG91clN0YXJ0EiEuY29tcGFzcy52MS5DbGFpbVRvdXJTdGFydFJlcXVlc3QaIi5jb21wYXNzLnYxLkNsYWltVG91clN0YXJ0UmVzcG9uc2USUQoMU2V0VG91clN0YXRlEh8uY29tcGFzcy52MS5TZXRUb3VyU3RhdGVSZXF1ZXN0GiAuY29tcGFzcy52MS5TZXRUb3VyU3RhdGVSZXNwb25zZRJcCg9TdWJzY3JpYmVFdmVudHMSIi5jb21wYXNzLnYxLlN1YnNjcmliZUV2ZW50c1JlcXVlc3QaIy5jb21wYXNzLnYxLlN1YnNjcmliZUV2ZW50c1Jlc3BvbnNlMAESWgoPTGlzdEJvYXJkSXNzdWVzEiIuY29tcGFzcy52MS5MaXN0Qm9hcmRJc3N1ZXNSZXF1ZXN0GiMuY29tcGFzcy52MS5MaXN0Qm9hcmRJc3N1ZXNSZXNwb25zZRJRCgxTZWFyY2hJc3N1ZXMSHy5jb21wYXNzLnYxLlNlYXJjaElzc3Vlc1JlcXVlc3QaIC5jb21wYXNzLnYxLlNlYXJjaElzc3Vlc1Jlc3BvbnNlEnIKF1Byb3Zpc2lvbkFnZW50V29ya3NwYWNlEiouY29tcGFzcy52MS5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QaKy5jb21wYXNzLnYxLlByb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVzcG9uc2USYAoRU3RhcnRBZ2VudFNlc3Npb24SJC5jb21wYXNzLnYxLlN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBolLmNvbXBhc3MudjEuU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRJLCgpTcGF3bkFnZW50Eh0uY29tcGFzcy52MS5TcGF3bkFnZW50UmVxdWVzdBoeLmNvbXBhc3MudjEuU3Bhd25BZ2VudFJlc3BvbnNlEl0KEFN0b3BBZ2VudFNlc3Npb24SIy5jb21wYXNzLnYxLlN0b3BBZ2VudFNlc3Npb25SZXF1ZXN0GiQuY29tcGFzcy52MS5TdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2USWgoPU2tpcEJhdGNoV2luZG93EiIuY29tcGFzcy52MS5Ta2lwQmF0Y2hXaW5kb3dSZXF1ZXN0GiMuY29tcGFzcy52MS5Ta2lwQmF0Y2hXaW5kb3dSZXNwb25zZRJpChRSZW1vdmVBZ2VudFdvcmtzcGFjZRInLmNvbXBhc3MudjEuUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0GiguY29tcGFzcy52MS5SZW1vdmVBZ2VudFdvcmtzcGFjZVJlc3BvbnNlEmMKElJlbG9hZEFnZW50U2Vzc2lvbhIlLmNvbXBhc3MudjEuUmVsb2FkQWdlbnRTZXNzaW9uUmVxdWVzdBomLmNvbXBhc3MudjEuUmVsb2FkQWdlbnRTZXNzaW9uUmVzcG9uc2USVwoOR2V0QWdlbnRTdGF0dXMSIS5jb21wYXNzLnYxLkdldEFnZW50U3RhdHVzUmVxdWVzdBoiLmNvbXBhc3MudjEuR2V0QWdlbnRTdGF0dXNSZXNwb25zZRJiChVTdWJzY3JpYmVBZ2VudFNlc3Npb24SKC5jb21wYXNzLnYxLlN1YnNjcmliZUFnZW50U2Vzc2lvblJlcXVlc3QaHS5jb21wYXNzLnYxLkFnZW50U2Vzc2lvbkZyYW1lMAESSwoKSXNzdWVUb2tlbhIdLmNvbXBhc3MudjEuSXNzdWVUb2tlblJlcXVlc3QaHi5jb21wYXNzLnYxLklzc3VlVG9rZW5SZXNwb25zZRJUCg1SZW5hbWVBY2NvdW50EiAuY29tcGFzcy52MS5SZW5hbWVBY2NvdW50UmVxdWVzdBohLmNvbXBhc3MudjEuUmVuYW1lQWNjb3VudFJlc3BvbnNlEk4KC1Jldm9rZVRva2VuEh4uY29tcGFzcy52MS5SZXZva2VUb2tlblJlcXVlc3QaHy5jb21wYXNzLnYxLlJldm9rZVRva2VuUmVzcG9uc2USVwoOUHV0QWdlbnRDb25maWcSIS5jb21wYXNzLnYxLlB1dEFnZW50Q29uZmlnUmVxdWVzdBoiLmNvbXBhc3MudjEuUHV0QWdlbnRDb25maWdSZXNwb25zZRJjChJHZXRBZ2VudENvbmZpZ0luZm8SJS5jb21wYXNzLnYxLkdldEFnZW50Q29uZmlnSW5mb1JlcXVlc3QaJi5jb21wYXNzLnYxLkdldEFnZW50Q29uZmlnSW5mb1Jlc3BvbnNlEmAKEURlbGV0ZUFnZW50Q29uZmlnEiQuY29tcGFzcy52MS5EZWxldGVBZ2VudENvbmZpZ1JlcXVlc3QaJS5jb21wYXNzLnYxLkRlbGV0ZUFnZW50Q29uZmlnUmVzcG9uc2USXQoQUHV0TW9kZWxSZWdpc3RyeRIjLmNvbXBhc3MudjEuUHV0TW9kZWxSZWdpc3RyeVJlcXVlc3QaJC5jb21wYXNzLnYxLlB1dE1vZGVsUmVnaXN0cnlSZXNwb25zZRJdChBHZXRNb2RlbFJlZ2lzdHJ5EiMuY29tcGFzcy52MS5HZXRNb2RlbFJlZ2lzdHJ5UmVxdWVzdBokLmNvbXBhc3MudjEuR2V0TW9kZWxSZWdpc3RyeVJlc3BvbnNlEmYKE0RlbGV0ZU1vZGVsUmVnaXN0cnkSJi5jb21wYXNzLnYxLkRlbGV0ZU1vZGVsUmVnaXN0cnlSZXF1ZXN0GicuY29tcGFzcy52MS5EZWxldGVNb2RlbFJlZ2lzdHJ5UmVzcG9uc2Uy3wIKDlNlY3JldHNTZXJ2aWNlEkgKCVNldFNlY3JldBIcLmNvbXBhc3MudjEuU2V0U2VjcmV0UmVxdWVzdBodLmNvbXBhc3MudjEuU2V0U2VjcmV0UmVzcG9uc2USTgoLTGlzdFNlY3JldHMSHi5jb21wYXNzLnYxLkxpc3RTZWNyZXRzUmVxdWVzdBofLmNvbXBhc3MudjEuTGlzdFNlY3JldHNSZXNwb25zZRJRCgxEZWxldGVTZWNyZXQSHy5jb21wYXNzLnYxLkRlbGV0ZVNlY3JldFJlcXVlc3QaIC5jb21wYXNzLnYxLkRlbGV0ZVNlY3JldFJlc3BvbnNlEmAKEUxpc3RTZXJ2ZXJTZWNyZXRzEiQuY29tcGFzcy52MS5MaXN0U2VydmVyU2VjcmV0c1JlcXVlc3QaJS5jb21wYXNzLnYxLkxpc3RTZXJ2ZXJTZWNyZXRzUmVzcG9uc2Uy3wIKFkFnZW50UmVwb3NpdG9yeVNlcnZpY2USaQoUR3JhbnRBZ2VudFJlcG9zaXRvcnkSJy5jb21wYXNzLnYxLkdyYW50QWdlbnRSZXBvc2l0b3J5UmVxdWVzdBooLmNvbXBhc3MudjEuR3JhbnRBZ2VudFJlcG9zaXRvcnlSZXNwb25zZRJsChVSZXZva2VBZ2VudFJlcG9zaXRvcnkSKC5jb21wYXNzLnYxLlJldm9rZUFnZW50UmVwb3NpdG9yeVJlcXVlc3QaKS5jb21wYXNzLnYxLlJldm9rZUFnZW50UmVwb3NpdG9yeVJlc3BvbnNlEmwKFUxpc3RBZ2VudFJlcG9zaXRvcmllcxIoLmNvbXBhc3MudjEuTGlzdEFnZW50UmVwb3NpdG9yaWVzUmVxdWVzdBopLmNvbXBhc3MudjEuTGlzdEFnZW50UmVwb3NpdG9yaWVzUmVzcG9uc2ViBnByb3RvMw", [file_google_protobuf_timestamp]); + fileDesc("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEiRwobR3JhbnRBZ2VudFJlcG9zaXRvcnlSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCRISCgpyZXBvc2l0b3J5GAIgASgJIi0KHEdyYW50QWdlbnRSZXBvc2l0b3J5UmVzcG9uc2USDQoFYWRkZWQYASABKAgiSAocUmV2b2tlQWdlbnRSZXBvc2l0b3J5UmVxdWVzdBIUCgxhZ2VudF9oYW5kbGUYASABKAkSEgoKcmVwb3NpdG9yeRgCIAEoCSIwCh1SZXZva2VBZ2VudFJlcG9zaXRvcnlSZXNwb25zZRIPCgdyZW1vdmVkGAEgASgIIjQKHExpc3RBZ2VudFJlcG9zaXRvcmllc1JlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJIjUKHUxpc3RBZ2VudFJlcG9zaXRvcmllc1Jlc3BvbnNlEhQKDHJlcG9zaXRvcmllcxgBIAMoCSLQAQoQU2V0U2VjcmV0UmVxdWVzdBIMCgRuYW1lGAEgASgJEhIKBXZhbHVlGAIgASgJQgOAAQESLAoIZGVsaXZlcnkYAyABKA4yGi5jb21wYXNzLnYxLlNlY3JldERlbGl2ZXJ5EiQKBGtpbmQYBCABKA4yFi5jb21wYXNzLnYxLlNlY3JldEtpbmQSEAoIcHJvdmlkZXIYBSABKAkSDAoEaG9zdBgGIAEoCRImCgVzY29wZRgHIAEoDjIXLmNvbXBhc3MudjEuU2VjcmV0U2NvcGUiEwoRU2V0U2VjcmV0UmVzcG9uc2UiFAoSTGlzdFNlY3JldHNSZXF1ZXN0IkAKE0xpc3RTZWNyZXRzUmVzcG9uc2USKQoHc2VjcmV0cxgBIAMoCzIYLmNvbXBhc3MudjEuU2VjcmV0U3RhdHVzIqABCgxTZWNyZXRTdGF0dXMSDAoEbmFtZRgBIAEoCRIOCgZpc19zZXQYAiABKAgSLAoIZGVsaXZlcnkYAyABKA4yGi5jb21wYXNzLnYxLlNlY3JldERlbGl2ZXJ5EiQKBGtpbmQYBCABKA4yFi5jb21wYXNzLnYxLlNlY3JldEtpbmQSEAoIcHJvdmlkZXIYBSABKAkSDAoEaG9zdBgGIAEoCSJLChNEZWxldGVTZWNyZXRSZXF1ZXN0EgwKBG5hbWUYASABKAkSJgoFc2NvcGUYAiABKA4yFy5jb21wYXNzLnYxLlNlY3JldFNjb3BlIhYKFERlbGV0ZVNlY3JldFJlc3BvbnNlIhoKGExpc3RTZXJ2ZXJTZWNyZXRzUmVxdWVzdCJTChlMaXN0U2VydmVyU2VjcmV0c1Jlc3BvbnNlEjYKDnNlcnZlcl9zZWNyZXRzGAEgAygLMh4uY29tcGFzcy52MS5TZXJ2ZXJTZWNyZXRTdGF0dXMiMgoSU2VydmVyU2VjcmV0U3RhdHVzEgwKBG5hbWUYASABKAkSDgoGaXNfc2V0GAIgASgIIhgKFkdldFJlZkxpbmtSdWxlc1JlcXVlc3QiQQoLUmVmTGlua1J1bGUSDAoEa2luZBgBIAEoCRIOCgZwcmVmaXgYAiABKAkSFAoMdXJsX3RlbXBsYXRlGAMgASgJIkEKF0dldFJlZkxpbmtSdWxlc1Jlc3BvbnNlEiYKBXJ1bGVzGAEgAygLMhcuY29tcGFzcy52MS5SZWZMaW5rUnVsZSIWChRHZXRTZXJ2ZXJJbmZvUmVxdWVzdCKAAQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSEwoLYXBpX3ZlcnNpb24YAiABKAkSCwoDcmV2GAMgASgJEjQKEGVucm9sbGVkX3J1bm5lcnMYBCADKAsyGi5jb21wYXNzLnYxLkVucm9sbGVkUnVubmVyIjAKDkVucm9sbGVkUnVubmVyEgoKAmlkGAEgASgJEhIKCmVucm9sbG1lbnQYAiABKAQiDwoNV2hvQW1JUmVxdWVzdCIkCg5XaG9BbUlSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJIhUKE0dldFRvdXJTdGF0ZVJlcXVlc3QiUQoUR2V0VG91clN0YXRlUmVzcG9uc2USKAoHb3V0Y29tZRgBIAEoDjIXLmNvbXBhc3MudjEuVG91ck91dGNvbWUSDwoHc3RlcF9pZBgCIAEoCSIoChVDbGFpbVRvdXJTdGFydFJlcXVlc3QSDwoHc3RlcF9pZBgBIAEoCSIpChZDbGFpbVRvdXJTdGFydFJlc3BvbnNlEg8KB2NsYWltZWQYASABKAgiUAoTU2V0VG91clN0YXRlUmVxdWVzdBIoCgdvdXRjb21lGAEgASgOMhcuY29tcGFzcy52MS5Ub3VyT3V0Y29tZRIPCgdzdGVwX2lkGAIgASgJIhYKFFNldFRvdXJTdGF0ZVJlc3BvbnNlIkMKFlN1YnNjcmliZUV2ZW50c1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEIuIDChdTdWJzY3JpYmVFdmVudHNSZXNwb25zZRILCgNzZXEYASABKAQSEgoKYXRfdW5peF9tcxgCIAEoAxIWCg5pbnN0YW5jZV9lcG9jaBgDIAEoBBIUCgxzbmFwc2hvdF9zZXEYBCABKAQSMQoNc2VydmVyX3N0YXR1cxgKIAEoCzIYLmNvbXBhc3MudjEuU2VydmVyU3RhdHVzSAASNQoPcmVzeW5jX3JlcXVpcmVkGAsgASgLMhouY29tcGFzcy52MS5SZXN5bmNSZXF1aXJlZEgAEj4KFGFnZW50X3Nlc3Npb25fc3RhdHVzGAwgASgLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXNIABI8ChNhZ2VudF9tZXNzYWdlX2NodW5rGA0gASgLMh0uY29tcGFzcy52MS5BZ2VudE1lc3NhZ2VDaHVua0gAEjQKD2FnZW50X3Rvb2xfY2FsbBgOIAEoCzIZLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbEgAEisKCmFnZW50X3BsYW4YDyABKAsyFS5jb21wYXNzLnYxLkFnZW50UGxhbkgAEiIKBWlzc3VlGBAgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAQgkKB3BheWxvYWQiLgoWTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPAoXTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSJJChNTZWFyY2hJc3N1ZXNSZXF1ZXN0Eg0KBXF1ZXJ5GAEgASgJEg0KBWxpbWl0GAIgASgNEhQKDHNuYXBzaG90X3NlcRgDIAEoBCI5ChRTZWFyY2hJc3N1ZXNSZXNwb25zZRIhCgZpc3N1ZXMYASADKAsyES5jb21wYXNzLnYxLklzc3VlIjYKDFNlcnZlclN0YXR1cxImCgVzdGF0ZRgBIAEoDjIXLmNvbXBhc3MudjEuU2VydmVyU3RhdGUiEAoOUmVzeW5jUmVxdWlyZWQi0gEKEkFnZW50U2Vzc2lvblN0YXR1cxISCgpzZXNzaW9uX2lkGAEgASgJEiwKBXN0YXRlGAIgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZRIYChBhZ2VudF9hY2NvdW50X2lkGAMgASgJEi0KDHJ1bnRpbWVfdGllchgEIAEoDjIXLmNvbXBhc3MudjEuUnVudGltZVRpZXISMQoOZWdyZXNzX3Bvc3R1cmUYBSABKA4yGS5jb21wYXNzLnYxLkVncmVzc1Bvc3R1cmUiSQoRQWdlbnRNZXNzYWdlQ2h1bmsSEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgR0ZXh0GAIgASgJEhIKCmlzX3Rob3VnaHQYAyABKAgieQoNQWdlbnRUb29sQ2FsbBISCgpzZXNzaW9uX2lkGAEgASgJEhQKDHRvb2xfY2FsbF9pZBgCIAEoCRINCgV0aXRsZRgDIAEoCRIvCgZzdGF0dXMYBCABKA4yHy5jb21wYXNzLnYxLkFnZW50VG9vbENhbGxTdGF0dXMiTAoJQWdlbnRQbGFuEhIKCnNlc3Npb25faWQYASABKAkSKwoHZW50cmllcxgCIAMoCzIaLmNvbXBhc3MudjEuQWdlbnRQbGFuRW50cnkiUwoOQWdlbnRQbGFuRW50cnkSDwoHY29udGVudBgBIAEoCRIwCgZzdGF0dXMYAiABKA4yIC5jb21wYXNzLnYxLkFnZW50UGxhbkVudHJ5U3RhdHVzIpkECgxTZXNzaW9uRXZlbnQSEAoIZXZlbnRfaWQYASABKAkSEgoKYXRfdW5peF9tcxgCIAEoAxI6Cg5hc3Npc3RhbnRfdGV4dBgDIAEoCzIgLmNvbXBhc3MudjEuU2Vzc2lvbkFzc2lzdGFudFRleHRIABIvCgh0aGlua2luZxgEIAEoCzIbLmNvbXBhc3MudjEuU2Vzc2lvblRoaW5raW5nSAASMAoJdG9vbF9jYWxsGAUgASgLMhsuY29tcGFzcy52MS5TZXNzaW9uVG9vbENhbGxIABI9ChB0b29sX2NhbGxfdXBkYXRlGAYgASgLMiEuY29tcGFzcy52MS5TZXNzaW9uVG9vbENhbGxVcGRhdGVIABInCgRwbGFuGAcgASgLMhcuY29tcGFzcy52MS5TZXNzaW9uUGxhbkgAEisKBm5vdGljZRgIIAEoCzIZLmNvbXBhc3MudjEuU2Vzc2lvbk5vdGljZUgAEjkKEXNlc3Npb25faW5qZWN0aW9uGAkgASgLMhwuY29tcGFzcy52MS5TZXNzaW9uSW5qZWN0aW9uSAASMQoNc2Vzc2lvbl9lcnJvchgKIAEoCzIYLmNvbXBhc3MudjEuU2Vzc2lvbkVycm9ySAASOAoNYmF0Y2hfcGVuZGluZxgLIAEoCzIfLmNvbXBhc3MudjEuU2Vzc2lvbkJhdGNoUGVuZGluZ0gAQgcKBWV2ZW50IjgKFFNlc3Npb25Bc3Npc3RhbnRUZXh0EgwKBHRleHQYASABKAkSEgoKbWVzc2FnZV9pZBgCIAEoCSIzCg9TZXNzaW9uVGhpbmtpbmcSDAoEdGV4dBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJImcKD1Nlc3Npb25Ub29sQ2FsbBIUCgx0b29sX2NhbGxfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSLwoGc3RhdHVzGAMgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIpoBChVTZXNzaW9uVG9vbENhbGxVcGRhdGUSFAoMdG9vbF9jYWxsX2lkGAEgASgJEi8KBnN0YXR1cxgCIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cxIOCgZvdXRwdXQYAyABKAkSKgoFZGlmZnMYBCADKAsyGy5jb21wYXNzLnYxLlNlc3Npb25GaWxlRGlmZiJVCg9TZXNzaW9uRmlsZURpZmYSDAoEcGF0aBgBIAEoCRIVCghvbGRfdGV4dBgCIAEoCUgAiAEBEhAKCG5ld190ZXh0GAMgASgJQgsKCV9vbGRfdGV4dCI6CgtTZXNzaW9uUGxhbhIrCgdlbnRyaWVzGAEgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSI5Cg1TZXNzaW9uTm90aWNlEgwKBHRleHQYASABKAkSEQoEbGluaxgCIAEoCUgAiAEBQgcKBV9saW5rIoMBChBTZXNzaW9uSW5qZWN0aW9uEjEKB29wX2tpbmQYASABKA4yIC5jb21wYXNzLnYxLlNlc3Npb25JbmplY3Rpb25LaW5kEhIKCm1lc3NhZ2VfaWQYAiABKAkSEwoLZnJvbV9oYW5kbGUYAyABKAkSEwoLdHJhY2VwYXJlbnQYBCABKAkiawoMU2Vzc2lvbkVycm9yEioKBGtpbmQYASABKA4yHC5jb21wYXNzLnYxLlNlc3Npb25FcnJvcktpbmQSDwoHbWVzc2FnZRgCIAEoCRITCgZzdGF0dXMYAyABKAVIAIgBAUIJCgdfc3RhdHVzIj4KE1Nlc3Npb25CYXRjaFBlbmRpbmcSDQoFY291bnQYASABKA0SGAoQZmlyZXNfYXRfdW5peF9tcxgCIAEoAyIyChxTdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkilQEKEUFnZW50U2Vzc2lvbkZyYW1lEhIKCnNlc3Npb25faWQYASABKAkSJwoFZXZlbnQYAiABKAsyGC5jb21wYXNzLnYxLlNlc3Npb25FdmVudBIsCgVzdGF0ZRgDIAEoDjIdLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdGUSFQoNdHVybl9zZXF1ZW5jZRgEIAEoBCJwCh5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJEg8KB3BlcnNvbmEYAyABKAkSDAoEcm9sZRgEIAEoCSI5Ch9Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEhYKDmNvbnRhaW5lcl9uYW1lGAEgASgJIlAKG1JlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgCIAEoCSIeChxSZW1vdmVBZ2VudFdvcmtzcGFjZVJlc3BvbnNlImMKGFN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFyZXN1bWVfc2Vzc2lvbl9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiLwoZU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIloKEVNwYXduQWdlbnRSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiQAoSU3Bhd25BZ2VudFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSFgoOY29udGFpbmVyX25hbWUYAiABKAkiLQoXU3RvcEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIaChhTdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2UiLAoWU2tpcEJhdGNoV2luZG93UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIhkKF1NraXBCYXRjaFdpbmRvd1Jlc3BvbnNlIi8KGVJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIwChpSZWxvYWRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIisKFUdldEFnZW50U3RhdHVzUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJInwKFkdldEFnZW50U3RhdHVzUmVzcG9uc2USMAoIc3RhdHVzZXMYASADKAsyHi5jb21wYXNzLnYxLkFnZW50U2Vzc2lvblN0YXR1cxIcCg9ydW5uZXJfZW5yb2xsZWQYAiABKAhIAIgBAUISChBfcnVubmVyX2Vucm9sbGVkIisKEUlzc3VlVG9rZW5SZXF1ZXN0EhYKDmFjY291bnRfaGFuZGxlGAEgASgJIiMKEklzc3VlVG9rZW5SZXNwb25zZRINCgV0b2tlbhgBIAEoCSJCChRSZW5hbWVBY2NvdW50UmVxdWVzdBIWCg5hY2NvdW50X2hhbmRsZRgBIAEoCRISCgpuZXdfaGFuZGxlGAIgASgJIjsKFVJlbmFtZUFjY291bnRSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJEg4KBmhhbmRsZRgCIAEoCSIoChJSZXZva2VUb2tlblJlcXVlc3QSEgoFdG9rZW4YASABKAlCA4ABASIVChNSZXZva2VUb2tlblJlc3BvbnNlIicKFVB1dEFnZW50Q29uZmlnUmVxdWVzdBIOCgZidW5kbGUYASABKAwiKQoWUHV0QWdlbnRDb25maWdSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJIhsKGUdldEFnZW50Q29uZmlnSW5mb1JlcXVlc3Qi2gEKGkdldEFnZW50Q29uZmlnSW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSDgoGc2tpbGxzGAIgAygJEhIKCmV4dGVuc2lvbnMYAyADKAkSEwoLbWNwX3NlcnZlcnMYBCADKAkSFAoMaGFzX3NldHRpbmdzGAUgASgIEhUKDWhhc19hZ2VudHNfbWQYBiABKAgSDQoFcnVsZXMYByADKAkSEQoJc3ViYWdlbnRzGAggAygJEhIKCmhhc19tb2RlbHMYCSABKAgSDwoHcHJvbXB0cxgKIAMoCSIaChhEZWxldGVBZ2VudENvbmZpZ1JlcXVlc3QiGwoZRGVsZXRlQWdlbnRDb25maWdSZXNwb25zZSI0Cg5Nb2RlbENhbmRpZGF0ZRIQCghwcm92aWRlchgBIAEoCRIQCghtb2RlbF9pZBgCIAEoCSJxCg1Nb2RlbE1ldGFkYXRhEhYKDmNvbnRleHRfd2luZG93GAEgASgDEhwKFGlucHV0X2Nvc3RfbWljcm9fdXNkGAIgASgDEh0KFW91dHB1dF9jb3N0X21pY3JvX3VzZBgDIAEoAxILCgNhcGkYBCABKAkihwEKEk1vZGVsUmVnaXN0cnlFbnRyeRIUCgxkaXNwbGF5X25hbWUYASABKAkSLgoKY2FuZGlkYXRlcxgCIAMoCzIaLmNvbXBhc3MudjEuTW9kZWxDYW5kaWRhdGUSKwoIbWV0YWRhdGEYAyABKAsyGS5jb21wYXNzLnYxLk1vZGVsTWV0YWRhdGEimAEKDU1vZGVsUmVnaXN0cnkSNwoHZW50cmllcxgBIAMoCzImLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeS5FbnRyaWVzRW50cnkaTgoMRW50cmllc0VudHJ5EgsKA2tleRgBIAEoCRItCgV2YWx1ZRgCIAEoCzIeLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeUVudHJ5OgI4ASJgChdQdXRNb2RlbFJlZ2lzdHJ5UmVxdWVzdBIrCghyZWdpc3RyeRgBIAEoCzIZLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeRIYChBleHBlY3RlZF92ZXJzaW9uGAIgASgDIisKGFB1dE1vZGVsUmVnaXN0cnlSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgDIhkKF0dldE1vZGVsUmVnaXN0cnlSZXF1ZXN0IlgKGEdldE1vZGVsUmVnaXN0cnlSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgDEisKCHJlZ2lzdHJ5GAIgASgLMhkuY29tcGFzcy52MS5Nb2RlbFJlZ2lzdHJ5IhwKGkRlbGV0ZU1vZGVsUmVnaXN0cnlSZXF1ZXN0Ih0KG0RlbGV0ZU1vZGVsUmVnaXN0cnlSZXNwb25zZSI+ChBBZ2VudEF0dHJpYnV0aW9uEhQKDGFnZW50X2hhbmRsZRgBIAEoCRIUCgxvd25lcl9oYW5kbGUYAiABKAkiRQoIRm9yZ2VSZWYSKwoIcHJvdmlkZXIYASABKA4yGS5jb21wYXNzLnYxLkZvcmdlUHJvdmlkZXISDAoEaG9zdBgCIAEoCSJyChJGb3JnZU1pcnJvclNldHRpbmcSIwoFZm9yZ2UYASABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAiABKAkSKQoEbW9kZRgDIAEoDjIbLmNvbXBhc3MudjEuRm9yZ2VNaXJyb3JNb2RlIiAKHkxpc3RGb3JnZU1pcnJvclNldHRpbmdzUmVxdWVzdCJlCh9MaXN0Rm9yZ2VNaXJyb3JTZXR0aW5nc1Jlc3BvbnNlEjAKCHNldHRpbmdzGAEgAygLMh4uY29tcGFzcy52MS5Gb3JnZU1pcnJvclNldHRpbmcSEAoIY2FuX2VkaXQYAiABKAgiTwocU2V0Rm9yZ2VNaXJyb3JTZXR0aW5nUmVxdWVzdBIvCgdzZXR0aW5nGAEgASgLMh4uY29tcGFzcy52MS5Gb3JnZU1pcnJvclNldHRpbmciUAodU2V0Rm9yZ2VNaXJyb3JTZXR0aW5nUmVzcG9uc2USLwoHc2V0dGluZxgBIAEoCzIeLmNvbXBhc3MudjEuRm9yZ2VNaXJyb3JTZXR0aW5nItQDCgVJc3N1ZRIKCgJpZBgBIAEoCRIjCgVmb3JnZRgCIAEoCzIULmNvbXBhc3MudjEuRm9yZ2VSZWYSDAoEcmVwbxgDIAEoCRIOCgZudW1iZXIYBCABKA0SDQoFdGl0bGUYBSABKAkSDAoEYm9keRgGIAEoCRITCgtmb3JnZV9zdGF0ZRgHIAEoCRILCgN1cmwYCCABKAkSKwoFYWdlbnQYCSABKAsyHC5jb21wYXNzLnYxLkFnZW50QXR0cmlidXRpb24SFQoNZm9yZ2VfYWNjb3VudBgKIAEoCRIOCgZsYWJlbHMYCyADKAkSLgoKdXBkYXRlZF9hdBgTIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASJQoFc3RhdGUYDCABKA4yFi5jb21wYXNzLnYxLklzc3VlU3RhdGUSEAoIcHJpb3JpdHkYDSABKAkSEAoIYXNzaWduZWUYDiABKAkSDwoHc3VtbWFyeRgPIAEoCRIOCgZicmFuY2gYECABKAkSJAoDcHJzGBEgAygLMhcuY29tcGFzcy52MS5QdWxsUmVxdWVzdBInCgd0cmFja2VyGBIgASgLMhYuY29tcGFzcy52MS5UcmFja2VyUmVmIvsDCgtQdWxsUmVxdWVzdBIjCgVmb3JnZRgBIAEoCzIULmNvbXBhc3MudjEuRm9yZ2VSZWYSDAoEcmVwbxgCIAEoCRIOCgZudW1iZXIYAyABKA0SDQoFdGl0bGUYBCABKAkSEwoLZm9yZ2Vfc3RhdGUYBSABKAkSCwoDdXJsGAYgASgJEhAKCGhlYWRfcmVmGAcgASgJEhAKCGJhc2VfcmVmGAggASgJEisKBWFnZW50GAkgASgLMhwuY29tcGFzcy52MS5BZ2VudEF0dHJpYnV0aW9uEhUKDWZvcmdlX2FjY291bnQYCiABKAkSDQoFZHJhZnQYCyABKAgSKQoHY2hhbmdlZBgMIAEoCzIYLmNvbXBhc3MudjEuQ2hhbmdlZFN0YXRzEikKBmNoZWNrcxgNIAEoCzIZLmNvbXBhc3MudjEuQ2hlY2tzU3VtbWFyeRIjCgdyZXZpZXdzGA4gAygLMhIuY29tcGFzcy52MS5SZXZpZXcSKQoHdGhyZWFkcxgPIAMoCzIYLmNvbXBhc3MudjEuUmV2aWV3VGhyZWFkEhAKCGhlYWRfc2hhGBAgASgJEhcKD21lcmdlYWJsZV9zdGF0ZRgRIAEoCRIwCgttZXJnZV9xdWV1ZRgSIAEoCzIbLmNvbXBhc3MudjEuTWVyZ2VRdWV1ZUVudHJ5IlMKDUNoZWNrc1N1bW1hcnkSEAoIaGVhZF9zaGEYASABKAkSDQoFc3RhdGUYAiABKAkSIQoGY2hlY2tzGAMgAygLMhEuY29tcGFzcy52MS5DaGVjayJDCgVDaGVjaxIMCgRuYW1lGAEgASgJEg0KBXN0YXRlGAIgASgJEgsKA3VybBgDIAEoCRIQCghyZXF1aXJlZBgEIAEoCCJDCgxDaGFuZ2VkU3RhdHMSDQoFZmlsZXMYASABKA0SEQoJYWRkaXRpb25zGAIgASgNEhEKCWRlbGV0aW9ucxgDIAEoDSJpCgpUcmFja2VyUmVmEgwKBGtpbmQYASABKAkSCgoCaWQYAiABKAkSDgoGc3RhdHVzGAMgASgJEgsKA3VybBgEIAEoCRIQCghzdGF0ZV9pZBgFIAEoCRISCgpzdGF0ZV90eXBlGAYgASgJIloKBlJldmlldxIOCgZhdXRob3IYASABKAkSDgoGaXNfYm90GAIgASgIEg8KB3ZlcmRpY3QYAyABKAkSDAoEYm9keRgEIAEoCRIRCgljb21taXRfaWQYBSABKAkiVQoMUmV2aWV3VGhyZWFkEgwKBHBhdGgYASABKAkSEAoIcmVzb2x2ZWQYAiABKAgSJQoIY29tbWVudHMYAyADKAsyEy5jb21wYXNzLnYxLkNvbW1lbnQiNwoHQ29tbWVudBIOCgZhdXRob3IYASABKAkSDgoGaXNfYm90GAIgASgIEgwKBGJvZHkYAyABKAkiYgoPTWVyZ2VRdWV1ZUVudHJ5EhAKCHByb3ZpZGVyGAEgASgJEg0KBXN0YXRlGAIgASgJEi4KCmNoYW5nZWRfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wKmQKDlNlY3JldERlbGl2ZXJ5Eh8KG1NFQ1JFVF9ERUxJVkVSWV9VTlNQRUNJRklFRBAAEhgKFFNFQ1JFVF9ERUxJVkVSWV9GSUxFEAESFwoTU0VDUkVUX0RFTElWRVJZX0VOVhACKnAKClNlY3JldEtpbmQSGwoXU0VDUkVUX0tJTkRfVU5TUEVDSUZJRUQQABIXChNTRUNSRVRfS0lORF9HRU5FUklDEAESGAoUU0VDUkVUX0tJTkRfUFJPVklERVIQAhISCg5TRUNSRVRfS0lORF9HSBADKlsKC1NlY3JldFNjb3BlEhwKGFNFQ1JFVF9TQ09QRV9VTlNQRUNJRklFRBAAEhUKEVNFQ1JFVF9TQ09QRV9VU0VSEAESFwoTU0VDUkVUX1NDT1BFX1RFTkFOVBACKn0KC1RvdXJPdXRjb21lEhwKGFRPVVJfT1VUQ09NRV9VTlNQRUNJRklFRBAAEhgKFFRPVVJfT1VUQ09NRV9TVEFSVEVEEAESGgoWVE9VUl9PVVRDT01FX0RJU01JU1NFRBACEhoKFlRPVVJfT1VUQ09NRV9DT01QTEVURUQQAypDCgtTZXJ2ZXJTdGF0ZRIcChhTRVJWRVJfU1RBVEVfVU5TUEVDSUZJRUQQABIWChJTRVJWRVJfU1RBVEVfUkVBRFkQASqXAQoLUnVudGltZVRpZXISHAoYUlVOVElNRV9USUVSX1VOU1BFQ0lGSUVEEAASFwoTUlVOVElNRV9USUVSX1BPRE1BThABEhgKFFJVTlRJTUVfVElFUl9NSUNST1ZNEAISIAocUlVOVElNRV9USUVSX0FQUExFX0NPTlRBSU5FUhADEhUKEVJVTlRJTUVfVElFUl9IT1NUEAQqaAoNRWdyZXNzUG9zdHVyZRIeChpFR1JFU1NfUE9TVFVSRV9VTlNQRUNJRklFRBAAEhgKFEVHUkVTU19QT1NUVVJFX0FSTUVEEAESHQoZRUdSRVNTX1BPU1RVUkVfVU5FTkZPUkNFRBACKoICChFBZ2VudFNlc3Npb25TdGF0ZRIjCh9BR0VOVF9TRVNTSU9OX1NUQVRFX1VOU1BFQ0lGSUVEEAASIAocQUdFTlRfU0VTU0lPTl9TVEFURV9TVEFSVElORxABEh0KGUFHRU5UX1NFU1NJT05fU1RBVEVfUkVBRFkQAhIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX1dPUktJTkcQAxIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX1NUT1BQRUQQBBIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX0VSUk9SRUQQBRIkCiBBR0VOVF9TRVNTSU9OX1NUQVRFX0RJU0NPTk5FQ1RFRBAGKtIBChNBZ2VudFRvb2xDYWxsU3RhdHVzEiYKIkFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfVU5TUEVDSUZJRUQQABIiCh5BR0VOVF9UT09MX0NBTExfU1RBVFVTX1BFTkRJTkcQARImCiJBR0VOVF9UT09MX0NBTExfU1RBVFVTX0lOX1BST0dSRVNTEAISJAogQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19DT01QTEVURUQQAxIhCh1BR0VOVF9UT09MX0NBTExfU1RBVFVTX0ZBSUxFRBAEKrQBChRBZ2VudFBsYW5FbnRyeVN0YXR1cxInCiNBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19VTlNQRUNJRklFRBAAEiMKH0FHRU5UX1BMQU5fRU5UUllfU1RBVFVTX1BFTkRJTkcQARInCiNBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19JTl9QUk9HUkVTUxACEiUKIUFHRU5UX1BMQU5fRU5UUllfU1RBVFVTX0NPTVBMRVRFRBADKoQBChRTZXNzaW9uSW5qZWN0aW9uS2luZBImCiJTRVNTSU9OX0lOSkVDVElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASIAocU0VTU0lPTl9JTkpFQ1RJT05fS0lORF9TVEVFUhABEiIKHlNFU1NJT05fSU5KRUNUSU9OX0tJTkRfREVMSVZFUhACKnQKEFNlc3Npb25FcnJvcktpbmQSIgoeU0VTU0lPTl9FUlJPUl9LSU5EX1VOU1BFQ0lGSUVEEAASHAoYU0VTU0lPTl9FUlJPUl9LSU5EX0VSUk9SEAESHgoaU0VTU0lPTl9FUlJPUl9LSU5EX0FCT1JURUQQAirxAQoKSXNzdWVTdGF0ZRIbChdJU1NVRV9TVEFURV9VTlNQRUNJRklFRBAAEhcKE0lTU1VFX1NUQVRFX0JBQ0tMT0cQARIUChBJU1NVRV9TVEFURV9UT0RPEAISFgoSSVNTVUVfU1RBVEVfUVVFVUVEEAMSFwoTSVNTVUVfU1RBVEVfQkxPQ0tFRBAEEhsKF0lTU1VFX1NUQVRFX0lOX1BST0dSRVNTEAUSGQoVSVNTVUVfU1RBVEVfSU5fUkVWSUVXEAYSFAoQSVNTVUVfU1RBVEVfRE9ORRAHEhgKFElTU1VFX1NUQVRFX0FSQ0hJVkVEEAgqnAEKDUZvcmdlUHJvdmlkZXISHgoaRk9SR0VfUFJPVklERVJfVU5TUEVDSUZJRUQQABIZChVGT1JHRV9QUk9WSURFUl9HSVRIVUIQARIZChVGT1JHRV9QUk9WSURFUl9HSVRMQUIQAhIaChZGT1JHRV9QUk9WSURFUl9GT1JHRUpPEAMSGQoVRk9SR0VfUFJPVklERVJfTElORUFSEAQqiQEKD0ZvcmdlTWlycm9yTW9kZRIhCh1GT1JHRV9NSVJST1JfTU9ERV9VTlNQRUNJRklFRBAAEhkKFUZPUkdFX01JUlJPUl9NT0RFX09GRhABEhwKGEZPUkdFX01JUlJPUl9NT0RFX01BTlVBTBACEhoKFkZPUkdFX01JUlJPUl9NT0RFX0FVVE8QAzKVFQoOQ29tcGFzc1NlcnZpY2USVAoNR2V0U2VydmVySW5mbxIgLmNvbXBhc3MudjEuR2V0U2VydmVySW5mb1JlcXVlc3QaIS5jb21wYXNzLnYxLkdldFNlcnZlckluZm9SZXNwb25zZRI/CgZXaG9BbUkSGS5jb21wYXNzLnYxLldob0FtSVJlcXVlc3QaGi5jb21wYXNzLnYxLldob0FtSVJlc3BvbnNlEloKD0dldFJlZkxpbmtSdWxlcxIiLmNvbXBhc3MudjEuR2V0UmVmTGlua1J1bGVzUmVxdWVzdBojLmNvbXBhc3MudjEuR2V0UmVmTGlua1J1bGVzUmVzcG9uc2USUQoMR2V0VG91clN0YXRlEh8uY29tcGFzcy52MS5HZXRUb3VyU3RhdGVSZXF1ZXN0GiAuY29tcGFzcy52MS5HZXRUb3VyU3RhdGVSZXNwb25zZRJXCg5DbGFpbVRvdXJTdGFydBIhLmNvbXBhc3MudjEuQ2xhaW1Ub3VyU3RhcnRSZXF1ZXN0GiIuY29tcGFzcy52MS5DbGFpbVRvdXJTdGFydFJlc3BvbnNlElEKDFNldFRvdXJTdGF0ZRIfLmNvbXBhc3MudjEuU2V0VG91clN0YXRlUmVxdWVzdBogLmNvbXBhc3MudjEuU2V0VG91clN0YXRlUmVzcG9uc2USXAoPU3Vic2NyaWJlRXZlbnRzEiIuY29tcGFzcy52MS5TdWJzY3JpYmVFdmVudHNSZXF1ZXN0GiMuY29tcGFzcy52MS5TdWJzY3JpYmVFdmVudHNSZXNwb25zZTABEloKD0xpc3RCb2FyZElzc3VlcxIiLmNvbXBhc3MudjEuTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBojLmNvbXBhc3MudjEuTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USUQoMU2VhcmNoSXNzdWVzEh8uY29tcGFzcy52MS5TZWFyY2hJc3N1ZXNSZXF1ZXN0GiAuY29tcGFzcy52MS5TZWFyY2hJc3N1ZXNSZXNwb25zZRJyChdQcm92aXNpb25BZ2VudFdvcmtzcGFjZRIqLmNvbXBhc3MudjEuUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0GisuY29tcGFzcy52MS5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEmAKEVN0YXJ0QWdlbnRTZXNzaW9uEiQuY29tcGFzcy52MS5TdGFydEFnZW50U2Vzc2lvblJlcXVlc3QaJS5jb21wYXNzLnYxLlN0YXJ0QWdlbnRTZXNzaW9uUmVzcG9uc2USSwoKU3Bhd25BZ2VudBIdLmNvbXBhc3MudjEuU3Bhd25BZ2VudFJlcXVlc3QaHi5jb21wYXNzLnYxLlNwYXduQWdlbnRSZXNwb25zZRJdChBTdG9wQWdlbnRTZXNzaW9uEiMuY29tcGFzcy52MS5TdG9wQWdlbnRTZXNzaW9uUmVxdWVzdBokLmNvbXBhc3MudjEuU3RvcEFnZW50U2Vzc2lvblJlc3BvbnNlEloKD1NraXBCYXRjaFdpbmRvdxIiLmNvbXBhc3MudjEuU2tpcEJhdGNoV2luZG93UmVxdWVzdBojLmNvbXBhc3MudjEuU2tpcEJhdGNoV2luZG93UmVzcG9uc2USaQoUUmVtb3ZlQWdlbnRXb3Jrc3BhY2USJy5jb21wYXNzLnYxLlJlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBooLmNvbXBhc3MudjEuUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJjChJSZWxvYWRBZ2VudFNlc3Npb24SJS5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QaJi5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlc3BvbnNlElcKDkdldEFnZW50U3RhdHVzEiEuY29tcGFzcy52MS5HZXRBZ2VudFN0YXR1c1JlcXVlc3QaIi5jb21wYXNzLnYxLkdldEFnZW50U3RhdHVzUmVzcG9uc2USYgoVU3Vic2NyaWJlQWdlbnRTZXNzaW9uEiguY29tcGFzcy52MS5TdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0Gh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25GcmFtZTABEksKCklzc3VlVG9rZW4SHS5jb21wYXNzLnYxLklzc3VlVG9rZW5SZXF1ZXN0Gh4uY29tcGFzcy52MS5Jc3N1ZVRva2VuUmVzcG9uc2USVAoNUmVuYW1lQWNjb3VudBIgLmNvbXBhc3MudjEuUmVuYW1lQWNjb3VudFJlcXVlc3QaIS5jb21wYXNzLnYxLlJlbmFtZUFjY291bnRSZXNwb25zZRJOCgtSZXZva2VUb2tlbhIeLmNvbXBhc3MudjEuUmV2b2tlVG9rZW5SZXF1ZXN0Gh8uY29tcGFzcy52MS5SZXZva2VUb2tlblJlc3BvbnNlElcKDlB1dEFnZW50Q29uZmlnEiEuY29tcGFzcy52MS5QdXRBZ2VudENvbmZpZ1JlcXVlc3QaIi5jb21wYXNzLnYxLlB1dEFnZW50Q29uZmlnUmVzcG9uc2USYwoSR2V0QWdlbnRDb25maWdJbmZvEiUuY29tcGFzcy52MS5HZXRBZ2VudENvbmZpZ0luZm9SZXF1ZXN0GiYuY29tcGFzcy52MS5HZXRBZ2VudENvbmZpZ0luZm9SZXNwb25zZRJgChFEZWxldGVBZ2VudENvbmZpZxIkLmNvbXBhc3MudjEuRGVsZXRlQWdlbnRDb25maWdSZXF1ZXN0GiUuY29tcGFzcy52MS5EZWxldGVBZ2VudENvbmZpZ1Jlc3BvbnNlEl0KEFB1dE1vZGVsUmVnaXN0cnkSIy5jb21wYXNzLnYxLlB1dE1vZGVsUmVnaXN0cnlSZXF1ZXN0GiQuY29tcGFzcy52MS5QdXRNb2RlbFJlZ2lzdHJ5UmVzcG9uc2USXQoQR2V0TW9kZWxSZWdpc3RyeRIjLmNvbXBhc3MudjEuR2V0TW9kZWxSZWdpc3RyeVJlcXVlc3QaJC5jb21wYXNzLnYxLkdldE1vZGVsUmVnaXN0cnlSZXNwb25zZRJmChNEZWxldGVNb2RlbFJlZ2lzdHJ5EiYuY29tcGFzcy52MS5EZWxldGVNb2RlbFJlZ2lzdHJ5UmVxdWVzdBonLmNvbXBhc3MudjEuRGVsZXRlTW9kZWxSZWdpc3RyeVJlc3BvbnNlEnIKF0xpc3RGb3JnZU1pcnJvclNldHRpbmdzEiouY29tcGFzcy52MS5MaXN0Rm9yZ2VNaXJyb3JTZXR0aW5nc1JlcXVlc3QaKy5jb21wYXNzLnYxLkxpc3RGb3JnZU1pcnJvclNldHRpbmdzUmVzcG9uc2USbAoVU2V0Rm9yZ2VNaXJyb3JTZXR0aW5nEiguY29tcGFzcy52MS5TZXRGb3JnZU1pcnJvclNldHRpbmdSZXF1ZXN0GikuY29tcGFzcy52MS5TZXRGb3JnZU1pcnJvclNldHRpbmdSZXNwb25zZTLfAgoOU2VjcmV0c1NlcnZpY2USSAoJU2V0U2VjcmV0EhwuY29tcGFzcy52MS5TZXRTZWNyZXRSZXF1ZXN0Gh0uY29tcGFzcy52MS5TZXRTZWNyZXRSZXNwb25zZRJOCgtMaXN0U2VjcmV0cxIeLmNvbXBhc3MudjEuTGlzdFNlY3JldHNSZXF1ZXN0Gh8uY29tcGFzcy52MS5MaXN0U2VjcmV0c1Jlc3BvbnNlElEKDERlbGV0ZVNlY3JldBIfLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVxdWVzdBogLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVzcG9uc2USYAoRTGlzdFNlcnZlclNlY3JldHMSJC5jb21wYXNzLnYxLkxpc3RTZXJ2ZXJTZWNyZXRzUmVxdWVzdBolLmNvbXBhc3MudjEuTGlzdFNlcnZlclNlY3JldHNSZXNwb25zZTLfAgoWQWdlbnRSZXBvc2l0b3J5U2VydmljZRJpChRHcmFudEFnZW50UmVwb3NpdG9yeRInLmNvbXBhc3MudjEuR3JhbnRBZ2VudFJlcG9zaXRvcnlSZXF1ZXN0GiguY29tcGFzcy52MS5HcmFudEFnZW50UmVwb3NpdG9yeVJlc3BvbnNlEmwKFVJldm9rZUFnZW50UmVwb3NpdG9yeRIoLmNvbXBhc3MudjEuUmV2b2tlQWdlbnRSZXBvc2l0b3J5UmVxdWVzdBopLmNvbXBhc3MudjEuUmV2b2tlQWdlbnRSZXBvc2l0b3J5UmVzcG9uc2USbAoVTGlzdEFnZW50UmVwb3NpdG9yaWVzEiguY29tcGFzcy52MS5MaXN0QWdlbnRSZXBvc2l0b3JpZXNSZXF1ZXN0GikuY29tcGFzcy52MS5MaXN0QWdlbnRSZXBvc2l0b3JpZXNSZXNwb25zZWIGcHJvdG8z", [file_google_protobuf_timestamp]); /** * @generated from message compass.v1.GrantAgentRepositoryRequest @@ -2607,6 +2607,126 @@ export type ForgeRef = Message<"compass.v1.ForgeRef"> & { export const ForgeRefSchema: GenMessage = /*@__PURE__*/ messageDesc(file_compass_v1_compass, 95); +/** + * The tenant-selected mirror mode for one forge repository or Linear team. + * + * @generated from message compass.v1.ForgeMirrorSetting + */ +export type ForgeMirrorSetting = Message<"compass.v1.ForgeMirrorSetting"> & { + /** + * The provider and host that identify the forge coordinate. + * + * @generated from field: compass.v1.ForgeRef forge = 1; + */ + forge?: ForgeRef | undefined; + + /** + * The repository name or Linear team key on that coordinate. + * + * @generated from field: string repo = 2; + */ + repo: string; + + /** + * The selected mirror policy for the coordinate. + * + * @generated from field: compass.v1.ForgeMirrorMode mode = 3; + */ + mode: ForgeMirrorMode; +}; + +/** + * Describes the message compass.v1.ForgeMirrorSetting. + * Use `create(ForgeMirrorSettingSchema)` to create a new message. + */ +export const ForgeMirrorSettingSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 96); + +/** + * Request to list enabled forge-repository mirror settings. + * + * @generated from message compass.v1.ListForgeMirrorSettingsRequest + */ +export type ListForgeMirrorSettingsRequest = Message<"compass.v1.ListForgeMirrorSettingsRequest"> & { +}; + +/** + * Describes the message compass.v1.ListForgeMirrorSettingsRequest. + * Use `create(ListForgeMirrorSettingsRequestSchema)` to create a new message. + */ +export const ListForgeMirrorSettingsRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 97); + +/** + * The caller's settings for enabled forge repositories. + * + * @generated from message compass.v1.ListForgeMirrorSettingsResponse + */ +export type ListForgeMirrorSettingsResponse = Message<"compass.v1.ListForgeMirrorSettingsResponse"> & { + /** + * Each enabled repository and its selected mirror mode. + * + * @generated from field: repeated compass.v1.ForgeMirrorSetting settings = 1; + */ + settings: ForgeMirrorSetting[]; + + /** + * Whether the authenticated caller can change these settings. + * + * @generated from field: bool can_edit = 2; + */ + canEdit: boolean; +}; + +/** + * Describes the message compass.v1.ListForgeMirrorSettingsResponse. + * Use `create(ListForgeMirrorSettingsResponseSchema)` to create a new message. + */ +export const ListForgeMirrorSettingsResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 98); + +/** + * Request to set a mirror mode for one enabled forge repository. + * + * @generated from message compass.v1.SetForgeMirrorSettingRequest + */ +export type SetForgeMirrorSettingRequest = Message<"compass.v1.SetForgeMirrorSettingRequest"> & { + /** + * The coordinate and mode to set. UNSPECIFIED is invalid. + * + * @generated from field: compass.v1.ForgeMirrorSetting setting = 1; + */ + setting?: ForgeMirrorSetting | undefined; +}; + +/** + * Describes the message compass.v1.SetForgeMirrorSettingRequest. + * Use `create(SetForgeMirrorSettingRequestSchema)` to create a new message. + */ +export const SetForgeMirrorSettingRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 99); + +/** + * The stored setting after a successful update. + * + * @generated from message compass.v1.SetForgeMirrorSettingResponse + */ +export type SetForgeMirrorSettingResponse = Message<"compass.v1.SetForgeMirrorSettingResponse"> & { + /** + * The coordinate and mode that were stored. + * + * @generated from field: compass.v1.ForgeMirrorSetting setting = 1; + */ + setting?: ForgeMirrorSetting | undefined; +}; + +/** + * Describes the message compass.v1.SetForgeMirrorSettingResponse. + * Use `create(SetForgeMirrorSettingResponseSchema)` to create a new message. + */ +export const SetForgeMirrorSettingResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 100); + /** * The board unit: a Compass Issue — the forge issue's fields PLUS the Compass * agent attribution PLUS the Compass machinery, translated from raw forge data @@ -2763,7 +2883,7 @@ export type Issue = Message<"compass.v1.Issue"> & { * Use `create(IssueSchema)` to create a new message. */ export const IssueSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 96); + messageDesc(file_compass_v1_compass, 101); /** * A Compass pull request: the forge PR's fields plus the Compass agent @@ -2891,7 +3011,7 @@ export type PullRequest = Message<"compass.v1.PullRequest"> & { * Use `create(PullRequestSchema)` to create a new message. */ export const PullRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 97); + messageDesc(file_compass_v1_compass, 102); /** * The rolled-up CI + status-check state on a PR head — Compass-owned, populated @@ -2923,7 +3043,7 @@ export type ChecksSummary = Message<"compass.v1.ChecksSummary"> & { * Use `create(ChecksSummarySchema)` to create a new message. */ export const ChecksSummarySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 98); + messageDesc(file_compass_v1_compass, 103); /** * @generated from message compass.v1.Check @@ -2957,7 +3077,7 @@ export type Check = Message<"compass.v1.Check"> & { * Use `create(CheckSchema)` to create a new message. */ export const CheckSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 99); + messageDesc(file_compass_v1_compass, 104); /** * A PR diffstat (files/additions/deletions), carried on PullRequest — a @@ -2987,7 +3107,7 @@ export type ChangedStats = Message<"compass.v1.ChangedStats"> & { * Use `create(ChangedStatsSchema)` to create a new message. */ export const ChangedStatsSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 100); + messageDesc(file_compass_v1_compass, 105); /** * The linked tracker issue — the projection target (DL-032); the @@ -3042,7 +3162,7 @@ export type TrackerRef = Message<"compass.v1.TrackerRef"> & { * Use `create(TrackerRefSchema)` to create a new message. */ export const TrackerRefSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 101); + messageDesc(file_compass_v1_compass, 106); /** * The full review state the right-sidebar PR pane shows — every submitted review @@ -3096,7 +3216,7 @@ export type Review = Message<"compass.v1.Review"> & { * Use `create(ReviewSchema)` to create a new message. */ export const ReviewSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 102); + messageDesc(file_compass_v1_compass, 107); /** * @generated from message compass.v1.ReviewThread @@ -3125,7 +3245,7 @@ export type ReviewThread = Message<"compass.v1.ReviewThread"> & { * Use `create(ReviewThreadSchema)` to create a new message. */ export const ReviewThreadSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 103); + messageDesc(file_compass_v1_compass, 108); /** * @generated from message compass.v1.Comment @@ -3152,7 +3272,7 @@ export type Comment = Message<"compass.v1.Comment"> & { * Use `create(CommentSchema)` to create a new message. */ export const CommentSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 104); + messageDesc(file_compass_v1_compass, 109); /** * The observed state of a pull request in its merge queue. @@ -3187,7 +3307,7 @@ export type MergeQueueEntry = Message<"compass.v1.MergeQueueEntry"> & { * Use `create(MergeQueueEntrySchema)` to create a new message. */ export const MergeQueueEntrySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 105); + messageDesc(file_compass_v1_compass, 110); /** * How a resolved secret is delivered into the agent container. Mirrors @@ -3724,6 +3844,47 @@ export enum ForgeProvider { export const ForgeProviderSchema: GenEnum = /*@__PURE__*/ enumDesc(file_compass_v1_compass, 13); +/** + * Whether asks naming a forge coordinate may be mirrored by an agent or user. + * + * @generated from enum compass.v1.ForgeMirrorMode + */ +export enum ForgeMirrorMode { + /** + * The mode is omitted or invalid. + * + * @generated from enum value: FORGE_MIRROR_MODE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * Mirroring is disabled. No settings row is stored for this mode. + * + * @generated from enum value: FORGE_MIRROR_MODE_OFF = 1; + */ + OFF = 1, + + /** + * A user may click to mirror; agents may not mirror automatically. + * + * @generated from enum value: FORGE_MIRROR_MODE_MANUAL = 2; + */ + MANUAL = 2, + + /** + * A user may click and agents may mirror asks automatically. + * + * @generated from enum value: FORGE_MIRROR_MODE_AUTO = 3; + */ + AUTO = 3, +} + +/** + * Describes the enum compass.v1.ForgeMirrorMode. + */ +export const ForgeMirrorModeSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_compass_v1_compass, 14); + /** * The Compass server service. * @@ -4105,6 +4266,28 @@ export const CompassService: GenService<{ input: typeof DeleteModelRegistryRequestSchema; output: typeof DeleteModelRegistryResponseSchema; }, + /** + * List the tenant's enabled forge-repository mirror settings. Any authenticated + * account may read them; can_edit reports whether the caller may change them. + * + * @generated from rpc compass.v1.CompassService.ListForgeMirrorSettings + */ + listForgeMirrorSettings: { + methodKind: "unary"; + input: typeof ListForgeMirrorSettingsRequestSchema; + output: typeof ListForgeMirrorSettingsResponseSchema; + }, + /** + * Set one tenant's mirror mode for an enabled forge repository. Requires a + * tenant admin; OFF removes the setting row. + * + * @generated from rpc compass.v1.CompassService.SetForgeMirrorSetting + */ + setForgeMirrorSetting: { + methodKind: "unary"; + input: typeof SetForgeMirrorSettingRequestSchema; + output: typeof SetForgeMirrorSettingResponseSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_compass_v1_compass, 0); diff --git a/packages/compass-client/src/gen/compass/v1/comms_pb.ts b/packages/compass-client/src/gen/compass/v1/comms_pb.ts index 18fe2d057..3b37acd23 100644 --- a/packages/compass-client/src/gen/compass/v1/comms_pb.ts +++ b/packages/compass-client/src/gen/compass/v1/comms_pb.ts @@ -29,7 +29,7 @@ import type { Message as Message$1 } from "@bufbuild/protobuf"; * Describes the file compass/v1/comms.proto. */ export const file_compass_v1_comms: GenFile = /*@__PURE__*/ - fileDesc("ChZjb21wYXNzL3YxL2NvbW1zLnByb3RvEgpjb21wYXNzLnYxIsQBCgdBY2NvdW50EgoKAmlkGAEgASgJEg4KBmhhbmRsZRgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSJwoEdXNlchgKIAEoCzIXLmNvbXBhc3MudjEuVXNlckFjY291bnRIABIpCgVhZ2VudBgLIAEoCzIYLmNvbXBhc3MudjEuQWdlbnRBY2NvdW50SAASKwoGc3lzdGVtGAwgASgLMhkuY29tcGFzcy52MS5TeXN0ZW1BY2NvdW50SABCBgoEa2luZCIPCg1TeXN0ZW1BY2NvdW50IjEKC1VzZXJBY2NvdW50EiIKBHJvbGUYASABKA4yFC5jb21wYXNzLnYxLlVzZXJSb2xlIlcKDEFnZW50QWNjb3VudBIVCg1vd25lcl91c2VyX2lkGAEgASgJEhcKD2hvbWVfY2hhbm5lbF9pZBgCIAEoCRIXCg9wYXJlbnRfYWdlbnRfaWQYAyABKAkikAEKDENoYW5uZWxHcm91cBIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEhcKD3BhcmVudF9ncm91cF9pZBgDIAEoCRIVCg1vd25lcl91c2VyX2lkGAQgASgJEjYKCnZpc2liaWxpdHkYBSABKA4yIi5jb21wYXNzLnYxLkNoYW5uZWxHcm91cFZpc2liaWxpdHkijAMKB0NoYW5uZWwSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRIQCghncm91cF9pZBgDIAEoCRIlCgRraW5kGAQgASgOMhcuY29tcGFzcy52MS5DaGFubmVsS2luZBIaChJtZW1iZXJfYWNjb3VudF9pZHMYBSADKAkSHgoWc3Vic2NyaWJlcl9hY2NvdW50X2lkcxgGIAMoCRIyCgtwb3N0X3BvbGljeRgHIAEoDjIdLmNvbXBhc3MudjEuQ2hhbm5lbFBvc3RQb2xpY3kSGAoQb3duZXJfYWNjb3VudF9pZBgIIAEoCRIeChZtYW5kYXRvcnlfc3Vic2NyaXB0aW9uGAkgASgIEi8KDnBpbm5lZF9lbnRyaWVzGAogAygLMhcuY29tcGFzcy52MS5QaW5uZWRFbnRyeRIXCg9wYXJlbnRfYWdlbnRfaWQYCyABKAkSOgoPbWVtYmVyc2hpcF9tb2RlGAwgASgOMiEuY29tcGFzcy52MS5DaGFubmVsTWVtYmVyc2hpcE1vZGUibAoLUGlubmVkRW50cnkSEgoKbWVzc2FnZV9pZBgBIAEoCRIQCghwb3NpdGlvbhgCIAEoBRIZChFwaW5uZWRfYXRfdW5peF9tcxgDIAEoAxIcChRwaW5uZWRfYnlfYWNjb3VudF9pZBgEIAEoCSI2Cg5BZ2VudFdvcmtzcGFjZRIKCgJpZBgBIAEoCRIYChBhZ2VudF9hY2NvdW50X2lkGAIgASgJIvIBCgVUb3BpYxIKCgJpZBgBIAEoCRISCgpjaGFubmVsX2lkGAIgASgJEgwKBG5hbWUYAyABKAkSGgoSY3JlYXRlZF9hdF91bml4X21zGAQgASgDEh0KFWNyZWF0ZWRfYnlfYWNjb3VudF9pZBgFIAEoCRIQCghhcmNoaXZlZBgGIAEoCBIaCg1tZXNzYWdlX2NvdW50GAcgASgESACIAQESJAoXbGFzdF9tZXNzYWdlX2F0X3VuaXhfbXMYCCABKANIAYgBAUIQCg5fbWVzc2FnZV9jb3VudEIaChhfbGFzdF9tZXNzYWdlX2F0X3VuaXhfbXMiwgEKB01lc3NhZ2USCgoCaWQYASABKAkSEAoIdG9waWNfaWQYAiABKAkSGQoRYXV0aG9yX2FjY291bnRfaWQYAyABKAkSEgoKYXRfdW5peF9tcxgEIAEoAxIoCgZibG9ja3MYBSADKAsyGC5jb21wYXNzLnYxLk1lc3NhZ2VCbG9jaxIVCg1hdXRob3JfaGFuZGxlGAYgASgJEhIKCmNoYW5uZWxfaWQYByABKAkSFQoNdHVybl9zZXF1ZW5jZRgIIAEoBCJ5CgxNZXNzYWdlQmxvY2sSDgoEdGV4dBgBIAEoCUgAEh4KA2FzaxgCIAEoCzIPLmNvbXBhc3MudjEuQXNrSAASMAoKYXNrX2Fuc3dlchgDIAEoCzIaLmNvbXBhc3MudjEuQXNrQW5zd2VyQmxvY2tIAEIHCgVibG9jayJICg5Bc2tBbnN3ZXJCbG9jaxIcCgNhc2sYASABKAsyDy5jb21wYXNzLnYxLkFzaxIYChBhc2tlcl9hY2NvdW50X2lkGAIgASgJImAKA0FzaxIOCgZhc2tfaWQYASABKAkSKgoJcXVlc3Rpb25zGAIgAygLMhcuY29tcGFzcy52MS5Bc2tRdWVzdGlvbhIQCghhbnN3ZXJlZBgDIAEoCBILCgNyZWYYBCABKAki8QEKC0Fza1F1ZXN0aW9uEhMKC3F1ZXN0aW9uX2lkGAEgASgJEhAKCHF1ZXN0aW9uGAIgASgJEg4KBmhlYWRlchgDIAEoCRImCgdvcHRpb25zGAQgAygLMhUuY29tcGFzcy52MS5Bc2tPcHRpb24SFgoOYWxsb3dfbXVsdGlwbGUYBSABKAgSGAoLcmVjb21tZW5kZWQYBiABKAVIAIgBARIZChFjaG9zZW5fb3B0aW9uX2lkcxgHIAMoCRITCgtjdXN0b21fdGV4dBgIIAEoCRIRCgl0aW1lZF9vdXQYCSABKAhCDgoMX3JlY29tbWVuZGVkIkwKCUFza09wdGlvbhIKCgJpZBgBIAEoCRINCgVsYWJlbBgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIPCgdwcmV2aWV3GAQgASgJIscFChZTdWJzY3JpYmVDb21tc1Jlc3BvbnNlEgsKA3NlcRgBIAEoBBISCgphdF91bml4X21zGAIgASgDEhYKDmluc3RhbmNlX2Vwb2NoGAMgASgEEhQKDHNuYXBzaG90X3NlcRgEIAEoBBIzCg5tZXNzYWdlX3Bvc3RlZBgKIAEoCzIZLmNvbXBhc3MudjEuTWVzc2FnZVBvc3RlZEgAEjUKD21lc3NhZ2VfdXBkYXRlZBgLIAEoCzIaLmNvbXBhc3MudjEuTWVzc2FnZVVwZGF0ZWRIABI1Cg9jaGFubmVsX2NoYW5nZWQYDCABKAsyGi5jb21wYXNzLnYxLkNoYW5uZWxDaGFuZ2VkSAASQAoVY2hhbm5lbF9ncm91cF9jaGFuZ2VkGA0gASgLMh8uY29tcGFzcy52MS5DaGFubmVsR3JvdXBDaGFuZ2VkSAASNQoPYWNjb3VudF9jaGFuZ2VkGA4gASgLMhouY29tcGFzcy52MS5BY2NvdW50Q2hhbmdlZEgAEkQKF2FnZW50X3dvcmtzcGFjZV9jaGFuZ2VkGA8gASgLMiEuY29tcGFzcy52MS5BZ2VudFdvcmtzcGFjZUNoYW5nZWRIABI6Cg9yZXN5bmNfcmVxdWlyZWQYECABKAsyHy5jb21wYXNzLnYxLkNvbW1zUmVzeW5jUmVxdWlyZWRIABJCChZhZ2VudF9wcmVzZW5jZV9jaGFuZ2VkGBEgASgLMiAuY29tcGFzcy52MS5BZ2VudFByZXNlbmNlQ2hhbmdlZEgAEjMKDnRvcGljX3Vwc2VydGVkGBIgASgLMhkuY29tcGFzcy52MS5Ub3BpY1Vwc2VydGVkSAASPAoTYWdlbnRfYm9hcmRfY2hhbmdlZBgTIAEoCzIdLmNvbXBhc3MudjEuQWdlbnRCb2FyZENoYW5nZWRIAEIJCgdwYXlsb2FkIkwKDU1lc3NhZ2VQb3N0ZWQSJAoHbWVzc2FnZRgBIAEoCzITLmNvbXBhc3MudjEuTWVzc2FnZRIVCg10dXJuX3NlcXVlbmNlGAIgASgEIjYKDk1lc3NhZ2VVcGRhdGVkEiQKB21lc3NhZ2UYASABKAsyEy5jb21wYXNzLnYxLk1lc3NhZ2UiMQoNVG9waWNVcHNlcnRlZBIgCgV0b3BpYxgBIAEoCzIRLmNvbXBhc3MudjEuVG9waWMiUwoOQ2hhbm5lbENoYW5nZWQSJAoHY2hhbm5lbBgBIAEoCzITLmNvbXBhc3MudjEuQ2hhbm5lbBIbChNyZW1vdmVkX2FjY291bnRfaWRzGAIgAygJIj4KE0NoYW5uZWxHcm91cENoYW5nZWQSJwoFZ3JvdXAYASABKAsyGC5jb21wYXNzLnYxLkNoYW5uZWxHcm91cCI2Cg5BY2NvdW50Q2hhbmdlZBIkCgdhY2NvdW50GAEgASgLMhMuY29tcGFzcy52MS5BY2NvdW50IkYKFUFnZW50V29ya3NwYWNlQ2hhbmdlZBItCgl3b3Jrc3BhY2UYASABKAsyGi5jb21wYXNzLnYxLkFnZW50V29ya3NwYWNlIhUKE0NvbW1zUmVzeW5jUmVxdWlyZWQibwoUQWdlbnRQcmVzZW5jZUNoYW5nZWQSGAoQYWdlbnRfYWNjb3VudF9pZBgBIAEoCRIrCghwcmVzZW5jZRgCIAEoDjIZLmNvbXBhc3MudjEuQWdlbnRQcmVzZW5jZRIQCghhY3Rpdml0eRgDIAEoCSJ3CgxCb2FyZFdhaXRpbmcSJgoCb24YASABKA4yGi5jb21wYXNzLnYxLkJvYXJkV2FpdGluZ09uEhQKDGFnZW50X2hhbmRsZRgCIAEoCRIMCgR3aGF0GAMgASgJEgsKA3JlZhgEIAEoCRIOCgZibG9ja3MYBSADKAkihQEKDUJvYXJkU3ViYWdlbnQSCgoCaWQYASABKAkSDQoFYWdlbnQYAiABKAkSEwoLZGVzY3JpcHRpb24YAyABKAkSDgoGc3RhdHVzGAQgASgJEhoKEnN0YXJ0ZWRfYXRfdW5peF9tcxgFIAEoAxIYChBlbmRlZF9hdF91bml4X21zGAYgASgDIvgBCglCb2FyZFRhc2sSCwoDa2V5GAEgASgJEg0KBXRpdGxlGAIgASgJEhMKC3ByaW1hcnlfcmVmGAMgASgJEgwKBHJlZnMYBCADKAkSKQoFc3RhdGUYBSABKA4yGi5jb21wYXNzLnYxLkJvYXJkVGFza1N0YXRlEgwKBG5vdGUYBiABKAkSKQoHd2FpdGluZxgHIAMoCzIYLmNvbXBhc3MudjEuQm9hcmRXYWl0aW5nEiwKCXN1YmFnZW50cxgIIAMoCzIZLmNvbXBhc3MudjEuQm9hcmRTdWJhZ2VudBIaChJ3cml0dGVuX2F0X3VuaXhfbXMYCSABKAMiHQoLQm9hcmRSZWZzT3ASDgoGdmFsdWVzGAEgAygJIjoKDkJvYXJkV2FpdGluZ09wEigKBnZhbHVlcxgBIAMoCzIYLmNvbXBhc3MudjEuQm9hcmRXYWl0aW5nIsQCCg5Cb2FyZFRhc2tQYXRjaBILCgNrZXkYASABKAkSEgoFdGl0bGUYAiABKAlIAIgBARIYCgtwcmltYXJ5X3JlZhgDIAEoCUgBiAEBEi4KBXN0YXRlGAQgASgOMhouY29tcGFzcy52MS5Cb2FyZFRhc2tTdGF0ZUgCiAEBEhEKBG5vdGUYBSABKAlIA4gBARIlCgRyZWZzGAYgASgLMhcuY29tcGFzcy52MS5Cb2FyZFJlZnNPcBIrCgd3YWl0aW5nGAcgASgLMhouY29tcGFzcy52MS5Cb2FyZFdhaXRpbmdPcBIzChB1cHNlcnRfc3ViYWdlbnRzGAggAygLMhkuY29tcGFzcy52MS5Cb2FyZFN1YmFnZW50QggKBl90aXRsZUIOCgxfcHJpbWFyeV9yZWZCCAoGX3N0YXRlQgcKBV9ub3RlIlEKCUJvYXJkSGFsdBInCgRraW5kGAEgASgOMhkuY29tcGFzcy52MS5Cb2FyZEhhbHRLaW5kEgsKA3JlZhgCIAEoCRIOCgZyZWFzb24YAyABKAkijwMKCEJvYXJkUmVmEgsKA3JlZhgBIAEoCRImCgRraW5kGAIgASgOMhguY29tcGFzcy52MS5Cb2FyZFJlZktpbmQSDQoFdGl0bGUYAyABKAkSCwoDdXJsGAQgASgJEhMKC2ZvcmdlX3N0YXRlGAUgASgJEisKC2lzc3VlX3N0YXRlGAYgASgOMhYuY29tcGFzcy52MS5Jc3N1ZVN0YXRlEg0KBWRyYWZ0GAcgASgIEhQKDGNoZWNrc19zdGF0ZRgIIAEoCRIlCgRnYXRlGAkgASgOMhcuY29tcGFzcy52MS5Cb2FyZFByR2F0ZRIVCg1zdGFja190aXBfcmVmGAogASgJEhYKDnN0YWNrX3Bvc2l0aW9uGAsgASgNEg0KBW93bmVkGAwgASgIEhsKE29ic2VydmVkX2F0X3VuaXhfbXMYDSABKAMSGQoRbWVyZ2VfcXVldWVfc3RhdGUYDiABKAkSFgoOc3RhbGVfYXBwcm92YWwYDyABKAgSFgoOdHJhY2tlcl9zdGF0dXMYECABKAkijAEKCEJvYXJkQXNrEg4KBmFza19pZBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJEhIKCmNoYW5uZWxfaWQYAyABKAkSCwoDcmVmGAQgASgJEg4KBmhlYWRlchgFIAEoCRIQCghhbnN3ZXJlZBgGIAEoCBIZChFwb3N0ZWRfYXRfdW5peF9tcxgHIAEoAyLVAgoKQWdlbnRCb2FyZBIYChBhZ2VudF9hY2NvdW50X2lkGAEgASgJEg4KBmhhbmRsZRgCIAEoCRIWCg5mb2N1c190YXNrX2tleRgDIAEoCRIjCgRoYWx0GAQgASgLMhUuY29tcGFzcy52MS5Cb2FyZEhhbHQSJAoFdGFza3MYBSADKAsyFS5jb21wYXNzLnYxLkJvYXJkVGFzaxIiCgRyZWZzGAYgAygLMhQuY29tcGFzcy52MS5Cb2FyZFJlZhIWCg51bnRyYWNrZWRfcmVmcxgHIAMoCRIiCgRhc2tzGAggAygLMhQuY29tcGFzcy52MS5Cb2FyZEFzaxIUCgx3cml0dGVuX3R1cm4YCSABKAQSGgoSd3JpdHRlbl9hdF91bml4X21zGAogASgDEg0KBXN0YWxlGAsgASgIEhkKEW1hdGNoZWRfdGFza19rZXlzGAwgAygJIqMBChFBZ2VudEJvYXJkU3VtbWFyeRINCgVmb2N1cxgBIAEoCRISCgp0YXNrX2NvdW50GAIgASgNEhsKE3dhaXRpbmdfb25fb3BlcmF0b3IYAyABKA0SIwoEaGFsdBgEIAEoCzIVLmNvbXBhc3MudjEuQm9hcmRIYWx0EhoKEndyaXR0ZW5fYXRfdW5peF9tcxgFIAEoAxINCgVzdGFsZRgGIAEoCCL4AQoXVXBkYXRlQWdlbnRCb2FyZFJlcXVlc3QSMAoMdXBzZXJ0X3Rhc2tzGAEgAygLMhouY29tcGFzcy52MS5Cb2FyZFRhc2tQYXRjaBIYChByZW1vdmVfdGFza19rZXlzGAIgAygJEhsKDmZvY3VzX3Rhc2tfa2V5GAMgASgJSACIAQESIwoEaGFsdBgEIAEoCzIVLmNvbXBhc3MudjEuQm9hcmRIYWx0EhIKCmNsZWFyX2hhbHQYBSABKAgSFQoNdHVybl9zZXF1ZW5jZRgGIAEoBBIRCglhdXRvbWF0aWMYByABKAhCEQoPX2ZvY3VzX3Rhc2tfa2V5IkEKGFVwZGF0ZUFnZW50Qm9hcmRSZXNwb25zZRIlCgVib2FyZBgBIAEoCzIWLmNvbXBhc3MudjEuQWdlbnRCb2FyZCLeAQoWTGlzdEFnZW50Qm9hcmRzUmVxdWVzdBIVCg1hZ2VudF9oYW5kbGVzGAEgAygJEg0KBXF1ZXJ5GAIgASgJEgsKA3JlZhgDIAEoCRIuCgp3YWl0aW5nX29uGAQgASgOMhouY29tcGFzcy52MS5Cb2FyZFdhaXRpbmdPbhIpCgVzdGF0ZRgFIAEoDjIaLmNvbXBhc3MudjEuQm9hcmRUYXNrU3RhdGUSJwoEaGFsdBgGIAEoDjIZLmNvbXBhc3MudjEuQm9hcmRIYWx0S2luZBINCgVsaW1pdBgHIAEoDSJBChdMaXN0QWdlbnRCb2FyZHNSZXNwb25zZRImCgZib2FyZHMYASADKAsyFi5jb21wYXNzLnYxLkFnZW50Qm9hcmQiVAoRQWdlbnRCb2FyZENoYW5nZWQSGAoQYWdlbnRfYWNjb3VudF9pZBgBIAEoCRIlCgVib2FyZBgCIAEoCzIWLmNvbXBhc3MudjEuQWdlbnRCb2FyZCJbCgdQZWVyaW5nEhcKD3VzZXJfYWNjb3VudF9pZBgBIAEoCRIOCgZoYW5kbGUYAiABKAkSJwoFc3RhdGUYAyABKA4yGC5jb21wYXNzLnYxLlBlZXJpbmdTdGF0ZSIpChJBcHByb3ZlUGVlclJlcXVlc3QSEwoLcGVlcl9oYW5kbGUYASABKAkiOwoTQXBwcm92ZVBlZXJSZXNwb25zZRIkCgdwZWVyaW5nGAEgASgLMhMuY29tcGFzcy52MS5QZWVyaW5nIigKEVJldm9rZVBlZXJSZXF1ZXN0EhMKC3BlZXJfaGFuZGxlGAEgASgJIiUKElJldm9rZVBlZXJSZXNwb25zZRIPCgdkZWxldGVkGAEgASgIIhIKEExpc3RQZWVyc1JlcXVlc3QiOgoRTGlzdFBlZXJzUmVzcG9uc2USJQoIcGVlcmluZ3MYASADKAsyEy5jb21wYXNzLnYxLlBlZXJpbmciOQoRQ3JlYXRlVXNlclJlcXVlc3QSDgoGaGFuZGxlGAEgASgJEhQKDGRpc3BsYXlfbmFtZRgCIAEoCSI6ChJDcmVhdGVVc2VyUmVzcG9uc2USJAoHYWNjb3VudBgBIAEoCzITLmNvbXBhc3MudjEuQWNjb3VudCJwChJDcmVhdGVBZ2VudFJlcXVlc3QSDgoGaGFuZGxlGAEgASgJEhQKDGRpc3BsYXlfbmFtZRgCIAEoCRIVCg1wYXJlbnRfaGFuZGxlGAMgASgJEgwKBHJvbGUYBCABKAkSDwoHcGVyc29uYRgFIAEoCSI7ChNDcmVhdGVBZ2VudFJlc3BvbnNlEiQKB2FjY291bnQYASABKAsyEy5jb21wYXNzLnYxLkFjY291bnQiKwoTTGlzdEFjY291bnRzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPQoUTGlzdEFjY291bnRzUmVzcG9uc2USJQoIYWNjb3VudHMYASADKAsyEy5jb21wYXNzLnYxLkFjY291bnQilQEKGUNyZWF0ZUNoYW5uZWxHcm91cFJlcXVlc3QSDAoEbmFtZRgBIAEoCRIXCg9wYXJlbnRfZ3JvdXBfaWQYAiABKAkSNgoKdmlzaWJpbGl0eRgDIAEoDjIiLmNvbXBhc3MudjEuQ2hhbm5lbEdyb3VwVmlzaWJpbGl0eRIZChFwYXJlbnRfZ3JvdXBfbmFtZRgEIAEoCSJFChpDcmVhdGVDaGFubmVsR3JvdXBSZXNwb25zZRInCgVncm91cBgBIAEoCzIYLmNvbXBhc3MudjEuQ2hhbm5lbEdyb3VwIjAKGExpc3RDaGFubmVsR3JvdXBzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiRQoZTGlzdENoYW5uZWxHcm91cHNSZXNwb25zZRIoCgZncm91cHMYASADKAsyGC5jb21wYXNzLnYxLkNoYW5uZWxHcm91cCIrChNMaXN0Q2hhbm5lbHNSZXF1ZXN0EhQKDHNuYXBzaG90X3NlcRgBIAEoBCI9ChRMaXN0Q2hhbm5lbHNSZXNwb25zZRIlCghjaGFubmVscxgBIAMoCzITLmNvbXBhc3MudjEuQ2hhbm5lbCLiAQoUQ3JlYXRlQ2hhbm5lbFJlcXVlc3QSDAoEbmFtZRgBIAEoCRIQCghncm91cF9pZBgCIAEoCRIlCgRraW5kGAMgASgOMhcuY29tcGFzcy52MS5DaGFubmVsS2luZBIWCg5tZW1iZXJfaGFuZGxlcxgEIAMoCRIbChNwYXJlbnRfYWdlbnRfaGFuZGxlGAUgASgJEjoKD21lbWJlcnNoaXBfbW9kZRgGIAEoDjIhLmNvbXBhc3MudjEuQ2hhbm5lbE1lbWJlcnNoaXBNb2RlEhIKCmdyb3VwX25hbWUYByABKAkiPQoVQ3JlYXRlQ2hhbm5lbFJlc3BvbnNlEiQKB2NoYW5uZWwYASABKAsyEy5jb21wYXNzLnYxLkNoYW5uZWwiwgEKG1VwZGF0ZUNoYW5uZWxNZW1iZXJzUmVxdWVzdBISCgpjaGFubmVsX2lkGAEgASgJEhoKEmFkZF9tZW1iZXJfaGFuZGxlcxgCIAMoCRIdChVyZW1vdmVfbWVtYmVyX2hhbmRsZXMYAyADKAkSGQoRc3Vic2NyaWJlX2hhbmRsZXMYBCADKAkSGwoTdW5zdWJzY3JpYmVfaGFuZGxlcxgFIAMoCRIcChRjb252ZXJ0X2NoYW5uZWxfbmFtZRgGIAEoCSJEChxVcGRhdGVDaGFubmVsTWVtYmVyc1Jlc3BvbnNlEiQKB2NoYW5uZWwYASABKAsyEy5jb21wYXNzLnYxLkNoYW5uZWwiJAoNT3BlbkRNUmVxdWVzdBITCgtwZWVyX2hhbmRsZRgBIAEoCSJHCg5PcGVuRE1SZXNwb25zZRIkCgdjaGFubmVsGAEgASgLMhMuY29tcGFzcy52MS5DaGFubmVsEg8KB2NyZWF0ZWQYAiABKAgiRwoUUmVwYXJlbnRBZ2VudFJlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJEhkKEW5ld19wYXJlbnRfaGFuZGxlGAIgASgJIj0KFVJlcGFyZW50QWdlbnRSZXNwb25zZRIkCgdhY2NvdW50GAEgASgLMhMuY29tcGFzcy52MS5BY2NvdW50Ik0KFlJlcGFyZW50Q2hhbm5lbFJlcXVlc3QSEgoKY2hhbm5lbF9pZBgBIAEoCRIfChduZXdfcGFyZW50X2FnZW50X2hhbmRsZRgCIAEoCSI/ChdSZXBhcmVudENoYW5uZWxSZXNwb25zZRIkCgdjaGFubmVsGAEgASgLMhMuY29tcGFzcy52MS5DaGFubmVsIpcBChdTZXRDaGFubmVsUG9saWN5UmVxdWVzdBISCgpjaGFubmVsX2lkGAEgASgJEjIKC3Bvc3RfcG9saWN5GAIgASgOMh0uY29tcGFzcy52MS5DaGFubmVsUG9zdFBvbGljeRIUCgxvd25lcl9oYW5kbGUYAyABKAkSHgoWbWFuZGF0b3J5X3N1YnNjcmlwdGlvbhgEIAEoCCJAChhTZXRDaGFubmVsUG9saWN5UmVzcG9uc2USJAoHY2hhbm5lbBgBIAEoCzITLmNvbXBhc3MudjEuQ2hhbm5lbCJSChBHZXRSb3N0ZXJSZXF1ZXN0EiYKBXNjb3BlGAEgASgOMhcuY29tcGFzcy52MS5Sb3N0ZXJTY29wZRIWCg52YW50YWdlX2hhbmRsZRgCIAEoCSI9ChFHZXRSb3N0ZXJSZXNwb25zZRIoCgdlbnRyaWVzGAEgAygLMhcuY29tcGFzcy52MS5Sb3N0ZXJFbnRyeSLwAQoLUm9zdGVyRW50cnkSGAoQYWdlbnRfYWNjb3VudF9pZBgBIAEoCRIOCgZoYW5kbGUYAiABKAkSFAoMZGlzcGxheV9uYW1lGAMgASgJEhcKD3BhcmVudF9hZ2VudF9pZBgEIAEoCRIrCghwcmVzZW5jZRgFIAEoDjIZLmNvbXBhc3MudjEuQWdlbnRQcmVzZW5jZRIQCghhY3Rpdml0eRgGIAEoCRIbChNhY3Rpdml0eV9hdF91bml4X21zGAcgASgDEiwKBWJvYXJkGAggASgLMh0uY29tcGFzcy52MS5BZ2VudEJvYXJkU3VtbWFyeSJ3ChhVcGRhdGVQaW5uZWRCb2FyZFJlcXVlc3QSEgoKY2hhbm5lbF9pZBgBIAEoCRIlCgNwaW4YAiABKAsyFi5jb21wYXNzLnYxLlBpbk1lc3NhZ2VIABIaChB1bnBpbl9tZXNzYWdlX2lkGAMgASgJSABCBAoCb3AiPAoKUGluTWVzc2FnZRISCgptZXNzYWdlX2lkGAEgASgJEhoKEnJlcGxhY2VfbWVzc2FnZV9pZBgCIAEoCSJBChlVcGRhdGVQaW5uZWRCb2FyZFJlc3BvbnNlEiQKB2NoYW5uZWwYASABKAsyEy5jb21wYXNzLnYxLkNoYW5uZWwiMQoZT3BlbkFnZW50V29ya3NwYWNlUmVxdWVzdBIUCgxhZ2VudF9oYW5kbGUYASABKAkiSwoaT3BlbkFnZW50V29ya3NwYWNlUmVzcG9uc2USLQoJd29ya3NwYWNlGAEgASgLMhouY29tcGFzcy52MS5BZ2VudFdvcmtzcGFjZSKKAQoTTGlzdE1lc3NhZ2VzUmVxdWVzdBIUCgpjaGFubmVsX2lkGAEgASgJSAASDQoFbGltaXQYAiABKA0SGQoRYmVmb3JlX21lc3NhZ2VfaWQYAyABKAkSFAoMc25hcHNob3Rfc2VxGAQgASgEEhAKCHRvcGljX2lkGAUgASgJQgsKCWNvbnRhaW5lciI9ChRMaXN0TWVzc2FnZXNSZXNwb25zZRIlCghtZXNzYWdlcxgBIAMoCzITLmNvbXBhc3MudjEuTWVzc2FnZSLcAQoSUG9zdE1lc3NhZ2VSZXF1ZXN0EhQKCmNoYW5uZWxfaWQYASABKAlIABIoCgZibG9ja3MYAiADKAsyGC5jb21wYXNzLnYxLk1lc3NhZ2VCbG9jaxISCgh0b3BpY19pZBgDIAEoCUgBEhQKCnRvcGljX25hbWUYBCABKAlIARIZChFjbGllbnRfcmVxdWVzdF9pZBgFIAEoCRIUCgxjcmVhdGVfdG9waWMYBiABKAgSFQoNdHVybl9zZXF1ZW5jZRgHIAEoBEILCgljb250YWluZXJCBwoFdG9waWMiOwoTUG9zdE1lc3NhZ2VSZXNwb25zZRIkCgdtZXNzYWdlGAEgASgLMhMuY29tcGFzcy52MS5NZXNzYWdlIkEKEUxpc3RUb3BpY3NSZXF1ZXN0EhIKCmNoYW5uZWxfaWQYASABKAkSGAoQaW5jbHVkZV9hcmNoaXZlZBgCIAEoCCI3ChJMaXN0VG9waWNzUmVzcG9uc2USIQoGdG9waWNzGAEgAygLMhEuY29tcGFzcy52MS5Ub3BpYyJmChJVcGRhdGVUb3BpY1JlcXVlc3QSEAoIdG9waWNfaWQYASABKAkSEQoEbmFtZRgCIAEoCUgAiAEBEhUKCGFyY2hpdmVkGAMgASgISAGIAQFCBwoFX25hbWVCCwoJX2FyY2hpdmVkIjcKE1VwZGF0ZVRvcGljUmVzcG9uc2USIAoFdG9waWMYASABKAsyES5jb21wYXNzLnYxLlRvcGljIlUKE1Jlc3BvbmRUb0Fza1JlcXVlc3QSDgoGYXNrX2lkGAEgASgJEi4KB2Fuc3dlcnMYAiADKAsyHS5jb21wYXNzLnYxLkFza1F1ZXN0aW9uQW5zd2VyIlgKEUFza1F1ZXN0aW9uQW5zd2VyEhMKC3F1ZXN0aW9uX2lkGAEgASgJEhkKEWNob3Nlbl9vcHRpb25faWRzGAIgAygJEhMKC2N1c3RvbV90ZXh0GAMgASgJIhYKFFJlc3BvbmRUb0Fza1Jlc3BvbnNlImoKFVNlYXJjaE1lc3NhZ2VzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIUCgpjaGFubmVsX2lkGAIgASgJSAASDQoFbGltaXQYAyABKA0SFAoMc25hcHNob3Rfc2VxGAQgASgEQgcKBXNjb3BlIj8KFlNlYXJjaE1lc3NhZ2VzUmVzcG9uc2USJQoIbWVzc2FnZXMYASADKAsyEy5jb21wYXNzLnYxLk1lc3NhZ2UiQgoVU3Vic2NyaWJlQ29tbXNSZXF1ZXN0EhEKCXNpbmNlX3NlcRgBIAEoBBIWCg5pbnN0YW5jZV9lcG9jaBgCIAEoBCo1CghVc2VyUm9sZRIUChBVU0VSX1JPTEVfTUVNQkVSEAASEwoPVVNFUl9ST0xFX0FETUlOEAEqYQoWQ2hhbm5lbEdyb3VwVmlzaWJpbGl0eRIiCh5DSEFOTkVMX0dST1VQX1ZJU0lCSUxJVFlfT1dORVIQABIjCh9DSEFOTkVMX0dST1VQX1ZJU0lCSUxJVFlfU0hBUkVEEAEqVQoRQ2hhbm5lbFBvc3RQb2xpY3kSHAoYQ0hBTk5FTF9QT1NUX1BPTElDWV9PUEVOEAASIgoeQ0hBTk5FTF9QT1NUX1BPTElDWV9PV05FUl9PTkxZEAEqXwoVQ2hhbm5lbE1lbWJlcnNoaXBNb2RlEiQKIENIQU5ORUxfTUVNQkVSU0hJUF9NT0RFX0VYUExJQ0lUEAASIAocQ0hBTk5FTF9NRU1CRVJTSElQX01PREVfVFJFRRABKlsKC0NoYW5uZWxLaW5kEhgKFENIQU5ORUxfS0lORF9DSEFOTkVMEAASEwoPQ0hBTk5FTF9LSU5EX0RNEAESHQoVQ0hBTk5FTF9LSU5EX0dST1VQX0RNEAIaAggBKpwBCg1BZ2VudFByZXNlbmNlEh4KGkFHRU5UX1BSRVNFTkNFX1VOU1BFQ0lGSUVEEAASFwoTQUdFTlRfUFJFU0VOQ0VfSURMRRABEhoKFkFHRU5UX1BSRVNFTkNFX1dPUktJTkcQAhIaChZBR0VOVF9QUkVTRU5DRV9XQUlUSU5HEAMSGgoWQUdFTlRfUFJFU0VOQ0VfT0ZGTElORRAEKsMBCg5Cb2FyZFRhc2tTdGF0ZRIgChxCT0FSRF9UQVNLX1NUQVRFX1VOU1BFQ0lGSUVEEAASGwoXQk9BUkRfVEFTS19TVEFURV9BQ1RJVkUQARIcChhCT0FSRF9UQVNLX1NUQVRFX0JMT0NLRUQQAhIcChhCT0FSRF9UQVNLX1NUQVRFX1dBSVRJTkcQAxIbChdCT0FSRF9UQVNLX1NUQVRFX1BBUktFRBAEEhkKFUJPQVJEX1RBU0tfU1RBVEVfRE9ORRAFKooBCg1Cb2FyZEhhbHRLaW5kEh8KG0JPQVJEX0hBTFRfS0lORF9VTlNQRUNJRklFRBAAEhgKFEJPQVJEX0hBTFRfS0lORF9ET05FEAESGwoXQk9BUkRfSEFMVF9LSU5EX0JMT0NLRUQQAhIhCh1CT0FSRF9IQUxUX0tJTkRfQVdBSVRJTkdfVVNFUhADKowBCg5Cb2FyZFdhaXRpbmdPbhIgChxCT0FSRF9XQUlUSU5HX09OX1VOU1BFQ0lGSUVEEAASHQoZQk9BUkRfV0FJVElOR19PTl9PUEVSQVRPUhABEhoKFkJPQVJEX1dBSVRJTkdfT05fQUdFTlQQAhIdChlCT0FSRF9XQUlUSU5HX09OX0VYVEVSTkFMEAMqaQoMQm9hcmRSZWZLaW5kEh4KGkJPQVJEX1JFRl9LSU5EX1VOU1BFQ0lGSUVEEAASGAoUQk9BUkRfUkVGX0tJTkRfSVNTVUUQARIfChtCT0FSRF9SRUZfS0lORF9QVUxMX1JFUVVFU1QQAiqgAwoLQm9hcmRQckdhdGUSHQoZQk9BUkRfUFJfR0FURV9VTlNQRUNJRklFRBAAEhcKE0JPQVJEX1BSX0dBVEVfRFJBRlQQARIgChxCT0FSRF9QUl9HQVRFX0NIRUNLU19QRU5ESU5HEAISHwobQk9BUkRfUFJfR0FURV9DSEVDS1NfRkFJTEVEEAMSIwofQk9BUkRfUFJfR0FURV9DSEFOR0VTX1JFUVVFU1RFRBAEEiEKHUJPQVJEX1BSX0dBVEVfQVdBSVRJTkdfUkVWSUVXEAUSIgoeQk9BUkRfUFJfR0FURV9BV0FJVElOR19FTlFVRVVFEAYSGAoUQk9BUkRfUFJfR0FURV9NRVJHRUQQBxIYChRCT0FSRF9QUl9HQVRFX0NMT1NFRBAIEhgKFEJPQVJEX1BSX0dBVEVfUVVFVUVEEAkSHgoaQk9BUkRfUFJfR0FURV9RVUVVRV9GQUlMRUQQChIfChtCT0FSRF9QUl9HQVRFX1FVRVVFX1JFTU9WRUQQCxIbChdCT0FSRF9QUl9HQVRFX0NPTkZMSUNUUxAMKpEBCgxQZWVyaW5nU3RhdGUSHQoZUEVFUklOR19TVEFURV9VTlNQRUNJRklFRBAAEiIKHlBFRVJJTkdfU1RBVEVfUEVORElOR19PVVRHT0lORxABEiIKHlBFRVJJTkdfU1RBVEVfUEVORElOR19JTkNPTUlORxACEhoKFlBFRVJJTkdfU1RBVEVfQVBQUk9WRUQQAypeCgtSb3N0ZXJTY29wZRIdChlST1NURVJfU0NPUEVfTkVJR0hCT1JIT09EEAASGAoUUk9TVEVSX1NDT1BFX1NVQlRSRUUQARIWChJST1NURVJfU0NPUEVfT1dORVIQAjLqEAoMQ29tbXNTZXJ2aWNlEksKCkNyZWF0ZVVzZXISHS5jb21wYXNzLnYxLkNyZWF0ZVVzZXJSZXF1ZXN0Gh4uY29tcGFzcy52MS5DcmVhdGVVc2VyUmVzcG9uc2USTgoLQ3JlYXRlQWdlbnQSHi5jb21wYXNzLnYxLkNyZWF0ZUFnZW50UmVxdWVzdBofLmNvbXBhc3MudjEuQ3JlYXRlQWdlbnRSZXNwb25zZRJRCgxMaXN0QWNjb3VudHMSHy5jb21wYXNzLnYxLkxpc3RBY2NvdW50c1JlcXVlc3QaIC5jb21wYXNzLnYxLkxpc3RBY2NvdW50c1Jlc3BvbnNlEmMKEkNyZWF0ZUNoYW5uZWxHcm91cBIlLmNvbXBhc3MudjEuQ3JlYXRlQ2hhbm5lbEdyb3VwUmVxdWVzdBomLmNvbXBhc3MudjEuQ3JlYXRlQ2hhbm5lbEdyb3VwUmVzcG9uc2USYAoRTGlzdENoYW5uZWxHcm91cHMSJC5jb21wYXNzLnYxLkxpc3RDaGFubmVsR3JvdXBzUmVxdWVzdBolLmNvbXBhc3MudjEuTGlzdENoYW5uZWxHcm91cHNSZXNwb25zZRJRCgxMaXN0Q2hhbm5lbHMSHy5jb21wYXNzLnYxLkxpc3RDaGFubmVsc1JlcXVlc3QaIC5jb21wYXNzLnYxLkxpc3RDaGFubmVsc1Jlc3BvbnNlElQKDUNyZWF0ZUNoYW5uZWwSIC5jb21wYXNzLnYxLkNyZWF0ZUNoYW5uZWxSZXF1ZXN0GiEuY29tcGFzcy52MS5DcmVhdGVDaGFubmVsUmVzcG9uc2USaQoUVXBkYXRlQ2hhbm5lbE1lbWJlcnMSJy5jb21wYXNzLnYxLlVwZGF0ZUNoYW5uZWxNZW1iZXJzUmVxdWVzdBooLmNvbXBhc3MudjEuVXBkYXRlQ2hhbm5lbE1lbWJlcnNSZXNwb25zZRI/CgZPcGVuRE0SGS5jb21wYXNzLnYxLk9wZW5ETVJlcXVlc3QaGi5jb21wYXNzLnYxLk9wZW5ETVJlc3BvbnNlElQKDVJlcGFyZW50QWdlbnQSIC5jb21wYXNzLnYxLlJlcGFyZW50QWdlbnRSZXF1ZXN0GiEuY29tcGFzcy52MS5SZXBhcmVudEFnZW50UmVzcG9uc2USWgoPUmVwYXJlbnRDaGFubmVsEiIuY29tcGFzcy52MS5SZXBhcmVudENoYW5uZWxSZXF1ZXN0GiMuY29tcGFzcy52MS5SZXBhcmVudENoYW5uZWxSZXNwb25zZRJjChJPcGVuQWdlbnRXb3Jrc3BhY2USJS5jb21wYXNzLnYxLk9wZW5BZ2VudFdvcmtzcGFjZVJlcXVlc3QaJi5jb21wYXNzLnYxLk9wZW5BZ2VudFdvcmtzcGFjZVJlc3BvbnNlElEKDExpc3RNZXNzYWdlcxIfLmNvbXBhc3MudjEuTGlzdE1lc3NhZ2VzUmVxdWVzdBogLmNvbXBhc3MudjEuTGlzdE1lc3NhZ2VzUmVzcG9uc2USTgoLUG9zdE1lc3NhZ2USHi5jb21wYXNzLnYxLlBvc3RNZXNzYWdlUmVxdWVzdBofLmNvbXBhc3MudjEuUG9zdE1lc3NhZ2VSZXNwb25zZRJLCgpMaXN0VG9waWNzEh0uY29tcGFzcy52MS5MaXN0VG9waWNzUmVxdWVzdBoeLmNvbXBhc3MudjEuTGlzdFRvcGljc1Jlc3BvbnNlEk4KC1VwZGF0ZVRvcGljEh4uY29tcGFzcy52MS5VcGRhdGVUb3BpY1JlcXVlc3QaHy5jb21wYXNzLnYxLlVwZGF0ZVRvcGljUmVzcG9uc2USUQoMUmVzcG9uZFRvQXNrEh8uY29tcGFzcy52MS5SZXNwb25kVG9Bc2tSZXF1ZXN0GiAuY29tcGFzcy52MS5SZXNwb25kVG9Bc2tSZXNwb25zZRJXCg5TZWFyY2hNZXNzYWdlcxIhLmNvbXBhc3MudjEuU2VhcmNoTWVzc2FnZXNSZXF1ZXN0GiIuY29tcGFzcy52MS5TZWFyY2hNZXNzYWdlc1Jlc3BvbnNlEl0KEFNldENoYW5uZWxQb2xpY3kSIy5jb21wYXNzLnYxLlNldENoYW5uZWxQb2xpY3lSZXF1ZXN0GiQuY29tcGFzcy52MS5TZXRDaGFubmVsUG9saWN5UmVzcG9uc2USSAoJR2V0Um9zdGVyEhwuY29tcGFzcy52MS5HZXRSb3N0ZXJSZXF1ZXN0Gh0uY29tcGFzcy52MS5HZXRSb3N0ZXJSZXNwb25zZRJgChFVcGRhdGVQaW5uZWRCb2FyZBIkLmNvbXBhc3MudjEuVXBkYXRlUGlubmVkQm9hcmRSZXF1ZXN0GiUuY29tcGFzcy52MS5VcGRhdGVQaW5uZWRCb2FyZFJlc3BvbnNlEk4KC0FwcHJvdmVQZWVyEh4uY29tcGFzcy52MS5BcHByb3ZlUGVlclJlcXVlc3QaHy5jb21wYXNzLnYxLkFwcHJvdmVQZWVyUmVzcG9uc2USSwoKUmV2b2tlUGVlchIdLmNvbXBhc3MudjEuUmV2b2tlUGVlclJlcXVlc3QaHi5jb21wYXNzLnYxLlJldm9rZVBlZXJSZXNwb25zZRJICglMaXN0UGVlcnMSHC5jb21wYXNzLnYxLkxpc3RQZWVyc1JlcXVlc3QaHS5jb21wYXNzLnYxLkxpc3RQZWVyc1Jlc3BvbnNlElkKDlN1YnNjcmliZUNvbW1zEiEuY29tcGFzcy52MS5TdWJzY3JpYmVDb21tc1JlcXVlc3QaIi5jb21wYXNzLnYxLlN1YnNjcmliZUNvbW1zUmVzcG9uc2UwAWIGcHJvdG8z", [file_compass_v1_compass]); + fileDesc("ChZjb21wYXNzL3YxL2NvbW1zLnByb3RvEgpjb21wYXNzLnYxIsQBCgdBY2NvdW50EgoKAmlkGAEgASgJEg4KBmhhbmRsZRgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSJwoEdXNlchgKIAEoCzIXLmNvbXBhc3MudjEuVXNlckFjY291bnRIABIpCgVhZ2VudBgLIAEoCzIYLmNvbXBhc3MudjEuQWdlbnRBY2NvdW50SAASKwoGc3lzdGVtGAwgASgLMhkuY29tcGFzcy52MS5TeXN0ZW1BY2NvdW50SABCBgoEa2luZCIPCg1TeXN0ZW1BY2NvdW50IjEKC1VzZXJBY2NvdW50EiIKBHJvbGUYASABKA4yFC5jb21wYXNzLnYxLlVzZXJSb2xlIlcKDEFnZW50QWNjb3VudBIVCg1vd25lcl91c2VyX2lkGAEgASgJEhcKD2hvbWVfY2hhbm5lbF9pZBgCIAEoCRIXCg9wYXJlbnRfYWdlbnRfaWQYAyABKAkikAEKDENoYW5uZWxHcm91cBIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEhcKD3BhcmVudF9ncm91cF9pZBgDIAEoCRIVCg1vd25lcl91c2VyX2lkGAQgASgJEjYKCnZpc2liaWxpdHkYBSABKA4yIi5jb21wYXNzLnYxLkNoYW5uZWxHcm91cFZpc2liaWxpdHkijAMKB0NoYW5uZWwSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRIQCghncm91cF9pZBgDIAEoCRIlCgRraW5kGAQgASgOMhcuY29tcGFzcy52MS5DaGFubmVsS2luZBIaChJtZW1iZXJfYWNjb3VudF9pZHMYBSADKAkSHgoWc3Vic2NyaWJlcl9hY2NvdW50X2lkcxgGIAMoCRIyCgtwb3N0X3BvbGljeRgHIAEoDjIdLmNvbXBhc3MudjEuQ2hhbm5lbFBvc3RQb2xpY3kSGAoQb3duZXJfYWNjb3VudF9pZBgIIAEoCRIeChZtYW5kYXRvcnlfc3Vic2NyaXB0aW9uGAkgASgIEi8KDnBpbm5lZF9lbnRyaWVzGAogAygLMhcuY29tcGFzcy52MS5QaW5uZWRFbnRyeRIXCg9wYXJlbnRfYWdlbnRfaWQYCyABKAkSOgoPbWVtYmVyc2hpcF9tb2RlGAwgASgOMiEuY29tcGFzcy52MS5DaGFubmVsTWVtYmVyc2hpcE1vZGUibAoLUGlubmVkRW50cnkSEgoKbWVzc2FnZV9pZBgBIAEoCRIQCghwb3NpdGlvbhgCIAEoBRIZChFwaW5uZWRfYXRfdW5peF9tcxgDIAEoAxIcChRwaW5uZWRfYnlfYWNjb3VudF9pZBgEIAEoCSI2Cg5BZ2VudFdvcmtzcGFjZRIKCgJpZBgBIAEoCRIYChBhZ2VudF9hY2NvdW50X2lkGAIgASgJIvIBCgVUb3BpYxIKCgJpZBgBIAEoCRISCgpjaGFubmVsX2lkGAIgASgJEgwKBG5hbWUYAyABKAkSGgoSY3JlYXRlZF9hdF91bml4X21zGAQgASgDEh0KFWNyZWF0ZWRfYnlfYWNjb3VudF9pZBgFIAEoCRIQCghhcmNoaXZlZBgGIAEoCBIaCg1tZXNzYWdlX2NvdW50GAcgASgESACIAQESJAoXbGFzdF9tZXNzYWdlX2F0X3VuaXhfbXMYCCABKANIAYgBAUIQCg5fbWVzc2FnZV9jb3VudEIaChhfbGFzdF9tZXNzYWdlX2F0X3VuaXhfbXMiwgEKB01lc3NhZ2USCgoCaWQYASABKAkSEAoIdG9waWNfaWQYAiABKAkSGQoRYXV0aG9yX2FjY291bnRfaWQYAyABKAkSEgoKYXRfdW5peF9tcxgEIAEoAxIoCgZibG9ja3MYBSADKAsyGC5jb21wYXNzLnYxLk1lc3NhZ2VCbG9jaxIVCg1hdXRob3JfaGFuZGxlGAYgASgJEhIKCmNoYW5uZWxfaWQYByABKAkSFQoNdHVybl9zZXF1ZW5jZRgIIAEoBCJ5CgxNZXNzYWdlQmxvY2sSDgoEdGV4dBgBIAEoCUgAEh4KA2FzaxgCIAEoCzIPLmNvbXBhc3MudjEuQXNrSAASMAoKYXNrX2Fuc3dlchgDIAEoCzIaLmNvbXBhc3MudjEuQXNrQW5zd2VyQmxvY2tIAEIHCgVibG9jayJICg5Bc2tBbnN3ZXJCbG9jaxIcCgNhc2sYASABKAsyDy5jb21wYXNzLnYxLkFzaxIYChBhc2tlcl9hY2NvdW50X2lkGAIgASgJImAKA0FzaxIOCgZhc2tfaWQYASABKAkSKgoJcXVlc3Rpb25zGAIgAygLMhcuY29tcGFzcy52MS5Bc2tRdWVzdGlvbhIQCghhbnN3ZXJlZBgDIAEoCBILCgNyZWYYBCABKAki8QEKC0Fza1F1ZXN0aW9uEhMKC3F1ZXN0aW9uX2lkGAEgASgJEhAKCHF1ZXN0aW9uGAIgASgJEg4KBmhlYWRlchgDIAEoCRImCgdvcHRpb25zGAQgAygLMhUuY29tcGFzcy52MS5Bc2tPcHRpb24SFgoOYWxsb3dfbXVsdGlwbGUYBSABKAgSGAoLcmVjb21tZW5kZWQYBiABKAVIAIgBARIZChFjaG9zZW5fb3B0aW9uX2lkcxgHIAMoCRITCgtjdXN0b21fdGV4dBgIIAEoCRIRCgl0aW1lZF9vdXQYCSABKAhCDgoMX3JlY29tbWVuZGVkIkwKCUFza09wdGlvbhIKCgJpZBgBIAEoCRINCgVsYWJlbBgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIPCgdwcmV2aWV3GAQgASgJIscFChZTdWJzY3JpYmVDb21tc1Jlc3BvbnNlEgsKA3NlcRgBIAEoBBISCgphdF91bml4X21zGAIgASgDEhYKDmluc3RhbmNlX2Vwb2NoGAMgASgEEhQKDHNuYXBzaG90X3NlcRgEIAEoBBIzCg5tZXNzYWdlX3Bvc3RlZBgKIAEoCzIZLmNvbXBhc3MudjEuTWVzc2FnZVBvc3RlZEgAEjUKD21lc3NhZ2VfdXBkYXRlZBgLIAEoCzIaLmNvbXBhc3MudjEuTWVzc2FnZVVwZGF0ZWRIABI1Cg9jaGFubmVsX2NoYW5nZWQYDCABKAsyGi5jb21wYXNzLnYxLkNoYW5uZWxDaGFuZ2VkSAASQAoVY2hhbm5lbF9ncm91cF9jaGFuZ2VkGA0gASgLMh8uY29tcGFzcy52MS5DaGFubmVsR3JvdXBDaGFuZ2VkSAASNQoPYWNjb3VudF9jaGFuZ2VkGA4gASgLMhouY29tcGFzcy52MS5BY2NvdW50Q2hhbmdlZEgAEkQKF2FnZW50X3dvcmtzcGFjZV9jaGFuZ2VkGA8gASgLMiEuY29tcGFzcy52MS5BZ2VudFdvcmtzcGFjZUNoYW5nZWRIABI6Cg9yZXN5bmNfcmVxdWlyZWQYECABKAsyHy5jb21wYXNzLnYxLkNvbW1zUmVzeW5jUmVxdWlyZWRIABJCChZhZ2VudF9wcmVzZW5jZV9jaGFuZ2VkGBEgASgLMiAuY29tcGFzcy52MS5BZ2VudFByZXNlbmNlQ2hhbmdlZEgAEjMKDnRvcGljX3Vwc2VydGVkGBIgASgLMhkuY29tcGFzcy52MS5Ub3BpY1Vwc2VydGVkSAASPAoTYWdlbnRfYm9hcmRfY2hhbmdlZBgTIAEoCzIdLmNvbXBhc3MudjEuQWdlbnRCb2FyZENoYW5nZWRIAEIJCgdwYXlsb2FkIkwKDU1lc3NhZ2VQb3N0ZWQSJAoHbWVzc2FnZRgBIAEoCzITLmNvbXBhc3MudjEuTWVzc2FnZRIVCg10dXJuX3NlcXVlbmNlGAIgASgEIjYKDk1lc3NhZ2VVcGRhdGVkEiQKB21lc3NhZ2UYASABKAsyEy5jb21wYXNzLnYxLk1lc3NhZ2UiMQoNVG9waWNVcHNlcnRlZBIgCgV0b3BpYxgBIAEoCzIRLmNvbXBhc3MudjEuVG9waWMiUwoOQ2hhbm5lbENoYW5nZWQSJAoHY2hhbm5lbBgBIAEoCzITLmNvbXBhc3MudjEuQ2hhbm5lbBIbChNyZW1vdmVkX2FjY291bnRfaWRzGAIgAygJIj4KE0NoYW5uZWxHcm91cENoYW5nZWQSJwoFZ3JvdXAYASABKAsyGC5jb21wYXNzLnYxLkNoYW5uZWxHcm91cCI2Cg5BY2NvdW50Q2hhbmdlZBIkCgdhY2NvdW50GAEgASgLMhMuY29tcGFzcy52MS5BY2NvdW50IkYKFUFnZW50V29ya3NwYWNlQ2hhbmdlZBItCgl3b3Jrc3BhY2UYASABKAsyGi5jb21wYXNzLnYxLkFnZW50V29ya3NwYWNlIhUKE0NvbW1zUmVzeW5jUmVxdWlyZWQibwoUQWdlbnRQcmVzZW5jZUNoYW5nZWQSGAoQYWdlbnRfYWNjb3VudF9pZBgBIAEoCRIrCghwcmVzZW5jZRgCIAEoDjIZLmNvbXBhc3MudjEuQWdlbnRQcmVzZW5jZRIQCghhY3Rpdml0eRgDIAEoCSJ3CgxCb2FyZFdhaXRpbmcSJgoCb24YASABKA4yGi5jb21wYXNzLnYxLkJvYXJkV2FpdGluZ09uEhQKDGFnZW50X2hhbmRsZRgCIAEoCRIMCgR3aGF0GAMgASgJEgsKA3JlZhgEIAEoCRIOCgZibG9ja3MYBSADKAkihQEKDUJvYXJkU3ViYWdlbnQSCgoCaWQYASABKAkSDQoFYWdlbnQYAiABKAkSEwoLZGVzY3JpcHRpb24YAyABKAkSDgoGc3RhdHVzGAQgASgJEhoKEnN0YXJ0ZWRfYXRfdW5peF9tcxgFIAEoAxIYChBlbmRlZF9hdF91bml4X21zGAYgASgDIvgBCglCb2FyZFRhc2sSCwoDa2V5GAEgASgJEg0KBXRpdGxlGAIgASgJEhMKC3ByaW1hcnlfcmVmGAMgASgJEgwKBHJlZnMYBCADKAkSKQoFc3RhdGUYBSABKA4yGi5jb21wYXNzLnYxLkJvYXJkVGFza1N0YXRlEgwKBG5vdGUYBiABKAkSKQoHd2FpdGluZxgHIAMoCzIYLmNvbXBhc3MudjEuQm9hcmRXYWl0aW5nEiwKCXN1YmFnZW50cxgIIAMoCzIZLmNvbXBhc3MudjEuQm9hcmRTdWJhZ2VudBIaChJ3cml0dGVuX2F0X3VuaXhfbXMYCSABKAMiHQoLQm9hcmRSZWZzT3ASDgoGdmFsdWVzGAEgAygJIjoKDkJvYXJkV2FpdGluZ09wEigKBnZhbHVlcxgBIAMoCzIYLmNvbXBhc3MudjEuQm9hcmRXYWl0aW5nIsQCCg5Cb2FyZFRhc2tQYXRjaBILCgNrZXkYASABKAkSEgoFdGl0bGUYAiABKAlIAIgBARIYCgtwcmltYXJ5X3JlZhgDIAEoCUgBiAEBEi4KBXN0YXRlGAQgASgOMhouY29tcGFzcy52MS5Cb2FyZFRhc2tTdGF0ZUgCiAEBEhEKBG5vdGUYBSABKAlIA4gBARIlCgRyZWZzGAYgASgLMhcuY29tcGFzcy52MS5Cb2FyZFJlZnNPcBIrCgd3YWl0aW5nGAcgASgLMhouY29tcGFzcy52MS5Cb2FyZFdhaXRpbmdPcBIzChB1cHNlcnRfc3ViYWdlbnRzGAggAygLMhkuY29tcGFzcy52MS5Cb2FyZFN1YmFnZW50QggKBl90aXRsZUIOCgxfcHJpbWFyeV9yZWZCCAoGX3N0YXRlQgcKBV9ub3RlIlEKCUJvYXJkSGFsdBInCgRraW5kGAEgASgOMhkuY29tcGFzcy52MS5Cb2FyZEhhbHRLaW5kEgsKA3JlZhgCIAEoCRIOCgZyZWFzb24YAyABKAkijwMKCEJvYXJkUmVmEgsKA3JlZhgBIAEoCRImCgRraW5kGAIgASgOMhguY29tcGFzcy52MS5Cb2FyZFJlZktpbmQSDQoFdGl0bGUYAyABKAkSCwoDdXJsGAQgASgJEhMKC2ZvcmdlX3N0YXRlGAUgASgJEisKC2lzc3VlX3N0YXRlGAYgASgOMhYuY29tcGFzcy52MS5Jc3N1ZVN0YXRlEg0KBWRyYWZ0GAcgASgIEhQKDGNoZWNrc19zdGF0ZRgIIAEoCRIlCgRnYXRlGAkgASgOMhcuY29tcGFzcy52MS5Cb2FyZFByR2F0ZRIVCg1zdGFja190aXBfcmVmGAogASgJEhYKDnN0YWNrX3Bvc2l0aW9uGAsgASgNEg0KBW93bmVkGAwgASgIEhsKE29ic2VydmVkX2F0X3VuaXhfbXMYDSABKAMSGQoRbWVyZ2VfcXVldWVfc3RhdGUYDiABKAkSFgoOc3RhbGVfYXBwcm92YWwYDyABKAgSFgoOdHJhY2tlcl9zdGF0dXMYECABKAki1AEKCEJvYXJkQXNrEg4KBmFza19pZBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJEhIKCmNoYW5uZWxfaWQYAyABKAkSCwoDcmVmGAQgASgJEg4KBmhlYWRlchgFIAEoCRIQCghhbnN3ZXJlZBgGIAEoCBIZChFwb3N0ZWRfYXRfdW5peF9tcxgHIAEoAxIyCgxtaXJyb3Jfc3RhdGUYCCABKA4yHC5jb21wYXNzLnYxLkJvYXJkTWlycm9yU3RhdGUSEgoKbWlycm9yX3VybBgJIAEoCSJuChRNaXJyb3JUb0ZvcmdlUmVxdWVzdBILCgNyZWYYASABKAkSDgoGYXNrX2lkGAIgASgJEhAKCHRvcGljX2lkGAMgASgJEhMKC21lc3NhZ2VfaWRzGAQgAygJEhIKCmNoYW5uZWxfaWQYBSABKAkiLAoVTWlycm9yVG9Gb3JnZVJlc3BvbnNlEhMKC2NvbW1lbnRfdXJsGAEgASgJItUCCgpBZ2VudEJvYXJkEhgKEGFnZW50X2FjY291bnRfaWQYASABKAkSDgoGaGFuZGxlGAIgASgJEhYKDmZvY3VzX3Rhc2tfa2V5GAMgASgJEiMKBGhhbHQYBCABKAsyFS5jb21wYXNzLnYxLkJvYXJkSGFsdBIkCgV0YXNrcxgFIAMoCzIVLmNvbXBhc3MudjEuQm9hcmRUYXNrEiIKBHJlZnMYBiADKAsyFC5jb21wYXNzLnYxLkJvYXJkUmVmEhYKDnVudHJhY2tlZF9yZWZzGAcgAygJEiIKBGFza3MYCCADKAsyFC5jb21wYXNzLnYxLkJvYXJkQXNrEhQKDHdyaXR0ZW5fdHVybhgJIAEoBBIaChJ3cml0dGVuX2F0X3VuaXhfbXMYCiABKAMSDQoFc3RhbGUYCyABKAgSGQoRbWF0Y2hlZF90YXNrX2tleXMYDCADKAkiowEKEUFnZW50Qm9hcmRTdW1tYXJ5Eg0KBWZvY3VzGAEgASgJEhIKCnRhc2tfY291bnQYAiABKA0SGwoTd2FpdGluZ19vbl9vcGVyYXRvchgDIAEoDRIjCgRoYWx0GAQgASgLMhUuY29tcGFzcy52MS5Cb2FyZEhhbHQSGgoSd3JpdHRlbl9hdF91bml4X21zGAUgASgDEg0KBXN0YWxlGAYgASgIIvgBChdVcGRhdGVBZ2VudEJvYXJkUmVxdWVzdBIwCgx1cHNlcnRfdGFza3MYASADKAsyGi5jb21wYXNzLnYxLkJvYXJkVGFza1BhdGNoEhgKEHJlbW92ZV90YXNrX2tleXMYAiADKAkSGwoOZm9jdXNfdGFza19rZXkYAyABKAlIAIgBARIjCgRoYWx0GAQgASgLMhUuY29tcGFzcy52MS5Cb2FyZEhhbHQSEgoKY2xlYXJfaGFsdBgFIAEoCBIVCg10dXJuX3NlcXVlbmNlGAYgASgEEhEKCWF1dG9tYXRpYxgHIAEoCEIRCg9fZm9jdXNfdGFza19rZXkiQQoYVXBkYXRlQWdlbnRCb2FyZFJlc3BvbnNlEiUKBWJvYXJkGAEgASgLMhYuY29tcGFzcy52MS5BZ2VudEJvYXJkIt4BChZMaXN0QWdlbnRCb2FyZHNSZXF1ZXN0EhUKDWFnZW50X2hhbmRsZXMYASADKAkSDQoFcXVlcnkYAiABKAkSCwoDcmVmGAMgASgJEi4KCndhaXRpbmdfb24YBCABKA4yGi5jb21wYXNzLnYxLkJvYXJkV2FpdGluZ09uEikKBXN0YXRlGAUgASgOMhouY29tcGFzcy52MS5Cb2FyZFRhc2tTdGF0ZRInCgRoYWx0GAYgASgOMhkuY29tcGFzcy52MS5Cb2FyZEhhbHRLaW5kEg0KBWxpbWl0GAcgASgNIkEKF0xpc3RBZ2VudEJvYXJkc1Jlc3BvbnNlEiYKBmJvYXJkcxgBIAMoCzIWLmNvbXBhc3MudjEuQWdlbnRCb2FyZCJUChFBZ2VudEJvYXJkQ2hhbmdlZBIYChBhZ2VudF9hY2NvdW50X2lkGAEgASgJEiUKBWJvYXJkGAIgASgLMhYuY29tcGFzcy52MS5BZ2VudEJvYXJkIlsKB1BlZXJpbmcSFwoPdXNlcl9hY2NvdW50X2lkGAEgASgJEg4KBmhhbmRsZRgCIAEoCRInCgVzdGF0ZRgDIAEoDjIYLmNvbXBhc3MudjEuUGVlcmluZ1N0YXRlIikKEkFwcHJvdmVQZWVyUmVxdWVzdBITCgtwZWVyX2hhbmRsZRgBIAEoCSI7ChNBcHByb3ZlUGVlclJlc3BvbnNlEiQKB3BlZXJpbmcYASABKAsyEy5jb21wYXNzLnYxLlBlZXJpbmciKAoRUmV2b2tlUGVlclJlcXVlc3QSEwoLcGVlcl9oYW5kbGUYASABKAkiJQoSUmV2b2tlUGVlclJlc3BvbnNlEg8KB2RlbGV0ZWQYASABKAgiEgoQTGlzdFBlZXJzUmVxdWVzdCI6ChFMaXN0UGVlcnNSZXNwb25zZRIlCghwZWVyaW5ncxgBIAMoCzITLmNvbXBhc3MudjEuUGVlcmluZyI5ChFDcmVhdGVVc2VyUmVxdWVzdBIOCgZoYW5kbGUYASABKAkSFAoMZGlzcGxheV9uYW1lGAIgASgJIjoKEkNyZWF0ZVVzZXJSZXNwb25zZRIkCgdhY2NvdW50GAEgASgLMhMuY29tcGFzcy52MS5BY2NvdW50InAKEkNyZWF0ZUFnZW50UmVxdWVzdBIOCgZoYW5kbGUYASABKAkSFAoMZGlzcGxheV9uYW1lGAIgASgJEhUKDXBhcmVudF9oYW5kbGUYAyABKAkSDAoEcm9sZRgEIAEoCRIPCgdwZXJzb25hGAUgASgJIjsKE0NyZWF0ZUFnZW50UmVzcG9uc2USJAoHYWNjb3VudBgBIAEoCzITLmNvbXBhc3MudjEuQWNjb3VudCIrChNMaXN0QWNjb3VudHNSZXF1ZXN0EhQKDHNuYXBzaG90X3NlcRgBIAEoBCI9ChRMaXN0QWNjb3VudHNSZXNwb25zZRIlCghhY2NvdW50cxgBIAMoCzITLmNvbXBhc3MudjEuQWNjb3VudCKVAQoZQ3JlYXRlQ2hhbm5lbEdyb3VwUmVxdWVzdBIMCgRuYW1lGAEgASgJEhcKD3BhcmVudF9ncm91cF9pZBgCIAEoCRI2Cgp2aXNpYmlsaXR5GAMgASgOMiIuY29tcGFzcy52MS5DaGFubmVsR3JvdXBWaXNpYmlsaXR5EhkKEXBhcmVudF9ncm91cF9uYW1lGAQgASgJIkUKGkNyZWF0ZUNoYW5uZWxHcm91cFJlc3BvbnNlEicKBWdyb3VwGAEgASgLMhguY29tcGFzcy52MS5DaGFubmVsR3JvdXAiMAoYTGlzdENoYW5uZWxHcm91cHNSZXF1ZXN0EhQKDHNuYXBzaG90X3NlcRgBIAEoBCJFChlMaXN0Q2hhbm5lbEdyb3Vwc1Jlc3BvbnNlEigKBmdyb3VwcxgBIAMoCzIYLmNvbXBhc3MudjEuQ2hhbm5lbEdyb3VwIisKE0xpc3RDaGFubmVsc1JlcXVlc3QSFAoMc25hcHNob3Rfc2VxGAEgASgEIj0KFExpc3RDaGFubmVsc1Jlc3BvbnNlEiUKCGNoYW5uZWxzGAEgAygLMhMuY29tcGFzcy52MS5DaGFubmVsIuIBChRDcmVhdGVDaGFubmVsUmVxdWVzdBIMCgRuYW1lGAEgASgJEhAKCGdyb3VwX2lkGAIgASgJEiUKBGtpbmQYAyABKA4yFy5jb21wYXNzLnYxLkNoYW5uZWxLaW5kEhYKDm1lbWJlcl9oYW5kbGVzGAQgAygJEhsKE3BhcmVudF9hZ2VudF9oYW5kbGUYBSABKAkSOgoPbWVtYmVyc2hpcF9tb2RlGAYgASgOMiEuY29tcGFzcy52MS5DaGFubmVsTWVtYmVyc2hpcE1vZGUSEgoKZ3JvdXBfbmFtZRgHIAEoCSI9ChVDcmVhdGVDaGFubmVsUmVzcG9uc2USJAoHY2hhbm5lbBgBIAEoCzITLmNvbXBhc3MudjEuQ2hhbm5lbCLCAQobVXBkYXRlQ2hhbm5lbE1lbWJlcnNSZXF1ZXN0EhIKCmNoYW5uZWxfaWQYASABKAkSGgoSYWRkX21lbWJlcl9oYW5kbGVzGAIgAygJEh0KFXJlbW92ZV9tZW1iZXJfaGFuZGxlcxgDIAMoCRIZChFzdWJzY3JpYmVfaGFuZGxlcxgEIAMoCRIbChN1bnN1YnNjcmliZV9oYW5kbGVzGAUgAygJEhwKFGNvbnZlcnRfY2hhbm5lbF9uYW1lGAYgASgJIkQKHFVwZGF0ZUNoYW5uZWxNZW1iZXJzUmVzcG9uc2USJAoHY2hhbm5lbBgBIAEoCzITLmNvbXBhc3MudjEuQ2hhbm5lbCIkCg1PcGVuRE1SZXF1ZXN0EhMKC3BlZXJfaGFuZGxlGAEgASgJIkcKDk9wZW5ETVJlc3BvbnNlEiQKB2NoYW5uZWwYASABKAsyEy5jb21wYXNzLnYxLkNoYW5uZWwSDwoHY3JlYXRlZBgCIAEoCCJHChRSZXBhcmVudEFnZW50UmVxdWVzdBIUCgxhZ2VudF9oYW5kbGUYASABKAkSGQoRbmV3X3BhcmVudF9oYW5kbGUYAiABKAkiPQoVUmVwYXJlbnRBZ2VudFJlc3BvbnNlEiQKB2FjY291bnQYASABKAsyEy5jb21wYXNzLnYxLkFjY291bnQiTQoWUmVwYXJlbnRDaGFubmVsUmVxdWVzdBISCgpjaGFubmVsX2lkGAEgASgJEh8KF25ld19wYXJlbnRfYWdlbnRfaGFuZGxlGAIgASgJIj8KF1JlcGFyZW50Q2hhbm5lbFJlc3BvbnNlEiQKB2NoYW5uZWwYASABKAsyEy5jb21wYXNzLnYxLkNoYW5uZWwilwEKF1NldENoYW5uZWxQb2xpY3lSZXF1ZXN0EhIKCmNoYW5uZWxfaWQYASABKAkSMgoLcG9zdF9wb2xpY3kYAiABKA4yHS5jb21wYXNzLnYxLkNoYW5uZWxQb3N0UG9saWN5EhQKDG93bmVyX2hhbmRsZRgDIAEoCRIeChZtYW5kYXRvcnlfc3Vic2NyaXB0aW9uGAQgASgIIkAKGFNldENoYW5uZWxQb2xpY3lSZXNwb25zZRIkCgdjaGFubmVsGAEgASgLMhMuY29tcGFzcy52MS5DaGFubmVsIlIKEEdldFJvc3RlclJlcXVlc3QSJgoFc2NvcGUYASABKA4yFy5jb21wYXNzLnYxLlJvc3RlclNjb3BlEhYKDnZhbnRhZ2VfaGFuZGxlGAIgASgJIj0KEUdldFJvc3RlclJlc3BvbnNlEigKB2VudHJpZXMYASADKAsyFy5jb21wYXNzLnYxLlJvc3RlckVudHJ5IvABCgtSb3N0ZXJFbnRyeRIYChBhZ2VudF9hY2NvdW50X2lkGAEgASgJEg4KBmhhbmRsZRgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSFwoPcGFyZW50X2FnZW50X2lkGAQgASgJEisKCHByZXNlbmNlGAUgASgOMhkuY29tcGFzcy52MS5BZ2VudFByZXNlbmNlEhAKCGFjdGl2aXR5GAYgASgJEhsKE2FjdGl2aXR5X2F0X3VuaXhfbXMYByABKAMSLAoFYm9hcmQYCCABKAsyHS5jb21wYXNzLnYxLkFnZW50Qm9hcmRTdW1tYXJ5IncKGFVwZGF0ZVBpbm5lZEJvYXJkUmVxdWVzdBISCgpjaGFubmVsX2lkGAEgASgJEiUKA3BpbhgCIAEoCzIWLmNvbXBhc3MudjEuUGluTWVzc2FnZUgAEhoKEHVucGluX21lc3NhZ2VfaWQYAyABKAlIAEIECgJvcCI8CgpQaW5NZXNzYWdlEhIKCm1lc3NhZ2VfaWQYASABKAkSGgoScmVwbGFjZV9tZXNzYWdlX2lkGAIgASgJIkEKGVVwZGF0ZVBpbm5lZEJvYXJkUmVzcG9uc2USJAoHY2hhbm5lbBgBIAEoCzITLmNvbXBhc3MudjEuQ2hhbm5lbCIxChlPcGVuQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCSJLChpPcGVuQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRItCgl3b3Jrc3BhY2UYASABKAsyGi5jb21wYXNzLnYxLkFnZW50V29ya3NwYWNlIooBChNMaXN0TWVzc2FnZXNSZXF1ZXN0EhQKCmNoYW5uZWxfaWQYASABKAlIABINCgVsaW1pdBgCIAEoDRIZChFiZWZvcmVfbWVzc2FnZV9pZBgDIAEoCRIUCgxzbmFwc2hvdF9zZXEYBCABKAQSEAoIdG9waWNfaWQYBSABKAlCCwoJY29udGFpbmVyIj0KFExpc3RNZXNzYWdlc1Jlc3BvbnNlEiUKCG1lc3NhZ2VzGAEgAygLMhMuY29tcGFzcy52MS5NZXNzYWdlItwBChJQb3N0TWVzc2FnZVJlcXVlc3QSFAoKY2hhbm5lbF9pZBgBIAEoCUgAEigKBmJsb2NrcxgCIAMoCzIYLmNvbXBhc3MudjEuTWVzc2FnZUJsb2NrEhIKCHRvcGljX2lkGAMgASgJSAESFAoKdG9waWNfbmFtZRgEIAEoCUgBEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAUgASgJEhQKDGNyZWF0ZV90b3BpYxgGIAEoCBIVCg10dXJuX3NlcXVlbmNlGAcgASgEQgsKCWNvbnRhaW5lckIHCgV0b3BpYyI7ChNQb3N0TWVzc2FnZVJlc3BvbnNlEiQKB21lc3NhZ2UYASABKAsyEy5jb21wYXNzLnYxLk1lc3NhZ2UiQQoRTGlzdFRvcGljc1JlcXVlc3QSEgoKY2hhbm5lbF9pZBgBIAEoCRIYChBpbmNsdWRlX2FyY2hpdmVkGAIgASgIIjcKEkxpc3RUb3BpY3NSZXNwb25zZRIhCgZ0b3BpY3MYASADKAsyES5jb21wYXNzLnYxLlRvcGljImYKElVwZGF0ZVRvcGljUmVxdWVzdBIQCgh0b3BpY19pZBgBIAEoCRIRCgRuYW1lGAIgASgJSACIAQESFQoIYXJjaGl2ZWQYAyABKAhIAYgBAUIHCgVfbmFtZUILCglfYXJjaGl2ZWQiNwoTVXBkYXRlVG9waWNSZXNwb25zZRIgCgV0b3BpYxgBIAEoCzIRLmNvbXBhc3MudjEuVG9waWMiVQoTUmVzcG9uZFRvQXNrUmVxdWVzdBIOCgZhc2tfaWQYASABKAkSLgoHYW5zd2VycxgCIAMoCzIdLmNvbXBhc3MudjEuQXNrUXVlc3Rpb25BbnN3ZXIiWAoRQXNrUXVlc3Rpb25BbnN3ZXISEwoLcXVlc3Rpb25faWQYASABKAkSGQoRY2hvc2VuX29wdGlvbl9pZHMYAiADKAkSEwoLY3VzdG9tX3RleHQYAyABKAkiFgoUUmVzcG9uZFRvQXNrUmVzcG9uc2UiagoVU2VhcmNoTWVzc2FnZXNSZXF1ZXN0Eg0KBXF1ZXJ5GAEgASgJEhQKCmNoYW5uZWxfaWQYAiABKAlIABINCgVsaW1pdBgDIAEoDRIUCgxzbmFwc2hvdF9zZXEYBCABKARCBwoFc2NvcGUiPwoWU2VhcmNoTWVzc2FnZXNSZXNwb25zZRIlCghtZXNzYWdlcxgBIAMoCzITLmNvbXBhc3MudjEuTWVzc2FnZSJCChVTdWJzY3JpYmVDb21tc1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEKjUKCFVzZXJSb2xlEhQKEFVTRVJfUk9MRV9NRU1CRVIQABITCg9VU0VSX1JPTEVfQURNSU4QASphChZDaGFubmVsR3JvdXBWaXNpYmlsaXR5EiIKHkNIQU5ORUxfR1JPVVBfVklTSUJJTElUWV9PV05FUhAAEiMKH0NIQU5ORUxfR1JPVVBfVklTSUJJTElUWV9TSEFSRUQQASpVChFDaGFubmVsUG9zdFBvbGljeRIcChhDSEFOTkVMX1BPU1RfUE9MSUNZX09QRU4QABIiCh5DSEFOTkVMX1BPU1RfUE9MSUNZX09XTkVSX09OTFkQASpfChVDaGFubmVsTWVtYmVyc2hpcE1vZGUSJAogQ0hBTk5FTF9NRU1CRVJTSElQX01PREVfRVhQTElDSVQQABIgChxDSEFOTkVMX01FTUJFUlNISVBfTU9ERV9UUkVFEAEqWwoLQ2hhbm5lbEtpbmQSGAoUQ0hBTk5FTF9LSU5EX0NIQU5ORUwQABITCg9DSEFOTkVMX0tJTkRfRE0QARIdChVDSEFOTkVMX0tJTkRfR1JPVVBfRE0QAhoCCAEqnAEKDUFnZW50UHJlc2VuY2USHgoaQUdFTlRfUFJFU0VOQ0VfVU5TUEVDSUZJRUQQABIXChNBR0VOVF9QUkVTRU5DRV9JRExFEAESGgoWQUdFTlRfUFJFU0VOQ0VfV09SS0lORxACEhoKFkFHRU5UX1BSRVNFTkNFX1dBSVRJTkcQAxIaChZBR0VOVF9QUkVTRU5DRV9PRkZMSU5FEAQqwwEKDkJvYXJkVGFza1N0YXRlEiAKHEJPQVJEX1RBU0tfU1RBVEVfVU5TUEVDSUZJRUQQABIbChdCT0FSRF9UQVNLX1NUQVRFX0FDVElWRRABEhwKGEJPQVJEX1RBU0tfU1RBVEVfQkxPQ0tFRBACEhwKGEJPQVJEX1RBU0tfU1RBVEVfV0FJVElORxADEhsKF0JPQVJEX1RBU0tfU1RBVEVfUEFSS0VEEAQSGQoVQk9BUkRfVEFTS19TVEFURV9ET05FEAUqigEKDUJvYXJkSGFsdEtpbmQSHwobQk9BUkRfSEFMVF9LSU5EX1VOU1BFQ0lGSUVEEAASGAoUQk9BUkRfSEFMVF9LSU5EX0RPTkUQARIbChdCT0FSRF9IQUxUX0tJTkRfQkxPQ0tFRBACEiEKHUJPQVJEX0hBTFRfS0lORF9BV0FJVElOR19VU0VSEAMqjAEKDkJvYXJkV2FpdGluZ09uEiAKHEJPQVJEX1dBSVRJTkdfT05fVU5TUEVDSUZJRUQQABIdChlCT0FSRF9XQUlUSU5HX09OX09QRVJBVE9SEAESGgoWQk9BUkRfV0FJVElOR19PTl9BR0VOVBACEh0KGUJPQVJEX1dBSVRJTkdfT05fRVhURVJOQUwQAyppCgxCb2FyZFJlZktpbmQSHgoaQk9BUkRfUkVGX0tJTkRfVU5TUEVDSUZJRUQQABIYChRCT0FSRF9SRUZfS0lORF9JU1NVRRABEh8KG0JPQVJEX1JFRl9LSU5EX1BVTExfUkVRVUVTVBACKqADCgtCb2FyZFByR2F0ZRIdChlCT0FSRF9QUl9HQVRFX1VOU1BFQ0lGSUVEEAASFwoTQk9BUkRfUFJfR0FURV9EUkFGVBABEiAKHEJPQVJEX1BSX0dBVEVfQ0hFQ0tTX1BFTkRJTkcQAhIfChtCT0FSRF9QUl9HQVRFX0NIRUNLU19GQUlMRUQQAxIjCh9CT0FSRF9QUl9HQVRFX0NIQU5HRVNfUkVRVUVTVEVEEAQSIQodQk9BUkRfUFJfR0FURV9BV0FJVElOR19SRVZJRVcQBRIiCh5CT0FSRF9QUl9HQVRFX0FXQUlUSU5HX0VOUVVFVUUQBhIYChRCT0FSRF9QUl9HQVRFX01FUkdFRBAHEhgKFEJPQVJEX1BSX0dBVEVfQ0xPU0VEEAgSGAoUQk9BUkRfUFJfR0FURV9RVUVVRUQQCRIeChpCT0FSRF9QUl9HQVRFX1FVRVVFX0ZBSUxFRBAKEh8KG0JPQVJEX1BSX0dBVEVfUVVFVUVfUkVNT1ZFRBALEhsKF0JPQVJEX1BSX0dBVEVfQ09ORkxJQ1RTEAwqkgEKEEJvYXJkTWlycm9yU3RhdGUSIgoeQk9BUkRfTUlSUk9SX1NUQVRFX1VOU1BFQ0lGSUVEEAASHgoaQk9BUkRfTUlSUk9SX1NUQVRFX1BFTkRJTkcQARIbChdCT0FSRF9NSVJST1JfU1RBVEVfRE9ORRACEh0KGUJPQVJEX01JUlJPUl9TVEFURV9GQUlMRUQQAyqRAQoMUGVlcmluZ1N0YXRlEh0KGVBFRVJJTkdfU1RBVEVfVU5TUEVDSUZJRUQQABIiCh5QRUVSSU5HX1NUQVRFX1BFTkRJTkdfT1VUR09JTkcQARIiCh5QRUVSSU5HX1NUQVRFX1BFTkRJTkdfSU5DT01JTkcQAhIaChZQRUVSSU5HX1NUQVRFX0FQUFJPVkVEEAMqXgoLUm9zdGVyU2NvcGUSHQoZUk9TVEVSX1NDT1BFX05FSUdIQk9SSE9PRBAAEhgKFFJPU1RFUl9TQ09QRV9TVUJUUkVFEAESFgoSUk9TVEVSX1NDT1BFX09XTkVSEAIywBEKDENvbW1zU2VydmljZRJLCgpDcmVhdGVVc2VyEh0uY29tcGFzcy52MS5DcmVhdGVVc2VyUmVxdWVzdBoeLmNvbXBhc3MudjEuQ3JlYXRlVXNlclJlc3BvbnNlEk4KC0NyZWF0ZUFnZW50Eh4uY29tcGFzcy52MS5DcmVhdGVBZ2VudFJlcXVlc3QaHy5jb21wYXNzLnYxLkNyZWF0ZUFnZW50UmVzcG9uc2USUQoMTGlzdEFjY291bnRzEh8uY29tcGFzcy52MS5MaXN0QWNjb3VudHNSZXF1ZXN0GiAuY29tcGFzcy52MS5MaXN0QWNjb3VudHNSZXNwb25zZRJjChJDcmVhdGVDaGFubmVsR3JvdXASJS5jb21wYXNzLnYxLkNyZWF0ZUNoYW5uZWxHcm91cFJlcXVlc3QaJi5jb21wYXNzLnYxLkNyZWF0ZUNoYW5uZWxHcm91cFJlc3BvbnNlEmAKEUxpc3RDaGFubmVsR3JvdXBzEiQuY29tcGFzcy52MS5MaXN0Q2hhbm5lbEdyb3Vwc1JlcXVlc3QaJS5jb21wYXNzLnYxLkxpc3RDaGFubmVsR3JvdXBzUmVzcG9uc2USUQoMTGlzdENoYW5uZWxzEh8uY29tcGFzcy52MS5MaXN0Q2hhbm5lbHNSZXF1ZXN0GiAuY29tcGFzcy52MS5MaXN0Q2hhbm5lbHNSZXNwb25zZRJUCg1DcmVhdGVDaGFubmVsEiAuY29tcGFzcy52MS5DcmVhdGVDaGFubmVsUmVxdWVzdBohLmNvbXBhc3MudjEuQ3JlYXRlQ2hhbm5lbFJlc3BvbnNlEmkKFFVwZGF0ZUNoYW5uZWxNZW1iZXJzEicuY29tcGFzcy52MS5VcGRhdGVDaGFubmVsTWVtYmVyc1JlcXVlc3QaKC5jb21wYXNzLnYxLlVwZGF0ZUNoYW5uZWxNZW1iZXJzUmVzcG9uc2USPwoGT3BlbkRNEhkuY29tcGFzcy52MS5PcGVuRE1SZXF1ZXN0GhouY29tcGFzcy52MS5PcGVuRE1SZXNwb25zZRJUCg1SZXBhcmVudEFnZW50EiAuY29tcGFzcy52MS5SZXBhcmVudEFnZW50UmVxdWVzdBohLmNvbXBhc3MudjEuUmVwYXJlbnRBZ2VudFJlc3BvbnNlEloKD1JlcGFyZW50Q2hhbm5lbBIiLmNvbXBhc3MudjEuUmVwYXJlbnRDaGFubmVsUmVxdWVzdBojLmNvbXBhc3MudjEuUmVwYXJlbnRDaGFubmVsUmVzcG9uc2USYwoST3BlbkFnZW50V29ya3NwYWNlEiUuY29tcGFzcy52MS5PcGVuQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0GiYuY29tcGFzcy52MS5PcGVuQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJRCgxMaXN0TWVzc2FnZXMSHy5jb21wYXNzLnYxLkxpc3RNZXNzYWdlc1JlcXVlc3QaIC5jb21wYXNzLnYxLkxpc3RNZXNzYWdlc1Jlc3BvbnNlEk4KC1Bvc3RNZXNzYWdlEh4uY29tcGFzcy52MS5Qb3N0TWVzc2FnZVJlcXVlc3QaHy5jb21wYXNzLnYxLlBvc3RNZXNzYWdlUmVzcG9uc2USSwoKTGlzdFRvcGljcxIdLmNvbXBhc3MudjEuTGlzdFRvcGljc1JlcXVlc3QaHi5jb21wYXNzLnYxLkxpc3RUb3BpY3NSZXNwb25zZRJOCgtVcGRhdGVUb3BpYxIeLmNvbXBhc3MudjEuVXBkYXRlVG9waWNSZXF1ZXN0Gh8uY29tcGFzcy52MS5VcGRhdGVUb3BpY1Jlc3BvbnNlElEKDFJlc3BvbmRUb0FzaxIfLmNvbXBhc3MudjEuUmVzcG9uZFRvQXNrUmVxdWVzdBogLmNvbXBhc3MudjEuUmVzcG9uZFRvQXNrUmVzcG9uc2USVAoNTWlycm9yVG9Gb3JnZRIgLmNvbXBhc3MudjEuTWlycm9yVG9Gb3JnZVJlcXVlc3QaIS5jb21wYXNzLnYxLk1pcnJvclRvRm9yZ2VSZXNwb25zZRJXCg5TZWFyY2hNZXNzYWdlcxIhLmNvbXBhc3MudjEuU2VhcmNoTWVzc2FnZXNSZXF1ZXN0GiIuY29tcGFzcy52MS5TZWFyY2hNZXNzYWdlc1Jlc3BvbnNlEl0KEFNldENoYW5uZWxQb2xpY3kSIy5jb21wYXNzLnYxLlNldENoYW5uZWxQb2xpY3lSZXF1ZXN0GiQuY29tcGFzcy52MS5TZXRDaGFubmVsUG9saWN5UmVzcG9uc2USSAoJR2V0Um9zdGVyEhwuY29tcGFzcy52MS5HZXRSb3N0ZXJSZXF1ZXN0Gh0uY29tcGFzcy52MS5HZXRSb3N0ZXJSZXNwb25zZRJgChFVcGRhdGVQaW5uZWRCb2FyZBIkLmNvbXBhc3MudjEuVXBkYXRlUGlubmVkQm9hcmRSZXF1ZXN0GiUuY29tcGFzcy52MS5VcGRhdGVQaW5uZWRCb2FyZFJlc3BvbnNlEk4KC0FwcHJvdmVQZWVyEh4uY29tcGFzcy52MS5BcHByb3ZlUGVlclJlcXVlc3QaHy5jb21wYXNzLnYxLkFwcHJvdmVQZWVyUmVzcG9uc2USSwoKUmV2b2tlUGVlchIdLmNvbXBhc3MudjEuUmV2b2tlUGVlclJlcXVlc3QaHi5jb21wYXNzLnYxLlJldm9rZVBlZXJSZXNwb25zZRJICglMaXN0UGVlcnMSHC5jb21wYXNzLnYxLkxpc3RQZWVyc1JlcXVlc3QaHS5jb21wYXNzLnYxLkxpc3RQZWVyc1Jlc3BvbnNlElkKDlN1YnNjcmliZUNvbW1zEiEuY29tcGFzcy52MS5TdWJzY3JpYmVDb21tc1JlcXVlc3QaIi5jb21wYXNzLnYxLlN1YnNjcmliZUNvbW1zUmVzcG9uc2UwAWIGcHJvdG8z", [file_compass_v1_compass]); /** * An account in the communication layer: a human user or an owned agent. Both @@ -1690,6 +1690,20 @@ export type BoardAsk = Message$1<"compass.v1.BoardAsk"> & { * @generated from field: int64 posted_at_unix_ms = 7; */ postedAtUnixMs: bigint; + + /** + * Set by the board read to report this ask's current forge delivery state. + * + * @generated from field: compass.v1.BoardMirrorState mirror_state = 8; + */ + mirrorState: BoardMirrorState; + + /** + * Set by the board read when the forge comment was delivered. + * + * @generated from field: string mirror_url = 9; + */ + mirrorUrl: string; }; /** @@ -1699,6 +1713,76 @@ export type BoardAsk = Message$1<"compass.v1.BoardAsk"> & { export const BoardAskSchema: GenMessage = /*@__PURE__*/ messageDesc(file_compass_v1_comms, 33); +/** + * Request to mirror a visible ask, topic, or message set to an allowed forge ref on the calling user's behalf. + * + * @generated from message compass.v1.MirrorToForgeRequest + */ +export type MirrorToForgeRequest = Message$1<"compass.v1.MirrorToForgeRequest"> & { + /** + * The issue or pull request destination. + * + * @generated from field: string ref = 1; + */ + ref: string; + + /** + * The ask to mirror; mutually exclusive with the other source fields. + * + * @generated from field: string ask_id = 2; + */ + askId: string; + + /** + * The topic to mirror; mutually exclusive with the other source fields. + * + * @generated from field: string topic_id = 3; + */ + topicId: string; + + /** + * One to fifty message IDs from channel_id, mutually exclusive with other sources. + * + * @generated from field: repeated string message_ids = 4; + */ + messageIds: string[]; + + /** + * The visible channel containing the chosen source. + * + * @generated from field: string channel_id = 5; + */ + channelId: string; +}; + +/** + * Describes the message compass.v1.MirrorToForgeRequest. + * Use `create(MirrorToForgeRequestSchema)` to create a new message. + */ +export const MirrorToForgeRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_comms, 34); + +/** + * The forge comment created or updated for the requested source. + * + * @generated from message compass.v1.MirrorToForgeResponse + */ +export type MirrorToForgeResponse = Message$1<"compass.v1.MirrorToForgeResponse"> & { + /** + * The URL of the resulting forge comment. + * + * @generated from field: string comment_url = 1; + */ + commentUrl: string; +}; + +/** + * Describes the message compass.v1.MirrorToForgeResponse. + * Use `create(MirrorToForgeResponseSchema)` to create a new message. + */ +export const MirrorToForgeResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_comms, 35); + /** * The current board state for one agent. * @@ -1795,7 +1879,7 @@ export type AgentBoard = Message$1<"compass.v1.AgentBoard"> & { * Use `create(AgentBoardSchema)` to create a new message. */ export const AgentBoardSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 34); + messageDesc(file_compass_v1_comms, 36); /** * The compact board summary attached to a roster entry. @@ -1851,7 +1935,7 @@ export type AgentBoardSummary = Message$1<"compass.v1.AgentBoardSummary"> & { * Use `create(AgentBoardSummarySchema)` to create a new message. */ export const AgentBoardSummarySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 35); + messageDesc(file_compass_v1_comms, 37); /** * A request to apply task changes and board metadata for the caller's agent. @@ -1914,7 +1998,7 @@ export type UpdateAgentBoardRequest = Message$1<"compass.v1.UpdateAgentBoardRequ * Use `create(UpdateAgentBoardRequestSchema)` to create a new message. */ export const UpdateAgentBoardRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 36); + messageDesc(file_compass_v1_comms, 38); /** * The board after an update. @@ -1935,7 +2019,7 @@ export type UpdateAgentBoardResponse = Message$1<"compass.v1.UpdateAgentBoardRes * Use `create(UpdateAgentBoardResponseSchema)` to create a new message. */ export const UpdateAgentBoardResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 37); + messageDesc(file_compass_v1_comms, 39); /** * Filters for a visible agent board listing. @@ -1998,7 +2082,7 @@ export type ListAgentBoardsRequest = Message$1<"compass.v1.ListAgentBoardsReques * Use `create(ListAgentBoardsRequestSchema)` to create a new message. */ export const ListAgentBoardsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 38); + messageDesc(file_compass_v1_comms, 40); /** * Agent boards matching a listing request. @@ -2019,7 +2103,7 @@ export type ListAgentBoardsResponse = Message$1<"compass.v1.ListAgentBoardsRespo * Use `create(ListAgentBoardsResponseSchema)` to create a new message. */ export const ListAgentBoardsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 39); + messageDesc(file_compass_v1_comms, 41); /** * A live agent-board update; the event's board has no asks. @@ -2047,7 +2131,7 @@ export type AgentBoardChanged = Message$1<"compass.v1.AgentBoardChanged"> & { * Use `create(AgentBoardChangedSchema)` to create a new message. */ export const AgentBoardChangedSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 40); + messageDesc(file_compass_v1_comms, 42); /** * @generated from message compass.v1.Peering @@ -2074,7 +2158,7 @@ export type Peering = Message$1<"compass.v1.Peering"> & { * Use `create(PeeringSchema)` to create a new message. */ export const PeeringSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 41); + messageDesc(file_compass_v1_comms, 43); /** * @generated from message compass.v1.ApprovePeerRequest @@ -2091,7 +2175,7 @@ export type ApprovePeerRequest = Message$1<"compass.v1.ApprovePeerRequest"> & { * Use `create(ApprovePeerRequestSchema)` to create a new message. */ export const ApprovePeerRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 42); + messageDesc(file_compass_v1_comms, 44); /** * @generated from message compass.v1.ApprovePeerResponse @@ -2108,7 +2192,7 @@ export type ApprovePeerResponse = Message$1<"compass.v1.ApprovePeerResponse"> & * Use `create(ApprovePeerResponseSchema)` to create a new message. */ export const ApprovePeerResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 43); + messageDesc(file_compass_v1_comms, 45); /** * @generated from message compass.v1.RevokePeerRequest @@ -2125,7 +2209,7 @@ export type RevokePeerRequest = Message$1<"compass.v1.RevokePeerRequest"> & { * Use `create(RevokePeerRequestSchema)` to create a new message. */ export const RevokePeerRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 44); + messageDesc(file_compass_v1_comms, 46); /** * @generated from message compass.v1.RevokePeerResponse @@ -2142,7 +2226,7 @@ export type RevokePeerResponse = Message$1<"compass.v1.RevokePeerResponse"> & { * Use `create(RevokePeerResponseSchema)` to create a new message. */ export const RevokePeerResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 45); + messageDesc(file_compass_v1_comms, 47); /** * @generated from message compass.v1.ListPeersRequest @@ -2155,7 +2239,7 @@ export type ListPeersRequest = Message$1<"compass.v1.ListPeersRequest"> & { * Use `create(ListPeersRequestSchema)` to create a new message. */ export const ListPeersRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 46); + messageDesc(file_compass_v1_comms, 48); /** * @generated from message compass.v1.ListPeersResponse @@ -2172,7 +2256,7 @@ export type ListPeersResponse = Message$1<"compass.v1.ListPeersResponse"> & { * Use `create(ListPeersResponseSchema)` to create a new message. */ export const ListPeersResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 47); + messageDesc(file_compass_v1_comms, 49); /** * @generated from message compass.v1.CreateUserRequest @@ -2194,7 +2278,7 @@ export type CreateUserRequest = Message$1<"compass.v1.CreateUserRequest"> & { * Use `create(CreateUserRequestSchema)` to create a new message. */ export const CreateUserRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 48); + messageDesc(file_compass_v1_comms, 50); /** * @generated from message compass.v1.CreateUserResponse @@ -2211,7 +2295,7 @@ export type CreateUserResponse = Message$1<"compass.v1.CreateUserResponse"> & { * Use `create(CreateUserResponseSchema)` to create a new message. */ export const CreateUserResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 49); + messageDesc(file_compass_v1_comms, 51); /** * @generated from message compass.v1.CreateAgentRequest @@ -2257,7 +2341,7 @@ export type CreateAgentRequest = Message$1<"compass.v1.CreateAgentRequest"> & { * Use `create(CreateAgentRequestSchema)` to create a new message. */ export const CreateAgentRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 50); + messageDesc(file_compass_v1_comms, 52); /** * @generated from message compass.v1.CreateAgentResponse @@ -2274,7 +2358,7 @@ export type CreateAgentResponse = Message$1<"compass.v1.CreateAgentResponse"> & * Use `create(CreateAgentResponseSchema)` to create a new message. */ export const CreateAgentResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 51); + messageDesc(file_compass_v1_comms, 53); /** * @generated from message compass.v1.ListAccountsRequest @@ -2294,7 +2378,7 @@ export type ListAccountsRequest = Message$1<"compass.v1.ListAccountsRequest"> & * Use `create(ListAccountsRequestSchema)` to create a new message. */ export const ListAccountsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 52); + messageDesc(file_compass_v1_comms, 54); /** * @generated from message compass.v1.ListAccountsResponse @@ -2311,7 +2395,7 @@ export type ListAccountsResponse = Message$1<"compass.v1.ListAccountsResponse"> * Use `create(ListAccountsResponseSchema)` to create a new message. */ export const ListAccountsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 53); + messageDesc(file_compass_v1_comms, 55); /** * @generated from message compass.v1.CreateChannelGroupRequest @@ -2356,7 +2440,7 @@ export type CreateChannelGroupRequest = Message$1<"compass.v1.CreateChannelGroup * Use `create(CreateChannelGroupRequestSchema)` to create a new message. */ export const CreateChannelGroupRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 54); + messageDesc(file_compass_v1_comms, 56); /** * @generated from message compass.v1.CreateChannelGroupResponse @@ -2373,7 +2457,7 @@ export type CreateChannelGroupResponse = Message$1<"compass.v1.CreateChannelGrou * Use `create(CreateChannelGroupResponseSchema)` to create a new message. */ export const CreateChannelGroupResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 55); + messageDesc(file_compass_v1_comms, 57); /** * @generated from message compass.v1.ListChannelGroupsRequest @@ -2393,7 +2477,7 @@ export type ListChannelGroupsRequest = Message$1<"compass.v1.ListChannelGroupsRe * Use `create(ListChannelGroupsRequestSchema)` to create a new message. */ export const ListChannelGroupsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 56); + messageDesc(file_compass_v1_comms, 58); /** * @generated from message compass.v1.ListChannelGroupsResponse @@ -2410,7 +2494,7 @@ export type ListChannelGroupsResponse = Message$1<"compass.v1.ListChannelGroupsR * Use `create(ListChannelGroupsResponseSchema)` to create a new message. */ export const ListChannelGroupsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 57); + messageDesc(file_compass_v1_comms, 59); /** * @generated from message compass.v1.ListChannelsRequest @@ -2430,7 +2514,7 @@ export type ListChannelsRequest = Message$1<"compass.v1.ListChannelsRequest"> & * Use `create(ListChannelsRequestSchema)` to create a new message. */ export const ListChannelsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 58); + messageDesc(file_compass_v1_comms, 60); /** * @generated from message compass.v1.ListChannelsResponse @@ -2447,7 +2531,7 @@ export type ListChannelsResponse = Message$1<"compass.v1.ListChannelsResponse"> * Use `create(ListChannelsResponseSchema)` to create a new message. */ export const ListChannelsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 59); + messageDesc(file_compass_v1_comms, 61); /** * @generated from message compass.v1.CreateChannelRequest @@ -2515,7 +2599,7 @@ export type CreateChannelRequest = Message$1<"compass.v1.CreateChannelRequest"> * Use `create(CreateChannelRequestSchema)` to create a new message. */ export const CreateChannelRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 60); + messageDesc(file_compass_v1_comms, 62); /** * @generated from message compass.v1.CreateChannelResponse @@ -2532,7 +2616,7 @@ export type CreateChannelResponse = Message$1<"compass.v1.CreateChannelResponse" * Use `create(CreateChannelResponseSchema)` to create a new message. */ export const CreateChannelResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 61); + messageDesc(file_compass_v1_comms, 63); /** * @generated from message compass.v1.UpdateChannelMembersRequest @@ -2596,7 +2680,7 @@ export type UpdateChannelMembersRequest = Message$1<"compass.v1.UpdateChannelMem * Use `create(UpdateChannelMembersRequestSchema)` to create a new message. */ export const UpdateChannelMembersRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 62); + messageDesc(file_compass_v1_comms, 64); /** * @generated from message compass.v1.UpdateChannelMembersResponse @@ -2613,7 +2697,7 @@ export type UpdateChannelMembersResponse = Message$1<"compass.v1.UpdateChannelMe * Use `create(UpdateChannelMembersResponseSchema)` to create a new message. */ export const UpdateChannelMembersResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 63); + messageDesc(file_compass_v1_comms, 65); /** * @generated from message compass.v1.OpenDMRequest @@ -2634,7 +2718,7 @@ export type OpenDMRequest = Message$1<"compass.v1.OpenDMRequest"> & { * Use `create(OpenDMRequestSchema)` to create a new message. */ export const OpenDMRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 64); + messageDesc(file_compass_v1_comms, 66); /** * @generated from message compass.v1.OpenDMResponse @@ -2660,7 +2744,7 @@ export type OpenDMResponse = Message$1<"compass.v1.OpenDMResponse"> & { * Use `create(OpenDMResponseSchema)` to create a new message. */ export const OpenDMResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 65); + messageDesc(file_compass_v1_comms, 67); /** * @generated from message compass.v1.ReparentAgentRequest @@ -2688,7 +2772,7 @@ export type ReparentAgentRequest = Message$1<"compass.v1.ReparentAgentRequest"> * Use `create(ReparentAgentRequestSchema)` to create a new message. */ export const ReparentAgentRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 66); + messageDesc(file_compass_v1_comms, 68); /** * @generated from message compass.v1.ReparentAgentResponse @@ -2705,7 +2789,7 @@ export type ReparentAgentResponse = Message$1<"compass.v1.ReparentAgentResponse" * Use `create(ReparentAgentResponseSchema)` to create a new message. */ export const ReparentAgentResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 67); + messageDesc(file_compass_v1_comms, 69); /** * @generated from message compass.v1.ReparentChannelRequest @@ -2730,7 +2814,7 @@ export type ReparentChannelRequest = Message$1<"compass.v1.ReparentChannelReques * Use `create(ReparentChannelRequestSchema)` to create a new message. */ export const ReparentChannelRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 68); + messageDesc(file_compass_v1_comms, 70); /** * @generated from message compass.v1.ReparentChannelResponse @@ -2747,7 +2831,7 @@ export type ReparentChannelResponse = Message$1<"compass.v1.ReparentChannelRespo * Use `create(ReparentChannelResponseSchema)` to create a new message. */ export const ReparentChannelResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 69); + messageDesc(file_compass_v1_comms, 71); /** * @generated from message compass.v1.SetChannelPolicyRequest @@ -2788,7 +2872,7 @@ export type SetChannelPolicyRequest = Message$1<"compass.v1.SetChannelPolicyRequ * Use `create(SetChannelPolicyRequestSchema)` to create a new message. */ export const SetChannelPolicyRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 70); + messageDesc(file_compass_v1_comms, 72); /** * @generated from message compass.v1.SetChannelPolicyResponse @@ -2807,7 +2891,7 @@ export type SetChannelPolicyResponse = Message$1<"compass.v1.SetChannelPolicyRes * Use `create(SetChannelPolicyResponseSchema)` to create a new message. */ export const SetChannelPolicyResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 71); + messageDesc(file_compass_v1_comms, 73); /** * @generated from message compass.v1.GetRosterRequest @@ -2835,7 +2919,7 @@ export type GetRosterRequest = Message$1<"compass.v1.GetRosterRequest"> & { * Use `create(GetRosterRequestSchema)` to create a new message. */ export const GetRosterRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 72); + messageDesc(file_compass_v1_comms, 74); /** * @generated from message compass.v1.GetRosterResponse @@ -2852,7 +2936,7 @@ export type GetRosterResponse = Message$1<"compass.v1.GetRosterResponse"> & { * Use `create(GetRosterResponseSchema)` to create a new message. */ export const GetRosterResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 73); + messageDesc(file_compass_v1_comms, 75); /** * One agent in a roster: its identity, tree position, and joined presence + @@ -2909,7 +2993,7 @@ export type RosterEntry = Message$1<"compass.v1.RosterEntry"> & { * Use `create(RosterEntrySchema)` to create a new message. */ export const RosterEntrySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 74); + messageDesc(file_compass_v1_comms, 76); /** * @generated from message compass.v1.UpdatePinnedBoardRequest @@ -2949,7 +3033,7 @@ export type UpdatePinnedBoardRequest = Message$1<"compass.v1.UpdatePinnedBoardRe * Use `create(UpdatePinnedBoardRequestSchema)` to create a new message. */ export const UpdatePinnedBoardRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 75); + messageDesc(file_compass_v1_comms, 77); /** * Pin an existing message. message_id must already live in a topic of the @@ -2975,7 +3059,7 @@ export type PinMessage = Message$1<"compass.v1.PinMessage"> & { * Use `create(PinMessageSchema)` to create a new message. */ export const PinMessageSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 76); + messageDesc(file_compass_v1_comms, 78); /** * @generated from message compass.v1.UpdatePinnedBoardResponse @@ -2994,7 +3078,7 @@ export type UpdatePinnedBoardResponse = Message$1<"compass.v1.UpdatePinnedBoardR * Use `create(UpdatePinnedBoardResponseSchema)` to create a new message. */ export const UpdatePinnedBoardResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 77); + messageDesc(file_compass_v1_comms, 79); /** * @generated from message compass.v1.OpenAgentWorkspaceRequest @@ -3014,7 +3098,7 @@ export type OpenAgentWorkspaceRequest = Message$1<"compass.v1.OpenAgentWorkspace * Use `create(OpenAgentWorkspaceRequestSchema)` to create a new message. */ export const OpenAgentWorkspaceRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 78); + messageDesc(file_compass_v1_comms, 80); /** * @generated from message compass.v1.OpenAgentWorkspaceResponse @@ -3031,7 +3115,7 @@ export type OpenAgentWorkspaceResponse = Message$1<"compass.v1.OpenAgentWorkspac * Use `create(OpenAgentWorkspaceResponseSchema)` to create a new message. */ export const OpenAgentWorkspaceResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 79); + messageDesc(file_compass_v1_comms, 81); /** * @generated from message compass.v1.ListMessagesRequest @@ -3087,7 +3171,7 @@ export type ListMessagesRequest = Message$1<"compass.v1.ListMessagesRequest"> & * Use `create(ListMessagesRequestSchema)` to create a new message. */ export const ListMessagesRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 80); + messageDesc(file_compass_v1_comms, 82); /** * @generated from message compass.v1.ListMessagesResponse @@ -3106,7 +3190,7 @@ export type ListMessagesResponse = Message$1<"compass.v1.ListMessagesResponse"> * Use `create(ListMessagesResponseSchema)` to create a new message. */ export const ListMessagesResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 81); + messageDesc(file_compass_v1_comms, 83); /** * @generated from message compass.v1.PostMessageRequest @@ -3189,7 +3273,7 @@ export type PostMessageRequest = Message$1<"compass.v1.PostMessageRequest"> & { * Use `create(PostMessageRequestSchema)` to create a new message. */ export const PostMessageRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 82); + messageDesc(file_compass_v1_comms, 84); /** * @generated from message compass.v1.PostMessageResponse @@ -3206,7 +3290,7 @@ export type PostMessageResponse = Message$1<"compass.v1.PostMessageResponse"> & * Use `create(PostMessageResponseSchema)` to create a new message. */ export const PostMessageResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 83); + messageDesc(file_compass_v1_comms, 85); /** * @generated from message compass.v1.ListTopicsRequest @@ -3232,7 +3316,7 @@ export type ListTopicsRequest = Message$1<"compass.v1.ListTopicsRequest"> & { * Use `create(ListTopicsRequestSchema)` to create a new message. */ export const ListTopicsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 84); + messageDesc(file_compass_v1_comms, 86); /** * @generated from message compass.v1.ListTopicsResponse @@ -3249,7 +3333,7 @@ export type ListTopicsResponse = Message$1<"compass.v1.ListTopicsResponse"> & { * Use `create(ListTopicsResponseSchema)` to create a new message. */ export const ListTopicsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 85); + messageDesc(file_compass_v1_comms, 87); /** * @generated from message compass.v1.UpdateTopicRequest @@ -3283,7 +3367,7 @@ export type UpdateTopicRequest = Message$1<"compass.v1.UpdateTopicRequest"> & { * Use `create(UpdateTopicRequestSchema)` to create a new message. */ export const UpdateTopicRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 86); + messageDesc(file_compass_v1_comms, 88); /** * @generated from message compass.v1.UpdateTopicResponse @@ -3300,7 +3384,7 @@ export type UpdateTopicResponse = Message$1<"compass.v1.UpdateTopicResponse"> & * Use `create(UpdateTopicResponseSchema)` to create a new message. */ export const UpdateTopicResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 87); + messageDesc(file_compass_v1_comms, 89); /** * @generated from message compass.v1.RespondToAskRequest @@ -3326,7 +3410,7 @@ export type RespondToAskRequest = Message$1<"compass.v1.RespondToAskRequest"> & * Use `create(RespondToAskRequestSchema)` to create a new message. */ export const RespondToAskRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 88); + messageDesc(file_compass_v1_comms, 90); /** * The answer to one AskQuestion. @@ -3360,7 +3444,7 @@ export type AskQuestionAnswer = Message$1<"compass.v1.AskQuestionAnswer"> & { * Use `create(AskQuestionAnswerSchema)` to create a new message. */ export const AskQuestionAnswerSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 89); + messageDesc(file_compass_v1_comms, 91); /** * @generated from message compass.v1.RespondToAskResponse @@ -3373,7 +3457,7 @@ export type RespondToAskResponse = Message$1<"compass.v1.RespondToAskResponse"> * Use `create(RespondToAskResponseSchema)` to create a new message. */ export const RespondToAskResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 90); + messageDesc(file_compass_v1_comms, 92); /** * @generated from message compass.v1.SearchMessagesRequest @@ -3419,7 +3503,7 @@ export type SearchMessagesRequest = Message$1<"compass.v1.SearchMessagesRequest" * Use `create(SearchMessagesRequestSchema)` to create a new message. */ export const SearchMessagesRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 91); + messageDesc(file_compass_v1_comms, 93); /** * @generated from message compass.v1.SearchMessagesResponse @@ -3438,7 +3522,7 @@ export type SearchMessagesResponse = Message$1<"compass.v1.SearchMessagesRespons * Use `create(SearchMessagesResponseSchema)` to create a new message. */ export const SearchMessagesResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 92); + messageDesc(file_compass_v1_comms, 94); /** * @generated from message compass.v1.SubscribeCommsRequest @@ -3466,7 +3550,7 @@ export type SubscribeCommsRequest = Message$1<"compass.v1.SubscribeCommsRequest" * Use `create(SubscribeCommsRequestSchema)` to create a new message. */ export const SubscribeCommsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_comms, 93); + messageDesc(file_compass_v1_comms, 95); /** * The user permission roles, expandable later. A regular member is the default @@ -3926,6 +4010,47 @@ export enum BoardPrGate { export const BoardPrGateSchema: GenEnum = /*@__PURE__*/ enumDesc(file_compass_v1_comms, 10); +/** + * Delivery state of an ask mirror outbox row. + * + * @generated from enum compass.v1.BoardMirrorState + */ +export enum BoardMirrorState { + /** + * No known mirror state was supplied. + * + * @generated from enum value: BOARD_MIRROR_STATE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * The comment is waiting for delivery or retry. + * + * @generated from enum value: BOARD_MIRROR_STATE_PENDING = 1; + */ + PENDING = 1, + + /** + * The forge comment was delivered. + * + * @generated from enum value: BOARD_MIRROR_STATE_DONE = 2; + */ + DONE = 2, + + /** + * Delivery was refused or exhausted. + * + * @generated from enum value: BOARD_MIRROR_STATE_FAILED = 3; + */ + FAILED = 3, +} + +/** + * Describes the enum compass.v1.BoardMirrorState. + */ +export const BoardMirrorStateSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_compass_v1_comms, 11); + /** * @generated from enum compass.v1.PeeringState */ @@ -3955,7 +4080,7 @@ export enum PeeringState { * Describes the enum compass.v1.PeeringState. */ export const PeeringStateSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_comms, 11); + enumDesc(file_compass_v1_comms, 12); /** * The scope of a roster read, relative to a vantage agent. @@ -3989,7 +4114,7 @@ export enum RosterScope { * Describes the enum compass.v1.RosterScope. */ export const RosterScopeSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_comms, 12); + enumDesc(file_compass_v1_comms, 13); /** * The communication-layer service: accounts, channel groups + channels, @@ -4205,6 +4330,17 @@ export const CommsService: GenService<{ input: typeof RespondToAskRequestSchema; output: typeof RespondToAskResponseSchema; }, + /** + * Mirror a visible ask, topic, or message set to an allowed forge ref on the + * calling user's behalf. + * + * @generated from rpc compass.v1.CommsService.MirrorToForge + */ + mirrorToForge: { + methodKind: "unary"; + input: typeof MirrorToForgeRequestSchema; + output: typeof MirrorToForgeResponseSchema; + }, /** * Full-text search across the messages the caller may see — the audit/search * property served from the store of record (D1, D12). Results are scoped to diff --git a/packages/compass-client/src/gen/compass/v1/compass_pb.ts b/packages/compass-client/src/gen/compass/v1/compass_pb.ts index 61258647a..110ef11e7 100644 --- a/packages/compass-client/src/gen/compass/v1/compass_pb.ts +++ b/packages/compass-client/src/gen/compass/v1/compass_pb.ts @@ -20,7 +20,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file compass/v1/compass.proto. */ export const file_compass_v1_compass: GenFile = /*@__PURE__*/ - fileDesc("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEiRwobR3JhbnRBZ2VudFJlcG9zaXRvcnlSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCRISCgpyZXBvc2l0b3J5GAIgASgJIi0KHEdyYW50QWdlbnRSZXBvc2l0b3J5UmVzcG9uc2USDQoFYWRkZWQYASABKAgiSAocUmV2b2tlQWdlbnRSZXBvc2l0b3J5UmVxdWVzdBIUCgxhZ2VudF9oYW5kbGUYASABKAkSEgoKcmVwb3NpdG9yeRgCIAEoCSIwCh1SZXZva2VBZ2VudFJlcG9zaXRvcnlSZXNwb25zZRIPCgdyZW1vdmVkGAEgASgIIjQKHExpc3RBZ2VudFJlcG9zaXRvcmllc1JlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJIjUKHUxpc3RBZ2VudFJlcG9zaXRvcmllc1Jlc3BvbnNlEhQKDHJlcG9zaXRvcmllcxgBIAMoCSLQAQoQU2V0U2VjcmV0UmVxdWVzdBIMCgRuYW1lGAEgASgJEhIKBXZhbHVlGAIgASgJQgOAAQESLAoIZGVsaXZlcnkYAyABKA4yGi5jb21wYXNzLnYxLlNlY3JldERlbGl2ZXJ5EiQKBGtpbmQYBCABKA4yFi5jb21wYXNzLnYxLlNlY3JldEtpbmQSEAoIcHJvdmlkZXIYBSABKAkSDAoEaG9zdBgGIAEoCRImCgVzY29wZRgHIAEoDjIXLmNvbXBhc3MudjEuU2VjcmV0U2NvcGUiEwoRU2V0U2VjcmV0UmVzcG9uc2UiFAoSTGlzdFNlY3JldHNSZXF1ZXN0IkAKE0xpc3RTZWNyZXRzUmVzcG9uc2USKQoHc2VjcmV0cxgBIAMoCzIYLmNvbXBhc3MudjEuU2VjcmV0U3RhdHVzIqABCgxTZWNyZXRTdGF0dXMSDAoEbmFtZRgBIAEoCRIOCgZpc19zZXQYAiABKAgSLAoIZGVsaXZlcnkYAyABKA4yGi5jb21wYXNzLnYxLlNlY3JldERlbGl2ZXJ5EiQKBGtpbmQYBCABKA4yFi5jb21wYXNzLnYxLlNlY3JldEtpbmQSEAoIcHJvdmlkZXIYBSABKAkSDAoEaG9zdBgGIAEoCSJLChNEZWxldGVTZWNyZXRSZXF1ZXN0EgwKBG5hbWUYASABKAkSJgoFc2NvcGUYAiABKA4yFy5jb21wYXNzLnYxLlNlY3JldFNjb3BlIhYKFERlbGV0ZVNlY3JldFJlc3BvbnNlIhoKGExpc3RTZXJ2ZXJTZWNyZXRzUmVxdWVzdCJTChlMaXN0U2VydmVyU2VjcmV0c1Jlc3BvbnNlEjYKDnNlcnZlcl9zZWNyZXRzGAEgAygLMh4uY29tcGFzcy52MS5TZXJ2ZXJTZWNyZXRTdGF0dXMiMgoSU2VydmVyU2VjcmV0U3RhdHVzEgwKBG5hbWUYASABKAkSDgoGaXNfc2V0GAIgASgIIhgKFkdldFJlZkxpbmtSdWxlc1JlcXVlc3QiQQoLUmVmTGlua1J1bGUSDAoEa2luZBgBIAEoCRIOCgZwcmVmaXgYAiABKAkSFAoMdXJsX3RlbXBsYXRlGAMgASgJIkEKF0dldFJlZkxpbmtSdWxlc1Jlc3BvbnNlEiYKBXJ1bGVzGAEgAygLMhcuY29tcGFzcy52MS5SZWZMaW5rUnVsZSIWChRHZXRTZXJ2ZXJJbmZvUmVxdWVzdCKAAQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSEwoLYXBpX3ZlcnNpb24YAiABKAkSCwoDcmV2GAMgASgJEjQKEGVucm9sbGVkX3J1bm5lcnMYBCADKAsyGi5jb21wYXNzLnYxLkVucm9sbGVkUnVubmVyIjAKDkVucm9sbGVkUnVubmVyEgoKAmlkGAEgASgJEhIKCmVucm9sbG1lbnQYAiABKAQiDwoNV2hvQW1JUmVxdWVzdCIkCg5XaG9BbUlSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJIhUKE0dldFRvdXJTdGF0ZVJlcXVlc3QiUQoUR2V0VG91clN0YXRlUmVzcG9uc2USKAoHb3V0Y29tZRgBIAEoDjIXLmNvbXBhc3MudjEuVG91ck91dGNvbWUSDwoHc3RlcF9pZBgCIAEoCSIoChVDbGFpbVRvdXJTdGFydFJlcXVlc3QSDwoHc3RlcF9pZBgBIAEoCSIpChZDbGFpbVRvdXJTdGFydFJlc3BvbnNlEg8KB2NsYWltZWQYASABKAgiUAoTU2V0VG91clN0YXRlUmVxdWVzdBIoCgdvdXRjb21lGAEgASgOMhcuY29tcGFzcy52MS5Ub3VyT3V0Y29tZRIPCgdzdGVwX2lkGAIgASgJIhYKFFNldFRvdXJTdGF0ZVJlc3BvbnNlIkMKFlN1YnNjcmliZUV2ZW50c1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEIuIDChdTdWJzY3JpYmVFdmVudHNSZXNwb25zZRILCgNzZXEYASABKAQSEgoKYXRfdW5peF9tcxgCIAEoAxIWCg5pbnN0YW5jZV9lcG9jaBgDIAEoBBIUCgxzbmFwc2hvdF9zZXEYBCABKAQSMQoNc2VydmVyX3N0YXR1cxgKIAEoCzIYLmNvbXBhc3MudjEuU2VydmVyU3RhdHVzSAASNQoPcmVzeW5jX3JlcXVpcmVkGAsgASgLMhouY29tcGFzcy52MS5SZXN5bmNSZXF1aXJlZEgAEj4KFGFnZW50X3Nlc3Npb25fc3RhdHVzGAwgASgLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXNIABI8ChNhZ2VudF9tZXNzYWdlX2NodW5rGA0gASgLMh0uY29tcGFzcy52MS5BZ2VudE1lc3NhZ2VDaHVua0gAEjQKD2FnZW50X3Rvb2xfY2FsbBgOIAEoCzIZLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbEgAEisKCmFnZW50X3BsYW4YDyABKAsyFS5jb21wYXNzLnYxLkFnZW50UGxhbkgAEiIKBWlzc3VlGBAgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAQgkKB3BheWxvYWQiLgoWTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPAoXTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSJJChNTZWFyY2hJc3N1ZXNSZXF1ZXN0Eg0KBXF1ZXJ5GAEgASgJEg0KBWxpbWl0GAIgASgNEhQKDHNuYXBzaG90X3NlcRgDIAEoBCI5ChRTZWFyY2hJc3N1ZXNSZXNwb25zZRIhCgZpc3N1ZXMYASADKAsyES5jb21wYXNzLnYxLklzc3VlIjYKDFNlcnZlclN0YXR1cxImCgVzdGF0ZRgBIAEoDjIXLmNvbXBhc3MudjEuU2VydmVyU3RhdGUiEAoOUmVzeW5jUmVxdWlyZWQi0gEKEkFnZW50U2Vzc2lvblN0YXR1cxISCgpzZXNzaW9uX2lkGAEgASgJEiwKBXN0YXRlGAIgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZRIYChBhZ2VudF9hY2NvdW50X2lkGAMgASgJEi0KDHJ1bnRpbWVfdGllchgEIAEoDjIXLmNvbXBhc3MudjEuUnVudGltZVRpZXISMQoOZWdyZXNzX3Bvc3R1cmUYBSABKA4yGS5jb21wYXNzLnYxLkVncmVzc1Bvc3R1cmUiSQoRQWdlbnRNZXNzYWdlQ2h1bmsSEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgR0ZXh0GAIgASgJEhIKCmlzX3Rob3VnaHQYAyABKAgieQoNQWdlbnRUb29sQ2FsbBISCgpzZXNzaW9uX2lkGAEgASgJEhQKDHRvb2xfY2FsbF9pZBgCIAEoCRINCgV0aXRsZRgDIAEoCRIvCgZzdGF0dXMYBCABKA4yHy5jb21wYXNzLnYxLkFnZW50VG9vbENhbGxTdGF0dXMiTAoJQWdlbnRQbGFuEhIKCnNlc3Npb25faWQYASABKAkSKwoHZW50cmllcxgCIAMoCzIaLmNvbXBhc3MudjEuQWdlbnRQbGFuRW50cnkiUwoOQWdlbnRQbGFuRW50cnkSDwoHY29udGVudBgBIAEoCRIwCgZzdGF0dXMYAiABKA4yIC5jb21wYXNzLnYxLkFnZW50UGxhbkVudHJ5U3RhdHVzIpkECgxTZXNzaW9uRXZlbnQSEAoIZXZlbnRfaWQYASABKAkSEgoKYXRfdW5peF9tcxgCIAEoAxI6Cg5hc3Npc3RhbnRfdGV4dBgDIAEoCzIgLmNvbXBhc3MudjEuU2Vzc2lvbkFzc2lzdGFudFRleHRIABIvCgh0aGlua2luZxgEIAEoCzIbLmNvbXBhc3MudjEuU2Vzc2lvblRoaW5raW5nSAASMAoJdG9vbF9jYWxsGAUgASgLMhsuY29tcGFzcy52MS5TZXNzaW9uVG9vbENhbGxIABI9ChB0b29sX2NhbGxfdXBkYXRlGAYgASgLMiEuY29tcGFzcy52MS5TZXNzaW9uVG9vbENhbGxVcGRhdGVIABInCgRwbGFuGAcgASgLMhcuY29tcGFzcy52MS5TZXNzaW9uUGxhbkgAEisKBm5vdGljZRgIIAEoCzIZLmNvbXBhc3MudjEuU2Vzc2lvbk5vdGljZUgAEjkKEXNlc3Npb25faW5qZWN0aW9uGAkgASgLMhwuY29tcGFzcy52MS5TZXNzaW9uSW5qZWN0aW9uSAASMQoNc2Vzc2lvbl9lcnJvchgKIAEoCzIYLmNvbXBhc3MudjEuU2Vzc2lvbkVycm9ySAASOAoNYmF0Y2hfcGVuZGluZxgLIAEoCzIfLmNvbXBhc3MudjEuU2Vzc2lvbkJhdGNoUGVuZGluZ0gAQgcKBWV2ZW50IjgKFFNlc3Npb25Bc3Npc3RhbnRUZXh0EgwKBHRleHQYASABKAkSEgoKbWVzc2FnZV9pZBgCIAEoCSIzCg9TZXNzaW9uVGhpbmtpbmcSDAoEdGV4dBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJImcKD1Nlc3Npb25Ub29sQ2FsbBIUCgx0b29sX2NhbGxfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSLwoGc3RhdHVzGAMgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIpoBChVTZXNzaW9uVG9vbENhbGxVcGRhdGUSFAoMdG9vbF9jYWxsX2lkGAEgASgJEi8KBnN0YXR1cxgCIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cxIOCgZvdXRwdXQYAyABKAkSKgoFZGlmZnMYBCADKAsyGy5jb21wYXNzLnYxLlNlc3Npb25GaWxlRGlmZiJVCg9TZXNzaW9uRmlsZURpZmYSDAoEcGF0aBgBIAEoCRIVCghvbGRfdGV4dBgCIAEoCUgAiAEBEhAKCG5ld190ZXh0GAMgASgJQgsKCV9vbGRfdGV4dCI6CgtTZXNzaW9uUGxhbhIrCgdlbnRyaWVzGAEgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSI5Cg1TZXNzaW9uTm90aWNlEgwKBHRleHQYASABKAkSEQoEbGluaxgCIAEoCUgAiAEBQgcKBV9saW5rIoMBChBTZXNzaW9uSW5qZWN0aW9uEjEKB29wX2tpbmQYASABKA4yIC5jb21wYXNzLnYxLlNlc3Npb25JbmplY3Rpb25LaW5kEhIKCm1lc3NhZ2VfaWQYAiABKAkSEwoLZnJvbV9oYW5kbGUYAyABKAkSEwoLdHJhY2VwYXJlbnQYBCABKAkiawoMU2Vzc2lvbkVycm9yEioKBGtpbmQYASABKA4yHC5jb21wYXNzLnYxLlNlc3Npb25FcnJvcktpbmQSDwoHbWVzc2FnZRgCIAEoCRITCgZzdGF0dXMYAyABKAVIAIgBAUIJCgdfc3RhdHVzIj4KE1Nlc3Npb25CYXRjaFBlbmRpbmcSDQoFY291bnQYASABKA0SGAoQZmlyZXNfYXRfdW5peF9tcxgCIAEoAyIyChxTdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkilQEKEUFnZW50U2Vzc2lvbkZyYW1lEhIKCnNlc3Npb25faWQYASABKAkSJwoFZXZlbnQYAiABKAsyGC5jb21wYXNzLnYxLlNlc3Npb25FdmVudBIsCgVzdGF0ZRgDIAEoDjIdLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdGUSFQoNdHVybl9zZXF1ZW5jZRgEIAEoBCJwCh5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJEg8KB3BlcnNvbmEYAyABKAkSDAoEcm9sZRgEIAEoCSI5Ch9Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEhYKDmNvbnRhaW5lcl9uYW1lGAEgASgJIlAKG1JlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgCIAEoCSIeChxSZW1vdmVBZ2VudFdvcmtzcGFjZVJlc3BvbnNlImMKGFN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFyZXN1bWVfc2Vzc2lvbl9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiLwoZU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIloKEVNwYXduQWdlbnRSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiQAoSU3Bhd25BZ2VudFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSFgoOY29udGFpbmVyX25hbWUYAiABKAkiLQoXU3RvcEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIaChhTdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2UiLAoWU2tpcEJhdGNoV2luZG93UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIhkKF1NraXBCYXRjaFdpbmRvd1Jlc3BvbnNlIi8KGVJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIwChpSZWxvYWRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIisKFUdldEFnZW50U3RhdHVzUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJInwKFkdldEFnZW50U3RhdHVzUmVzcG9uc2USMAoIc3RhdHVzZXMYASADKAsyHi5jb21wYXNzLnYxLkFnZW50U2Vzc2lvblN0YXR1cxIcCg9ydW5uZXJfZW5yb2xsZWQYAiABKAhIAIgBAUISChBfcnVubmVyX2Vucm9sbGVkIisKEUlzc3VlVG9rZW5SZXF1ZXN0EhYKDmFjY291bnRfaGFuZGxlGAEgASgJIiMKEklzc3VlVG9rZW5SZXNwb25zZRINCgV0b2tlbhgBIAEoCSJCChRSZW5hbWVBY2NvdW50UmVxdWVzdBIWCg5hY2NvdW50X2hhbmRsZRgBIAEoCRISCgpuZXdfaGFuZGxlGAIgASgJIjsKFVJlbmFtZUFjY291bnRSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJEg4KBmhhbmRsZRgCIAEoCSIoChJSZXZva2VUb2tlblJlcXVlc3QSEgoFdG9rZW4YASABKAlCA4ABASIVChNSZXZva2VUb2tlblJlc3BvbnNlIicKFVB1dEFnZW50Q29uZmlnUmVxdWVzdBIOCgZidW5kbGUYASABKAwiKQoWUHV0QWdlbnRDb25maWdSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJIhsKGUdldEFnZW50Q29uZmlnSW5mb1JlcXVlc3Qi2gEKGkdldEFnZW50Q29uZmlnSW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSDgoGc2tpbGxzGAIgAygJEhIKCmV4dGVuc2lvbnMYAyADKAkSEwoLbWNwX3NlcnZlcnMYBCADKAkSFAoMaGFzX3NldHRpbmdzGAUgASgIEhUKDWhhc19hZ2VudHNfbWQYBiABKAgSDQoFcnVsZXMYByADKAkSEQoJc3ViYWdlbnRzGAggAygJEhIKCmhhc19tb2RlbHMYCSABKAgSDwoHcHJvbXB0cxgKIAMoCSIaChhEZWxldGVBZ2VudENvbmZpZ1JlcXVlc3QiGwoZRGVsZXRlQWdlbnRDb25maWdSZXNwb25zZSI0Cg5Nb2RlbENhbmRpZGF0ZRIQCghwcm92aWRlchgBIAEoCRIQCghtb2RlbF9pZBgCIAEoCSJxCg1Nb2RlbE1ldGFkYXRhEhYKDmNvbnRleHRfd2luZG93GAEgASgDEhwKFGlucHV0X2Nvc3RfbWljcm9fdXNkGAIgASgDEh0KFW91dHB1dF9jb3N0X21pY3JvX3VzZBgDIAEoAxILCgNhcGkYBCABKAkihwEKEk1vZGVsUmVnaXN0cnlFbnRyeRIUCgxkaXNwbGF5X25hbWUYASABKAkSLgoKY2FuZGlkYXRlcxgCIAMoCzIaLmNvbXBhc3MudjEuTW9kZWxDYW5kaWRhdGUSKwoIbWV0YWRhdGEYAyABKAsyGS5jb21wYXNzLnYxLk1vZGVsTWV0YWRhdGEimAEKDU1vZGVsUmVnaXN0cnkSNwoHZW50cmllcxgBIAMoCzImLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeS5FbnRyaWVzRW50cnkaTgoMRW50cmllc0VudHJ5EgsKA2tleRgBIAEoCRItCgV2YWx1ZRgCIAEoCzIeLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeUVudHJ5OgI4ASJgChdQdXRNb2RlbFJlZ2lzdHJ5UmVxdWVzdBIrCghyZWdpc3RyeRgBIAEoCzIZLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeRIYChBleHBlY3RlZF92ZXJzaW9uGAIgASgDIisKGFB1dE1vZGVsUmVnaXN0cnlSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgDIhkKF0dldE1vZGVsUmVnaXN0cnlSZXF1ZXN0IlgKGEdldE1vZGVsUmVnaXN0cnlSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgDEisKCHJlZ2lzdHJ5GAIgASgLMhkuY29tcGFzcy52MS5Nb2RlbFJlZ2lzdHJ5IhwKGkRlbGV0ZU1vZGVsUmVnaXN0cnlSZXF1ZXN0Ih0KG0RlbGV0ZU1vZGVsUmVnaXN0cnlSZXNwb25zZSI+ChBBZ2VudEF0dHJpYnV0aW9uEhQKDGFnZW50X2hhbmRsZRgBIAEoCRIUCgxvd25lcl9oYW5kbGUYAiABKAkiRQoIRm9yZ2VSZWYSKwoIcHJvdmlkZXIYASABKA4yGS5jb21wYXNzLnYxLkZvcmdlUHJvdmlkZXISDAoEaG9zdBgCIAEoCSLUAwoFSXNzdWUSCgoCaWQYASABKAkSIwoFZm9yZ2UYAiABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAyABKAkSDgoGbnVtYmVyGAQgASgNEg0KBXRpdGxlGAUgASgJEgwKBGJvZHkYBiABKAkSEwoLZm9yZ2Vfc3RhdGUYByABKAkSCwoDdXJsGAggASgJEisKBWFnZW50GAkgASgLMhwuY29tcGFzcy52MS5BZ2VudEF0dHJpYnV0aW9uEhUKDWZvcmdlX2FjY291bnQYCiABKAkSDgoGbGFiZWxzGAsgAygJEi4KCnVwZGF0ZWRfYXQYEyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEiUKBXN0YXRlGAwgASgOMhYuY29tcGFzcy52MS5Jc3N1ZVN0YXRlEhAKCHByaW9yaXR5GA0gASgJEhAKCGFzc2lnbmVlGA4gASgJEg8KB3N1bW1hcnkYDyABKAkSDgoGYnJhbmNoGBAgASgJEiQKA3BycxgRIAMoCzIXLmNvbXBhc3MudjEuUHVsbFJlcXVlc3QSJwoHdHJhY2tlchgSIAEoCzIWLmNvbXBhc3MudjEuVHJhY2tlclJlZiL7AwoLUHVsbFJlcXVlc3QSIwoFZm9yZ2UYASABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAiABKAkSDgoGbnVtYmVyGAMgASgNEg0KBXRpdGxlGAQgASgJEhMKC2ZvcmdlX3N0YXRlGAUgASgJEgsKA3VybBgGIAEoCRIQCghoZWFkX3JlZhgHIAEoCRIQCghiYXNlX3JlZhgIIAEoCRIrCgVhZ2VudBgJIAEoCzIcLmNvbXBhc3MudjEuQWdlbnRBdHRyaWJ1dGlvbhIVCg1mb3JnZV9hY2NvdW50GAogASgJEg0KBWRyYWZ0GAsgASgIEikKB2NoYW5nZWQYDCABKAsyGC5jb21wYXNzLnYxLkNoYW5nZWRTdGF0cxIpCgZjaGVja3MYDSABKAsyGS5jb21wYXNzLnYxLkNoZWNrc1N1bW1hcnkSIwoHcmV2aWV3cxgOIAMoCzISLmNvbXBhc3MudjEuUmV2aWV3EikKB3RocmVhZHMYDyADKAsyGC5jb21wYXNzLnYxLlJldmlld1RocmVhZBIQCghoZWFkX3NoYRgQIAEoCRIXCg9tZXJnZWFibGVfc3RhdGUYESABKAkSMAoLbWVyZ2VfcXVldWUYEiABKAsyGy5jb21wYXNzLnYxLk1lcmdlUXVldWVFbnRyeSJTCg1DaGVja3NTdW1tYXJ5EhAKCGhlYWRfc2hhGAEgASgJEg0KBXN0YXRlGAIgASgJEiEKBmNoZWNrcxgDIAMoCzIRLmNvbXBhc3MudjEuQ2hlY2siQwoFQ2hlY2sSDAoEbmFtZRgBIAEoCRINCgVzdGF0ZRgCIAEoCRILCgN1cmwYAyABKAkSEAoIcmVxdWlyZWQYBCABKAgiQwoMQ2hhbmdlZFN0YXRzEg0KBWZpbGVzGAEgASgNEhEKCWFkZGl0aW9ucxgCIAEoDRIRCglkZWxldGlvbnMYAyABKA0iaQoKVHJhY2tlclJlZhIMCgRraW5kGAEgASgJEgoKAmlkGAIgASgJEg4KBnN0YXR1cxgDIAEoCRILCgN1cmwYBCABKAkSEAoIc3RhdGVfaWQYBSABKAkSEgoKc3RhdGVfdHlwZRgGIAEoCSJaCgZSZXZpZXcSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIPCgd2ZXJkaWN0GAMgASgJEgwKBGJvZHkYBCABKAkSEQoJY29tbWl0X2lkGAUgASgJIlUKDFJldmlld1RocmVhZBIMCgRwYXRoGAEgASgJEhAKCHJlc29sdmVkGAIgASgIEiUKCGNvbW1lbnRzGAMgAygLMhMuY29tcGFzcy52MS5Db21tZW50IjcKB0NvbW1lbnQSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIMCgRib2R5GAMgASgJImIKD01lcmdlUXVldWVFbnRyeRIQCghwcm92aWRlchgBIAEoCRINCgVzdGF0ZRgCIAEoCRIuCgpjaGFuZ2VkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCpkCg5TZWNyZXREZWxpdmVyeRIfChtTRUNSRVRfREVMSVZFUllfVU5TUEVDSUZJRUQQABIYChRTRUNSRVRfREVMSVZFUllfRklMRRABEhcKE1NFQ1JFVF9ERUxJVkVSWV9FTlYQAipwCgpTZWNyZXRLaW5kEhsKF1NFQ1JFVF9LSU5EX1VOU1BFQ0lGSUVEEAASFwoTU0VDUkVUX0tJTkRfR0VORVJJQxABEhgKFFNFQ1JFVF9LSU5EX1BST1ZJREVSEAISEgoOU0VDUkVUX0tJTkRfR0gQAypbCgtTZWNyZXRTY29wZRIcChhTRUNSRVRfU0NPUEVfVU5TUEVDSUZJRUQQABIVChFTRUNSRVRfU0NPUEVfVVNFUhABEhcKE1NFQ1JFVF9TQ09QRV9URU5BTlQQAip9CgtUb3VyT3V0Y29tZRIcChhUT1VSX09VVENPTUVfVU5TUEVDSUZJRUQQABIYChRUT1VSX09VVENPTUVfU1RBUlRFRBABEhoKFlRPVVJfT1VUQ09NRV9ESVNNSVNTRUQQAhIaChZUT1VSX09VVENPTUVfQ09NUExFVEVEEAMqQwoLU2VydmVyU3RhdGUSHAoYU0VSVkVSX1NUQVRFX1VOU1BFQ0lGSUVEEAASFgoSU0VSVkVSX1NUQVRFX1JFQURZEAEqlwEKC1J1bnRpbWVUaWVyEhwKGFJVTlRJTUVfVElFUl9VTlNQRUNJRklFRBAAEhcKE1JVTlRJTUVfVElFUl9QT0RNQU4QARIYChRSVU5USU1FX1RJRVJfTUlDUk9WTRACEiAKHFJVTlRJTUVfVElFUl9BUFBMRV9DT05UQUlORVIQAxIVChFSVU5USU1FX1RJRVJfSE9TVBAEKmgKDUVncmVzc1Bvc3R1cmUSHgoaRUdSRVNTX1BPU1RVUkVfVU5TUEVDSUZJRUQQABIYChRFR1JFU1NfUE9TVFVSRV9BUk1FRBABEh0KGUVHUkVTU19QT1NUVVJFX1VORU5GT1JDRUQQAiqCAgoRQWdlbnRTZXNzaW9uU3RhdGUSIwofQUdFTlRfU0VTU0lPTl9TVEFURV9VTlNQRUNJRklFRBAAEiAKHEFHRU5UX1NFU1NJT05fU1RBVEVfU1RBUlRJTkcQARIdChlBR0VOVF9TRVNTSU9OX1NUQVRFX1JFQURZEAISHwobQUdFTlRfU0VTU0lPTl9TVEFURV9XT1JLSU5HEAMSHwobQUdFTlRfU0VTU0lPTl9TVEFURV9TVE9QUEVEEAQSHwobQUdFTlRfU0VTU0lPTl9TVEFURV9FUlJPUkVEEAUSJAogQUdFTlRfU0VTU0lPTl9TVEFURV9ESVNDT05ORUNURUQQBirSAQoTQWdlbnRUb29sQ2FsbFN0YXR1cxImCiJBR0VOVF9UT09MX0NBTExfU1RBVFVTX1VOU1BFQ0lGSUVEEAASIgoeQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19QRU5ESU5HEAESJgoiQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19JTl9QUk9HUkVTUxACEiQKIEFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfQ09NUExFVEVEEAMSIQodQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19GQUlMRUQQBCq0AQoUQWdlbnRQbGFuRW50cnlTdGF0dXMSJwojQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfVU5TUEVDSUZJRUQQABIjCh9BR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19QRU5ESU5HEAESJwojQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfSU5fUFJPR1JFU1MQAhIlCiFBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19DT01QTEVURUQQAyqEAQoUU2Vzc2lvbkluamVjdGlvbktpbmQSJgoiU0VTU0lPTl9JTkpFQ1RJT05fS0lORF9VTlNQRUNJRklFRBAAEiAKHFNFU1NJT05fSU5KRUNUSU9OX0tJTkRfU1RFRVIQARIiCh5TRVNTSU9OX0lOSkVDVElPTl9LSU5EX0RFTElWRVIQAip0ChBTZXNzaW9uRXJyb3JLaW5kEiIKHlNFU1NJT05fRVJST1JfS0lORF9VTlNQRUNJRklFRBAAEhwKGFNFU1NJT05fRVJST1JfS0lORF9FUlJPUhABEh4KGlNFU1NJT05fRVJST1JfS0lORF9BQk9SVEVEEAIq8QEKCklzc3VlU3RhdGUSGwoXSVNTVUVfU1RBVEVfVU5TUEVDSUZJRUQQABIXChNJU1NVRV9TVEFURV9CQUNLTE9HEAESFAoQSVNTVUVfU1RBVEVfVE9ETxACEhYKEklTU1VFX1NUQVRFX1FVRVVFRBADEhcKE0lTU1VFX1NUQVRFX0JMT0NLRUQQBBIbChdJU1NVRV9TVEFURV9JTl9QUk9HUkVTUxAFEhkKFUlTU1VFX1NUQVRFX0lOX1JFVklFVxAGEhQKEElTU1VFX1NUQVRFX0RPTkUQBxIYChRJU1NVRV9TVEFURV9BUkNISVZFRBAIKpwBCg1Gb3JnZVByb3ZpZGVyEh4KGkZPUkdFX1BST1ZJREVSX1VOU1BFQ0lGSUVEEAASGQoVRk9SR0VfUFJPVklERVJfR0lUSFVCEAESGQoVRk9SR0VfUFJPVklERVJfR0lUTEFCEAISGgoWRk9SR0VfUFJPVklERVJfRk9SR0VKTxADEhkKFUZPUkdFX1BST1ZJREVSX0xJTkVBUhAEMrMTCg5Db21wYXNzU2VydmljZRJUCg1HZXRTZXJ2ZXJJbmZvEiAuY29tcGFzcy52MS5HZXRTZXJ2ZXJJbmZvUmVxdWVzdBohLmNvbXBhc3MudjEuR2V0U2VydmVySW5mb1Jlc3BvbnNlEj8KBldob0FtSRIZLmNvbXBhc3MudjEuV2hvQW1JUmVxdWVzdBoaLmNvbXBhc3MudjEuV2hvQW1JUmVzcG9uc2USWgoPR2V0UmVmTGlua1J1bGVzEiIuY29tcGFzcy52MS5HZXRSZWZMaW5rUnVsZXNSZXF1ZXN0GiMuY29tcGFzcy52MS5HZXRSZWZMaW5rUnVsZXNSZXNwb25zZRJRCgxHZXRUb3VyU3RhdGUSHy5jb21wYXNzLnYxLkdldFRvdXJTdGF0ZVJlcXVlc3QaIC5jb21wYXNzLnYxLkdldFRvdXJTdGF0ZVJlc3BvbnNlElcKDkNsYWltVG91clN0YXJ0EiEuY29tcGFzcy52MS5DbGFpbVRvdXJTdGFydFJlcXVlc3QaIi5jb21wYXNzLnYxLkNsYWltVG91clN0YXJ0UmVzcG9uc2USUQoMU2V0VG91clN0YXRlEh8uY29tcGFzcy52MS5TZXRUb3VyU3RhdGVSZXF1ZXN0GiAuY29tcGFzcy52MS5TZXRUb3VyU3RhdGVSZXNwb25zZRJcCg9TdWJzY3JpYmVFdmVudHMSIi5jb21wYXNzLnYxLlN1YnNjcmliZUV2ZW50c1JlcXVlc3QaIy5jb21wYXNzLnYxLlN1YnNjcmliZUV2ZW50c1Jlc3BvbnNlMAESWgoPTGlzdEJvYXJkSXNzdWVzEiIuY29tcGFzcy52MS5MaXN0Qm9hcmRJc3N1ZXNSZXF1ZXN0GiMuY29tcGFzcy52MS5MaXN0Qm9hcmRJc3N1ZXNSZXNwb25zZRJRCgxTZWFyY2hJc3N1ZXMSHy5jb21wYXNzLnYxLlNlYXJjaElzc3Vlc1JlcXVlc3QaIC5jb21wYXNzLnYxLlNlYXJjaElzc3Vlc1Jlc3BvbnNlEnIKF1Byb3Zpc2lvbkFnZW50V29ya3NwYWNlEiouY29tcGFzcy52MS5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QaKy5jb21wYXNzLnYxLlByb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVzcG9uc2USYAoRU3RhcnRBZ2VudFNlc3Npb24SJC5jb21wYXNzLnYxLlN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBolLmNvbXBhc3MudjEuU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRJLCgpTcGF3bkFnZW50Eh0uY29tcGFzcy52MS5TcGF3bkFnZW50UmVxdWVzdBoeLmNvbXBhc3MudjEuU3Bhd25BZ2VudFJlc3BvbnNlEl0KEFN0b3BBZ2VudFNlc3Npb24SIy5jb21wYXNzLnYxLlN0b3BBZ2VudFNlc3Npb25SZXF1ZXN0GiQuY29tcGFzcy52MS5TdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2USWgoPU2tpcEJhdGNoV2luZG93EiIuY29tcGFzcy52MS5Ta2lwQmF0Y2hXaW5kb3dSZXF1ZXN0GiMuY29tcGFzcy52MS5Ta2lwQmF0Y2hXaW5kb3dSZXNwb25zZRJpChRSZW1vdmVBZ2VudFdvcmtzcGFjZRInLmNvbXBhc3MudjEuUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0GiguY29tcGFzcy52MS5SZW1vdmVBZ2VudFdvcmtzcGFjZVJlc3BvbnNlEmMKElJlbG9hZEFnZW50U2Vzc2lvbhIlLmNvbXBhc3MudjEuUmVsb2FkQWdlbnRTZXNzaW9uUmVxdWVzdBomLmNvbXBhc3MudjEuUmVsb2FkQWdlbnRTZXNzaW9uUmVzcG9uc2USVwoOR2V0QWdlbnRTdGF0dXMSIS5jb21wYXNzLnYxLkdldEFnZW50U3RhdHVzUmVxdWVzdBoiLmNvbXBhc3MudjEuR2V0QWdlbnRTdGF0dXNSZXNwb25zZRJiChVTdWJzY3JpYmVBZ2VudFNlc3Npb24SKC5jb21wYXNzLnYxLlN1YnNjcmliZUFnZW50U2Vzc2lvblJlcXVlc3QaHS5jb21wYXNzLnYxLkFnZW50U2Vzc2lvbkZyYW1lMAESSwoKSXNzdWVUb2tlbhIdLmNvbXBhc3MudjEuSXNzdWVUb2tlblJlcXVlc3QaHi5jb21wYXNzLnYxLklzc3VlVG9rZW5SZXNwb25zZRJUCg1SZW5hbWVBY2NvdW50EiAuY29tcGFzcy52MS5SZW5hbWVBY2NvdW50UmVxdWVzdBohLmNvbXBhc3MudjEuUmVuYW1lQWNjb3VudFJlc3BvbnNlEk4KC1Jldm9rZVRva2VuEh4uY29tcGFzcy52MS5SZXZva2VUb2tlblJlcXVlc3QaHy5jb21wYXNzLnYxLlJldm9rZVRva2VuUmVzcG9uc2USVwoOUHV0QWdlbnRDb25maWcSIS5jb21wYXNzLnYxLlB1dEFnZW50Q29uZmlnUmVxdWVzdBoiLmNvbXBhc3MudjEuUHV0QWdlbnRDb25maWdSZXNwb25zZRJjChJHZXRBZ2VudENvbmZpZ0luZm8SJS5jb21wYXNzLnYxLkdldEFnZW50Q29uZmlnSW5mb1JlcXVlc3QaJi5jb21wYXNzLnYxLkdldEFnZW50Q29uZmlnSW5mb1Jlc3BvbnNlEmAKEURlbGV0ZUFnZW50Q29uZmlnEiQuY29tcGFzcy52MS5EZWxldGVBZ2VudENvbmZpZ1JlcXVlc3QaJS5jb21wYXNzLnYxLkRlbGV0ZUFnZW50Q29uZmlnUmVzcG9uc2USXQoQUHV0TW9kZWxSZWdpc3RyeRIjLmNvbXBhc3MudjEuUHV0TW9kZWxSZWdpc3RyeVJlcXVlc3QaJC5jb21wYXNzLnYxLlB1dE1vZGVsUmVnaXN0cnlSZXNwb25zZRJdChBHZXRNb2RlbFJlZ2lzdHJ5EiMuY29tcGFzcy52MS5HZXRNb2RlbFJlZ2lzdHJ5UmVxdWVzdBokLmNvbXBhc3MudjEuR2V0TW9kZWxSZWdpc3RyeVJlc3BvbnNlEmYKE0RlbGV0ZU1vZGVsUmVnaXN0cnkSJi5jb21wYXNzLnYxLkRlbGV0ZU1vZGVsUmVnaXN0cnlSZXF1ZXN0GicuY29tcGFzcy52MS5EZWxldGVNb2RlbFJlZ2lzdHJ5UmVzcG9uc2Uy3wIKDlNlY3JldHNTZXJ2aWNlEkgKCVNldFNlY3JldBIcLmNvbXBhc3MudjEuU2V0U2VjcmV0UmVxdWVzdBodLmNvbXBhc3MudjEuU2V0U2VjcmV0UmVzcG9uc2USTgoLTGlzdFNlY3JldHMSHi5jb21wYXNzLnYxLkxpc3RTZWNyZXRzUmVxdWVzdBofLmNvbXBhc3MudjEuTGlzdFNlY3JldHNSZXNwb25zZRJRCgxEZWxldGVTZWNyZXQSHy5jb21wYXNzLnYxLkRlbGV0ZVNlY3JldFJlcXVlc3QaIC5jb21wYXNzLnYxLkRlbGV0ZVNlY3JldFJlc3BvbnNlEmAKEUxpc3RTZXJ2ZXJTZWNyZXRzEiQuY29tcGFzcy52MS5MaXN0U2VydmVyU2VjcmV0c1JlcXVlc3QaJS5jb21wYXNzLnYxLkxpc3RTZXJ2ZXJTZWNyZXRzUmVzcG9uc2Uy3wIKFkFnZW50UmVwb3NpdG9yeVNlcnZpY2USaQoUR3JhbnRBZ2VudFJlcG9zaXRvcnkSJy5jb21wYXNzLnYxLkdyYW50QWdlbnRSZXBvc2l0b3J5UmVxdWVzdBooLmNvbXBhc3MudjEuR3JhbnRBZ2VudFJlcG9zaXRvcnlSZXNwb25zZRJsChVSZXZva2VBZ2VudFJlcG9zaXRvcnkSKC5jb21wYXNzLnYxLlJldm9rZUFnZW50UmVwb3NpdG9yeVJlcXVlc3QaKS5jb21wYXNzLnYxLlJldm9rZUFnZW50UmVwb3NpdG9yeVJlc3BvbnNlEmwKFUxpc3RBZ2VudFJlcG9zaXRvcmllcxIoLmNvbXBhc3MudjEuTGlzdEFnZW50UmVwb3NpdG9yaWVzUmVxdWVzdBopLmNvbXBhc3MudjEuTGlzdEFnZW50UmVwb3NpdG9yaWVzUmVzcG9uc2ViBnByb3RvMw", [file_google_protobuf_timestamp]); + fileDesc("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEiRwobR3JhbnRBZ2VudFJlcG9zaXRvcnlSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCRISCgpyZXBvc2l0b3J5GAIgASgJIi0KHEdyYW50QWdlbnRSZXBvc2l0b3J5UmVzcG9uc2USDQoFYWRkZWQYASABKAgiSAocUmV2b2tlQWdlbnRSZXBvc2l0b3J5UmVxdWVzdBIUCgxhZ2VudF9oYW5kbGUYASABKAkSEgoKcmVwb3NpdG9yeRgCIAEoCSIwCh1SZXZva2VBZ2VudFJlcG9zaXRvcnlSZXNwb25zZRIPCgdyZW1vdmVkGAEgASgIIjQKHExpc3RBZ2VudFJlcG9zaXRvcmllc1JlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJIjUKHUxpc3RBZ2VudFJlcG9zaXRvcmllc1Jlc3BvbnNlEhQKDHJlcG9zaXRvcmllcxgBIAMoCSLQAQoQU2V0U2VjcmV0UmVxdWVzdBIMCgRuYW1lGAEgASgJEhIKBXZhbHVlGAIgASgJQgOAAQESLAoIZGVsaXZlcnkYAyABKA4yGi5jb21wYXNzLnYxLlNlY3JldERlbGl2ZXJ5EiQKBGtpbmQYBCABKA4yFi5jb21wYXNzLnYxLlNlY3JldEtpbmQSEAoIcHJvdmlkZXIYBSABKAkSDAoEaG9zdBgGIAEoCRImCgVzY29wZRgHIAEoDjIXLmNvbXBhc3MudjEuU2VjcmV0U2NvcGUiEwoRU2V0U2VjcmV0UmVzcG9uc2UiFAoSTGlzdFNlY3JldHNSZXF1ZXN0IkAKE0xpc3RTZWNyZXRzUmVzcG9uc2USKQoHc2VjcmV0cxgBIAMoCzIYLmNvbXBhc3MudjEuU2VjcmV0U3RhdHVzIqABCgxTZWNyZXRTdGF0dXMSDAoEbmFtZRgBIAEoCRIOCgZpc19zZXQYAiABKAgSLAoIZGVsaXZlcnkYAyABKA4yGi5jb21wYXNzLnYxLlNlY3JldERlbGl2ZXJ5EiQKBGtpbmQYBCABKA4yFi5jb21wYXNzLnYxLlNlY3JldEtpbmQSEAoIcHJvdmlkZXIYBSABKAkSDAoEaG9zdBgGIAEoCSJLChNEZWxldGVTZWNyZXRSZXF1ZXN0EgwKBG5hbWUYASABKAkSJgoFc2NvcGUYAiABKA4yFy5jb21wYXNzLnYxLlNlY3JldFNjb3BlIhYKFERlbGV0ZVNlY3JldFJlc3BvbnNlIhoKGExpc3RTZXJ2ZXJTZWNyZXRzUmVxdWVzdCJTChlMaXN0U2VydmVyU2VjcmV0c1Jlc3BvbnNlEjYKDnNlcnZlcl9zZWNyZXRzGAEgAygLMh4uY29tcGFzcy52MS5TZXJ2ZXJTZWNyZXRTdGF0dXMiMgoSU2VydmVyU2VjcmV0U3RhdHVzEgwKBG5hbWUYASABKAkSDgoGaXNfc2V0GAIgASgIIhgKFkdldFJlZkxpbmtSdWxlc1JlcXVlc3QiQQoLUmVmTGlua1J1bGUSDAoEa2luZBgBIAEoCRIOCgZwcmVmaXgYAiABKAkSFAoMdXJsX3RlbXBsYXRlGAMgASgJIkEKF0dldFJlZkxpbmtSdWxlc1Jlc3BvbnNlEiYKBXJ1bGVzGAEgAygLMhcuY29tcGFzcy52MS5SZWZMaW5rUnVsZSIWChRHZXRTZXJ2ZXJJbmZvUmVxdWVzdCKAAQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSEwoLYXBpX3ZlcnNpb24YAiABKAkSCwoDcmV2GAMgASgJEjQKEGVucm9sbGVkX3J1bm5lcnMYBCADKAsyGi5jb21wYXNzLnYxLkVucm9sbGVkUnVubmVyIjAKDkVucm9sbGVkUnVubmVyEgoKAmlkGAEgASgJEhIKCmVucm9sbG1lbnQYAiABKAQiDwoNV2hvQW1JUmVxdWVzdCIkCg5XaG9BbUlSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJIhUKE0dldFRvdXJTdGF0ZVJlcXVlc3QiUQoUR2V0VG91clN0YXRlUmVzcG9uc2USKAoHb3V0Y29tZRgBIAEoDjIXLmNvbXBhc3MudjEuVG91ck91dGNvbWUSDwoHc3RlcF9pZBgCIAEoCSIoChVDbGFpbVRvdXJTdGFydFJlcXVlc3QSDwoHc3RlcF9pZBgBIAEoCSIpChZDbGFpbVRvdXJTdGFydFJlc3BvbnNlEg8KB2NsYWltZWQYASABKAgiUAoTU2V0VG91clN0YXRlUmVxdWVzdBIoCgdvdXRjb21lGAEgASgOMhcuY29tcGFzcy52MS5Ub3VyT3V0Y29tZRIPCgdzdGVwX2lkGAIgASgJIhYKFFNldFRvdXJTdGF0ZVJlc3BvbnNlIkMKFlN1YnNjcmliZUV2ZW50c1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEIuIDChdTdWJzY3JpYmVFdmVudHNSZXNwb25zZRILCgNzZXEYASABKAQSEgoKYXRfdW5peF9tcxgCIAEoAxIWCg5pbnN0YW5jZV9lcG9jaBgDIAEoBBIUCgxzbmFwc2hvdF9zZXEYBCABKAQSMQoNc2VydmVyX3N0YXR1cxgKIAEoCzIYLmNvbXBhc3MudjEuU2VydmVyU3RhdHVzSAASNQoPcmVzeW5jX3JlcXVpcmVkGAsgASgLMhouY29tcGFzcy52MS5SZXN5bmNSZXF1aXJlZEgAEj4KFGFnZW50X3Nlc3Npb25fc3RhdHVzGAwgASgLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXNIABI8ChNhZ2VudF9tZXNzYWdlX2NodW5rGA0gASgLMh0uY29tcGFzcy52MS5BZ2VudE1lc3NhZ2VDaHVua0gAEjQKD2FnZW50X3Rvb2xfY2FsbBgOIAEoCzIZLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbEgAEisKCmFnZW50X3BsYW4YDyABKAsyFS5jb21wYXNzLnYxLkFnZW50UGxhbkgAEiIKBWlzc3VlGBAgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAQgkKB3BheWxvYWQiLgoWTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPAoXTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSJJChNTZWFyY2hJc3N1ZXNSZXF1ZXN0Eg0KBXF1ZXJ5GAEgASgJEg0KBWxpbWl0GAIgASgNEhQKDHNuYXBzaG90X3NlcRgDIAEoBCI5ChRTZWFyY2hJc3N1ZXNSZXNwb25zZRIhCgZpc3N1ZXMYASADKAsyES5jb21wYXNzLnYxLklzc3VlIjYKDFNlcnZlclN0YXR1cxImCgVzdGF0ZRgBIAEoDjIXLmNvbXBhc3MudjEuU2VydmVyU3RhdGUiEAoOUmVzeW5jUmVxdWlyZWQi0gEKEkFnZW50U2Vzc2lvblN0YXR1cxISCgpzZXNzaW9uX2lkGAEgASgJEiwKBXN0YXRlGAIgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZRIYChBhZ2VudF9hY2NvdW50X2lkGAMgASgJEi0KDHJ1bnRpbWVfdGllchgEIAEoDjIXLmNvbXBhc3MudjEuUnVudGltZVRpZXISMQoOZWdyZXNzX3Bvc3R1cmUYBSABKA4yGS5jb21wYXNzLnYxLkVncmVzc1Bvc3R1cmUiSQoRQWdlbnRNZXNzYWdlQ2h1bmsSEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgR0ZXh0GAIgASgJEhIKCmlzX3Rob3VnaHQYAyABKAgieQoNQWdlbnRUb29sQ2FsbBISCgpzZXNzaW9uX2lkGAEgASgJEhQKDHRvb2xfY2FsbF9pZBgCIAEoCRINCgV0aXRsZRgDIAEoCRIvCgZzdGF0dXMYBCABKA4yHy5jb21wYXNzLnYxLkFnZW50VG9vbENhbGxTdGF0dXMiTAoJQWdlbnRQbGFuEhIKCnNlc3Npb25faWQYASABKAkSKwoHZW50cmllcxgCIAMoCzIaLmNvbXBhc3MudjEuQWdlbnRQbGFuRW50cnkiUwoOQWdlbnRQbGFuRW50cnkSDwoHY29udGVudBgBIAEoCRIwCgZzdGF0dXMYAiABKA4yIC5jb21wYXNzLnYxLkFnZW50UGxhbkVudHJ5U3RhdHVzIpkECgxTZXNzaW9uRXZlbnQSEAoIZXZlbnRfaWQYASABKAkSEgoKYXRfdW5peF9tcxgCIAEoAxI6Cg5hc3Npc3RhbnRfdGV4dBgDIAEoCzIgLmNvbXBhc3MudjEuU2Vzc2lvbkFzc2lzdGFudFRleHRIABIvCgh0aGlua2luZxgEIAEoCzIbLmNvbXBhc3MudjEuU2Vzc2lvblRoaW5raW5nSAASMAoJdG9vbF9jYWxsGAUgASgLMhsuY29tcGFzcy52MS5TZXNzaW9uVG9vbENhbGxIABI9ChB0b29sX2NhbGxfdXBkYXRlGAYgASgLMiEuY29tcGFzcy52MS5TZXNzaW9uVG9vbENhbGxVcGRhdGVIABInCgRwbGFuGAcgASgLMhcuY29tcGFzcy52MS5TZXNzaW9uUGxhbkgAEisKBm5vdGljZRgIIAEoCzIZLmNvbXBhc3MudjEuU2Vzc2lvbk5vdGljZUgAEjkKEXNlc3Npb25faW5qZWN0aW9uGAkgASgLMhwuY29tcGFzcy52MS5TZXNzaW9uSW5qZWN0aW9uSAASMQoNc2Vzc2lvbl9lcnJvchgKIAEoCzIYLmNvbXBhc3MudjEuU2Vzc2lvbkVycm9ySAASOAoNYmF0Y2hfcGVuZGluZxgLIAEoCzIfLmNvbXBhc3MudjEuU2Vzc2lvbkJhdGNoUGVuZGluZ0gAQgcKBWV2ZW50IjgKFFNlc3Npb25Bc3Npc3RhbnRUZXh0EgwKBHRleHQYASABKAkSEgoKbWVzc2FnZV9pZBgCIAEoCSIzCg9TZXNzaW9uVGhpbmtpbmcSDAoEdGV4dBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJImcKD1Nlc3Npb25Ub29sQ2FsbBIUCgx0b29sX2NhbGxfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSLwoGc3RhdHVzGAMgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIpoBChVTZXNzaW9uVG9vbENhbGxVcGRhdGUSFAoMdG9vbF9jYWxsX2lkGAEgASgJEi8KBnN0YXR1cxgCIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cxIOCgZvdXRwdXQYAyABKAkSKgoFZGlmZnMYBCADKAsyGy5jb21wYXNzLnYxLlNlc3Npb25GaWxlRGlmZiJVCg9TZXNzaW9uRmlsZURpZmYSDAoEcGF0aBgBIAEoCRIVCghvbGRfdGV4dBgCIAEoCUgAiAEBEhAKCG5ld190ZXh0GAMgASgJQgsKCV9vbGRfdGV4dCI6CgtTZXNzaW9uUGxhbhIrCgdlbnRyaWVzGAEgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSI5Cg1TZXNzaW9uTm90aWNlEgwKBHRleHQYASABKAkSEQoEbGluaxgCIAEoCUgAiAEBQgcKBV9saW5rIoMBChBTZXNzaW9uSW5qZWN0aW9uEjEKB29wX2tpbmQYASABKA4yIC5jb21wYXNzLnYxLlNlc3Npb25JbmplY3Rpb25LaW5kEhIKCm1lc3NhZ2VfaWQYAiABKAkSEwoLZnJvbV9oYW5kbGUYAyABKAkSEwoLdHJhY2VwYXJlbnQYBCABKAkiawoMU2Vzc2lvbkVycm9yEioKBGtpbmQYASABKA4yHC5jb21wYXNzLnYxLlNlc3Npb25FcnJvcktpbmQSDwoHbWVzc2FnZRgCIAEoCRITCgZzdGF0dXMYAyABKAVIAIgBAUIJCgdfc3RhdHVzIj4KE1Nlc3Npb25CYXRjaFBlbmRpbmcSDQoFY291bnQYASABKA0SGAoQZmlyZXNfYXRfdW5peF9tcxgCIAEoAyIyChxTdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkilQEKEUFnZW50U2Vzc2lvbkZyYW1lEhIKCnNlc3Npb25faWQYASABKAkSJwoFZXZlbnQYAiABKAsyGC5jb21wYXNzLnYxLlNlc3Npb25FdmVudBIsCgVzdGF0ZRgDIAEoDjIdLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdGUSFQoNdHVybl9zZXF1ZW5jZRgEIAEoBCJwCh5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJEg8KB3BlcnNvbmEYAyABKAkSDAoEcm9sZRgEIAEoCSI5Ch9Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEhYKDmNvbnRhaW5lcl9uYW1lGAEgASgJIlAKG1JlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgCIAEoCSIeChxSZW1vdmVBZ2VudFdvcmtzcGFjZVJlc3BvbnNlImMKGFN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFyZXN1bWVfc2Vzc2lvbl9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiLwoZU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIloKEVNwYXduQWdlbnRSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiQAoSU3Bhd25BZ2VudFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSFgoOY29udGFpbmVyX25hbWUYAiABKAkiLQoXU3RvcEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIaChhTdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2UiLAoWU2tpcEJhdGNoV2luZG93UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIhkKF1NraXBCYXRjaFdpbmRvd1Jlc3BvbnNlIi8KGVJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIwChpSZWxvYWRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIisKFUdldEFnZW50U3RhdHVzUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJInwKFkdldEFnZW50U3RhdHVzUmVzcG9uc2USMAoIc3RhdHVzZXMYASADKAsyHi5jb21wYXNzLnYxLkFnZW50U2Vzc2lvblN0YXR1cxIcCg9ydW5uZXJfZW5yb2xsZWQYAiABKAhIAIgBAUISChBfcnVubmVyX2Vucm9sbGVkIisKEUlzc3VlVG9rZW5SZXF1ZXN0EhYKDmFjY291bnRfaGFuZGxlGAEgASgJIiMKEklzc3VlVG9rZW5SZXNwb25zZRINCgV0b2tlbhgBIAEoCSJCChRSZW5hbWVBY2NvdW50UmVxdWVzdBIWCg5hY2NvdW50X2hhbmRsZRgBIAEoCRISCgpuZXdfaGFuZGxlGAIgASgJIjsKFVJlbmFtZUFjY291bnRSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJEg4KBmhhbmRsZRgCIAEoCSIoChJSZXZva2VUb2tlblJlcXVlc3QSEgoFdG9rZW4YASABKAlCA4ABASIVChNSZXZva2VUb2tlblJlc3BvbnNlIicKFVB1dEFnZW50Q29uZmlnUmVxdWVzdBIOCgZidW5kbGUYASABKAwiKQoWUHV0QWdlbnRDb25maWdSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJIhsKGUdldEFnZW50Q29uZmlnSW5mb1JlcXVlc3Qi2gEKGkdldEFnZW50Q29uZmlnSW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSDgoGc2tpbGxzGAIgAygJEhIKCmV4dGVuc2lvbnMYAyADKAkSEwoLbWNwX3NlcnZlcnMYBCADKAkSFAoMaGFzX3NldHRpbmdzGAUgASgIEhUKDWhhc19hZ2VudHNfbWQYBiABKAgSDQoFcnVsZXMYByADKAkSEQoJc3ViYWdlbnRzGAggAygJEhIKCmhhc19tb2RlbHMYCSABKAgSDwoHcHJvbXB0cxgKIAMoCSIaChhEZWxldGVBZ2VudENvbmZpZ1JlcXVlc3QiGwoZRGVsZXRlQWdlbnRDb25maWdSZXNwb25zZSI0Cg5Nb2RlbENhbmRpZGF0ZRIQCghwcm92aWRlchgBIAEoCRIQCghtb2RlbF9pZBgCIAEoCSJxCg1Nb2RlbE1ldGFkYXRhEhYKDmNvbnRleHRfd2luZG93GAEgASgDEhwKFGlucHV0X2Nvc3RfbWljcm9fdXNkGAIgASgDEh0KFW91dHB1dF9jb3N0X21pY3JvX3VzZBgDIAEoAxILCgNhcGkYBCABKAkihwEKEk1vZGVsUmVnaXN0cnlFbnRyeRIUCgxkaXNwbGF5X25hbWUYASABKAkSLgoKY2FuZGlkYXRlcxgCIAMoCzIaLmNvbXBhc3MudjEuTW9kZWxDYW5kaWRhdGUSKwoIbWV0YWRhdGEYAyABKAsyGS5jb21wYXNzLnYxLk1vZGVsTWV0YWRhdGEimAEKDU1vZGVsUmVnaXN0cnkSNwoHZW50cmllcxgBIAMoCzImLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeS5FbnRyaWVzRW50cnkaTgoMRW50cmllc0VudHJ5EgsKA2tleRgBIAEoCRItCgV2YWx1ZRgCIAEoCzIeLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeUVudHJ5OgI4ASJgChdQdXRNb2RlbFJlZ2lzdHJ5UmVxdWVzdBIrCghyZWdpc3RyeRgBIAEoCzIZLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeRIYChBleHBlY3RlZF92ZXJzaW9uGAIgASgDIisKGFB1dE1vZGVsUmVnaXN0cnlSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgDIhkKF0dldE1vZGVsUmVnaXN0cnlSZXF1ZXN0IlgKGEdldE1vZGVsUmVnaXN0cnlSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgDEisKCHJlZ2lzdHJ5GAIgASgLMhkuY29tcGFzcy52MS5Nb2RlbFJlZ2lzdHJ5IhwKGkRlbGV0ZU1vZGVsUmVnaXN0cnlSZXF1ZXN0Ih0KG0RlbGV0ZU1vZGVsUmVnaXN0cnlSZXNwb25zZSI+ChBBZ2VudEF0dHJpYnV0aW9uEhQKDGFnZW50X2hhbmRsZRgBIAEoCRIUCgxvd25lcl9oYW5kbGUYAiABKAkiRQoIRm9yZ2VSZWYSKwoIcHJvdmlkZXIYASABKA4yGS5jb21wYXNzLnYxLkZvcmdlUHJvdmlkZXISDAoEaG9zdBgCIAEoCSJyChJGb3JnZU1pcnJvclNldHRpbmcSIwoFZm9yZ2UYASABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAiABKAkSKQoEbW9kZRgDIAEoDjIbLmNvbXBhc3MudjEuRm9yZ2VNaXJyb3JNb2RlIiAKHkxpc3RGb3JnZU1pcnJvclNldHRpbmdzUmVxdWVzdCJlCh9MaXN0Rm9yZ2VNaXJyb3JTZXR0aW5nc1Jlc3BvbnNlEjAKCHNldHRpbmdzGAEgAygLMh4uY29tcGFzcy52MS5Gb3JnZU1pcnJvclNldHRpbmcSEAoIY2FuX2VkaXQYAiABKAgiTwocU2V0Rm9yZ2VNaXJyb3JTZXR0aW5nUmVxdWVzdBIvCgdzZXR0aW5nGAEgASgLMh4uY29tcGFzcy52MS5Gb3JnZU1pcnJvclNldHRpbmciUAodU2V0Rm9yZ2VNaXJyb3JTZXR0aW5nUmVzcG9uc2USLwoHc2V0dGluZxgBIAEoCzIeLmNvbXBhc3MudjEuRm9yZ2VNaXJyb3JTZXR0aW5nItQDCgVJc3N1ZRIKCgJpZBgBIAEoCRIjCgVmb3JnZRgCIAEoCzIULmNvbXBhc3MudjEuRm9yZ2VSZWYSDAoEcmVwbxgDIAEoCRIOCgZudW1iZXIYBCABKA0SDQoFdGl0bGUYBSABKAkSDAoEYm9keRgGIAEoCRITCgtmb3JnZV9zdGF0ZRgHIAEoCRILCgN1cmwYCCABKAkSKwoFYWdlbnQYCSABKAsyHC5jb21wYXNzLnYxLkFnZW50QXR0cmlidXRpb24SFQoNZm9yZ2VfYWNjb3VudBgKIAEoCRIOCgZsYWJlbHMYCyADKAkSLgoKdXBkYXRlZF9hdBgTIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASJQoFc3RhdGUYDCABKA4yFi5jb21wYXNzLnYxLklzc3VlU3RhdGUSEAoIcHJpb3JpdHkYDSABKAkSEAoIYXNzaWduZWUYDiABKAkSDwoHc3VtbWFyeRgPIAEoCRIOCgZicmFuY2gYECABKAkSJAoDcHJzGBEgAygLMhcuY29tcGFzcy52MS5QdWxsUmVxdWVzdBInCgd0cmFja2VyGBIgASgLMhYuY29tcGFzcy52MS5UcmFja2VyUmVmIvsDCgtQdWxsUmVxdWVzdBIjCgVmb3JnZRgBIAEoCzIULmNvbXBhc3MudjEuRm9yZ2VSZWYSDAoEcmVwbxgCIAEoCRIOCgZudW1iZXIYAyABKA0SDQoFdGl0bGUYBCABKAkSEwoLZm9yZ2Vfc3RhdGUYBSABKAkSCwoDdXJsGAYgASgJEhAKCGhlYWRfcmVmGAcgASgJEhAKCGJhc2VfcmVmGAggASgJEisKBWFnZW50GAkgASgLMhwuY29tcGFzcy52MS5BZ2VudEF0dHJpYnV0aW9uEhUKDWZvcmdlX2FjY291bnQYCiABKAkSDQoFZHJhZnQYCyABKAgSKQoHY2hhbmdlZBgMIAEoCzIYLmNvbXBhc3MudjEuQ2hhbmdlZFN0YXRzEikKBmNoZWNrcxgNIAEoCzIZLmNvbXBhc3MudjEuQ2hlY2tzU3VtbWFyeRIjCgdyZXZpZXdzGA4gAygLMhIuY29tcGFzcy52MS5SZXZpZXcSKQoHdGhyZWFkcxgPIAMoCzIYLmNvbXBhc3MudjEuUmV2aWV3VGhyZWFkEhAKCGhlYWRfc2hhGBAgASgJEhcKD21lcmdlYWJsZV9zdGF0ZRgRIAEoCRIwCgttZXJnZV9xdWV1ZRgSIAEoCzIbLmNvbXBhc3MudjEuTWVyZ2VRdWV1ZUVudHJ5IlMKDUNoZWNrc1N1bW1hcnkSEAoIaGVhZF9zaGEYASABKAkSDQoFc3RhdGUYAiABKAkSIQoGY2hlY2tzGAMgAygLMhEuY29tcGFzcy52MS5DaGVjayJDCgVDaGVjaxIMCgRuYW1lGAEgASgJEg0KBXN0YXRlGAIgASgJEgsKA3VybBgDIAEoCRIQCghyZXF1aXJlZBgEIAEoCCJDCgxDaGFuZ2VkU3RhdHMSDQoFZmlsZXMYASABKA0SEQoJYWRkaXRpb25zGAIgASgNEhEKCWRlbGV0aW9ucxgDIAEoDSJpCgpUcmFja2VyUmVmEgwKBGtpbmQYASABKAkSCgoCaWQYAiABKAkSDgoGc3RhdHVzGAMgASgJEgsKA3VybBgEIAEoCRIQCghzdGF0ZV9pZBgFIAEoCRISCgpzdGF0ZV90eXBlGAYgASgJIloKBlJldmlldxIOCgZhdXRob3IYASABKAkSDgoGaXNfYm90GAIgASgIEg8KB3ZlcmRpY3QYAyABKAkSDAoEYm9keRgEIAEoCRIRCgljb21taXRfaWQYBSABKAkiVQoMUmV2aWV3VGhyZWFkEgwKBHBhdGgYASABKAkSEAoIcmVzb2x2ZWQYAiABKAgSJQoIY29tbWVudHMYAyADKAsyEy5jb21wYXNzLnYxLkNvbW1lbnQiNwoHQ29tbWVudBIOCgZhdXRob3IYASABKAkSDgoGaXNfYm90GAIgASgIEgwKBGJvZHkYAyABKAkiYgoPTWVyZ2VRdWV1ZUVudHJ5EhAKCHByb3ZpZGVyGAEgASgJEg0KBXN0YXRlGAIgASgJEi4KCmNoYW5nZWRfYXQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wKmQKDlNlY3JldERlbGl2ZXJ5Eh8KG1NFQ1JFVF9ERUxJVkVSWV9VTlNQRUNJRklFRBAAEhgKFFNFQ1JFVF9ERUxJVkVSWV9GSUxFEAESFwoTU0VDUkVUX0RFTElWRVJZX0VOVhACKnAKClNlY3JldEtpbmQSGwoXU0VDUkVUX0tJTkRfVU5TUEVDSUZJRUQQABIXChNTRUNSRVRfS0lORF9HRU5FUklDEAESGAoUU0VDUkVUX0tJTkRfUFJPVklERVIQAhISCg5TRUNSRVRfS0lORF9HSBADKlsKC1NlY3JldFNjb3BlEhwKGFNFQ1JFVF9TQ09QRV9VTlNQRUNJRklFRBAAEhUKEVNFQ1JFVF9TQ09QRV9VU0VSEAESFwoTU0VDUkVUX1NDT1BFX1RFTkFOVBACKn0KC1RvdXJPdXRjb21lEhwKGFRPVVJfT1VUQ09NRV9VTlNQRUNJRklFRBAAEhgKFFRPVVJfT1VUQ09NRV9TVEFSVEVEEAESGgoWVE9VUl9PVVRDT01FX0RJU01JU1NFRBACEhoKFlRPVVJfT1VUQ09NRV9DT01QTEVURUQQAypDCgtTZXJ2ZXJTdGF0ZRIcChhTRVJWRVJfU1RBVEVfVU5TUEVDSUZJRUQQABIWChJTRVJWRVJfU1RBVEVfUkVBRFkQASqXAQoLUnVudGltZVRpZXISHAoYUlVOVElNRV9USUVSX1VOU1BFQ0lGSUVEEAASFwoTUlVOVElNRV9USUVSX1BPRE1BThABEhgKFFJVTlRJTUVfVElFUl9NSUNST1ZNEAISIAocUlVOVElNRV9USUVSX0FQUExFX0NPTlRBSU5FUhADEhUKEVJVTlRJTUVfVElFUl9IT1NUEAQqaAoNRWdyZXNzUG9zdHVyZRIeChpFR1JFU1NfUE9TVFVSRV9VTlNQRUNJRklFRBAAEhgKFEVHUkVTU19QT1NUVVJFX0FSTUVEEAESHQoZRUdSRVNTX1BPU1RVUkVfVU5FTkZPUkNFRBACKoICChFBZ2VudFNlc3Npb25TdGF0ZRIjCh9BR0VOVF9TRVNTSU9OX1NUQVRFX1VOU1BFQ0lGSUVEEAASIAocQUdFTlRfU0VTU0lPTl9TVEFURV9TVEFSVElORxABEh0KGUFHRU5UX1NFU1NJT05fU1RBVEVfUkVBRFkQAhIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX1dPUktJTkcQAxIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX1NUT1BQRUQQBBIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX0VSUk9SRUQQBRIkCiBBR0VOVF9TRVNTSU9OX1NUQVRFX0RJU0NPTk5FQ1RFRBAGKtIBChNBZ2VudFRvb2xDYWxsU3RhdHVzEiYKIkFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfVU5TUEVDSUZJRUQQABIiCh5BR0VOVF9UT09MX0NBTExfU1RBVFVTX1BFTkRJTkcQARImCiJBR0VOVF9UT09MX0NBTExfU1RBVFVTX0lOX1BST0dSRVNTEAISJAogQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19DT01QTEVURUQQAxIhCh1BR0VOVF9UT09MX0NBTExfU1RBVFVTX0ZBSUxFRBAEKrQBChRBZ2VudFBsYW5FbnRyeVN0YXR1cxInCiNBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19VTlNQRUNJRklFRBAAEiMKH0FHRU5UX1BMQU5fRU5UUllfU1RBVFVTX1BFTkRJTkcQARInCiNBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19JTl9QUk9HUkVTUxACEiUKIUFHRU5UX1BMQU5fRU5UUllfU1RBVFVTX0NPTVBMRVRFRBADKoQBChRTZXNzaW9uSW5qZWN0aW9uS2luZBImCiJTRVNTSU9OX0lOSkVDVElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASIAocU0VTU0lPTl9JTkpFQ1RJT05fS0lORF9TVEVFUhABEiIKHlNFU1NJT05fSU5KRUNUSU9OX0tJTkRfREVMSVZFUhACKnQKEFNlc3Npb25FcnJvcktpbmQSIgoeU0VTU0lPTl9FUlJPUl9LSU5EX1VOU1BFQ0lGSUVEEAASHAoYU0VTU0lPTl9FUlJPUl9LSU5EX0VSUk9SEAESHgoaU0VTU0lPTl9FUlJPUl9LSU5EX0FCT1JURUQQAirxAQoKSXNzdWVTdGF0ZRIbChdJU1NVRV9TVEFURV9VTlNQRUNJRklFRBAAEhcKE0lTU1VFX1NUQVRFX0JBQ0tMT0cQARIUChBJU1NVRV9TVEFURV9UT0RPEAISFgoSSVNTVUVfU1RBVEVfUVVFVUVEEAMSFwoTSVNTVUVfU1RBVEVfQkxPQ0tFRBAEEhsKF0lTU1VFX1NUQVRFX0lOX1BST0dSRVNTEAUSGQoVSVNTVUVfU1RBVEVfSU5fUkVWSUVXEAYSFAoQSVNTVUVfU1RBVEVfRE9ORRAHEhgKFElTU1VFX1NUQVRFX0FSQ0hJVkVEEAgqnAEKDUZvcmdlUHJvdmlkZXISHgoaRk9SR0VfUFJPVklERVJfVU5TUEVDSUZJRUQQABIZChVGT1JHRV9QUk9WSURFUl9HSVRIVUIQARIZChVGT1JHRV9QUk9WSURFUl9HSVRMQUIQAhIaChZGT1JHRV9QUk9WSURFUl9GT1JHRUpPEAMSGQoVRk9SR0VfUFJPVklERVJfTElORUFSEAQqiQEKD0ZvcmdlTWlycm9yTW9kZRIhCh1GT1JHRV9NSVJST1JfTU9ERV9VTlNQRUNJRklFRBAAEhkKFUZPUkdFX01JUlJPUl9NT0RFX09GRhABEhwKGEZPUkdFX01JUlJPUl9NT0RFX01BTlVBTBACEhoKFkZPUkdFX01JUlJPUl9NT0RFX0FVVE8QAzKVFQoOQ29tcGFzc1NlcnZpY2USVAoNR2V0U2VydmVySW5mbxIgLmNvbXBhc3MudjEuR2V0U2VydmVySW5mb1JlcXVlc3QaIS5jb21wYXNzLnYxLkdldFNlcnZlckluZm9SZXNwb25zZRI/CgZXaG9BbUkSGS5jb21wYXNzLnYxLldob0FtSVJlcXVlc3QaGi5jb21wYXNzLnYxLldob0FtSVJlc3BvbnNlEloKD0dldFJlZkxpbmtSdWxlcxIiLmNvbXBhc3MudjEuR2V0UmVmTGlua1J1bGVzUmVxdWVzdBojLmNvbXBhc3MudjEuR2V0UmVmTGlua1J1bGVzUmVzcG9uc2USUQoMR2V0VG91clN0YXRlEh8uY29tcGFzcy52MS5HZXRUb3VyU3RhdGVSZXF1ZXN0GiAuY29tcGFzcy52MS5HZXRUb3VyU3RhdGVSZXNwb25zZRJXCg5DbGFpbVRvdXJTdGFydBIhLmNvbXBhc3MudjEuQ2xhaW1Ub3VyU3RhcnRSZXF1ZXN0GiIuY29tcGFzcy52MS5DbGFpbVRvdXJTdGFydFJlc3BvbnNlElEKDFNldFRvdXJTdGF0ZRIfLmNvbXBhc3MudjEuU2V0VG91clN0YXRlUmVxdWVzdBogLmNvbXBhc3MudjEuU2V0VG91clN0YXRlUmVzcG9uc2USXAoPU3Vic2NyaWJlRXZlbnRzEiIuY29tcGFzcy52MS5TdWJzY3JpYmVFdmVudHNSZXF1ZXN0GiMuY29tcGFzcy52MS5TdWJzY3JpYmVFdmVudHNSZXNwb25zZTABEloKD0xpc3RCb2FyZElzc3VlcxIiLmNvbXBhc3MudjEuTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBojLmNvbXBhc3MudjEuTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USUQoMU2VhcmNoSXNzdWVzEh8uY29tcGFzcy52MS5TZWFyY2hJc3N1ZXNSZXF1ZXN0GiAuY29tcGFzcy52MS5TZWFyY2hJc3N1ZXNSZXNwb25zZRJyChdQcm92aXNpb25BZ2VudFdvcmtzcGFjZRIqLmNvbXBhc3MudjEuUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0GisuY29tcGFzcy52MS5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEmAKEVN0YXJ0QWdlbnRTZXNzaW9uEiQuY29tcGFzcy52MS5TdGFydEFnZW50U2Vzc2lvblJlcXVlc3QaJS5jb21wYXNzLnYxLlN0YXJ0QWdlbnRTZXNzaW9uUmVzcG9uc2USSwoKU3Bhd25BZ2VudBIdLmNvbXBhc3MudjEuU3Bhd25BZ2VudFJlcXVlc3QaHi5jb21wYXNzLnYxLlNwYXduQWdlbnRSZXNwb25zZRJdChBTdG9wQWdlbnRTZXNzaW9uEiMuY29tcGFzcy52MS5TdG9wQWdlbnRTZXNzaW9uUmVxdWVzdBokLmNvbXBhc3MudjEuU3RvcEFnZW50U2Vzc2lvblJlc3BvbnNlEloKD1NraXBCYXRjaFdpbmRvdxIiLmNvbXBhc3MudjEuU2tpcEJhdGNoV2luZG93UmVxdWVzdBojLmNvbXBhc3MudjEuU2tpcEJhdGNoV2luZG93UmVzcG9uc2USaQoUUmVtb3ZlQWdlbnRXb3Jrc3BhY2USJy5jb21wYXNzLnYxLlJlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBooLmNvbXBhc3MudjEuUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJjChJSZWxvYWRBZ2VudFNlc3Npb24SJS5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QaJi5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlc3BvbnNlElcKDkdldEFnZW50U3RhdHVzEiEuY29tcGFzcy52MS5HZXRBZ2VudFN0YXR1c1JlcXVlc3QaIi5jb21wYXNzLnYxLkdldEFnZW50U3RhdHVzUmVzcG9uc2USYgoVU3Vic2NyaWJlQWdlbnRTZXNzaW9uEiguY29tcGFzcy52MS5TdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0Gh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25GcmFtZTABEksKCklzc3VlVG9rZW4SHS5jb21wYXNzLnYxLklzc3VlVG9rZW5SZXF1ZXN0Gh4uY29tcGFzcy52MS5Jc3N1ZVRva2VuUmVzcG9uc2USVAoNUmVuYW1lQWNjb3VudBIgLmNvbXBhc3MudjEuUmVuYW1lQWNjb3VudFJlcXVlc3QaIS5jb21wYXNzLnYxLlJlbmFtZUFjY291bnRSZXNwb25zZRJOCgtSZXZva2VUb2tlbhIeLmNvbXBhc3MudjEuUmV2b2tlVG9rZW5SZXF1ZXN0Gh8uY29tcGFzcy52MS5SZXZva2VUb2tlblJlc3BvbnNlElcKDlB1dEFnZW50Q29uZmlnEiEuY29tcGFzcy52MS5QdXRBZ2VudENvbmZpZ1JlcXVlc3QaIi5jb21wYXNzLnYxLlB1dEFnZW50Q29uZmlnUmVzcG9uc2USYwoSR2V0QWdlbnRDb25maWdJbmZvEiUuY29tcGFzcy52MS5HZXRBZ2VudENvbmZpZ0luZm9SZXF1ZXN0GiYuY29tcGFzcy52MS5HZXRBZ2VudENvbmZpZ0luZm9SZXNwb25zZRJgChFEZWxldGVBZ2VudENvbmZpZxIkLmNvbXBhc3MudjEuRGVsZXRlQWdlbnRDb25maWdSZXF1ZXN0GiUuY29tcGFzcy52MS5EZWxldGVBZ2VudENvbmZpZ1Jlc3BvbnNlEl0KEFB1dE1vZGVsUmVnaXN0cnkSIy5jb21wYXNzLnYxLlB1dE1vZGVsUmVnaXN0cnlSZXF1ZXN0GiQuY29tcGFzcy52MS5QdXRNb2RlbFJlZ2lzdHJ5UmVzcG9uc2USXQoQR2V0TW9kZWxSZWdpc3RyeRIjLmNvbXBhc3MudjEuR2V0TW9kZWxSZWdpc3RyeVJlcXVlc3QaJC5jb21wYXNzLnYxLkdldE1vZGVsUmVnaXN0cnlSZXNwb25zZRJmChNEZWxldGVNb2RlbFJlZ2lzdHJ5EiYuY29tcGFzcy52MS5EZWxldGVNb2RlbFJlZ2lzdHJ5UmVxdWVzdBonLmNvbXBhc3MudjEuRGVsZXRlTW9kZWxSZWdpc3RyeVJlc3BvbnNlEnIKF0xpc3RGb3JnZU1pcnJvclNldHRpbmdzEiouY29tcGFzcy52MS5MaXN0Rm9yZ2VNaXJyb3JTZXR0aW5nc1JlcXVlc3QaKy5jb21wYXNzLnYxLkxpc3RGb3JnZU1pcnJvclNldHRpbmdzUmVzcG9uc2USbAoVU2V0Rm9yZ2VNaXJyb3JTZXR0aW5nEiguY29tcGFzcy52MS5TZXRGb3JnZU1pcnJvclNldHRpbmdSZXF1ZXN0GikuY29tcGFzcy52MS5TZXRGb3JnZU1pcnJvclNldHRpbmdSZXNwb25zZTLfAgoOU2VjcmV0c1NlcnZpY2USSAoJU2V0U2VjcmV0EhwuY29tcGFzcy52MS5TZXRTZWNyZXRSZXF1ZXN0Gh0uY29tcGFzcy52MS5TZXRTZWNyZXRSZXNwb25zZRJOCgtMaXN0U2VjcmV0cxIeLmNvbXBhc3MudjEuTGlzdFNlY3JldHNSZXF1ZXN0Gh8uY29tcGFzcy52MS5MaXN0U2VjcmV0c1Jlc3BvbnNlElEKDERlbGV0ZVNlY3JldBIfLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVxdWVzdBogLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVzcG9uc2USYAoRTGlzdFNlcnZlclNlY3JldHMSJC5jb21wYXNzLnYxLkxpc3RTZXJ2ZXJTZWNyZXRzUmVxdWVzdBolLmNvbXBhc3MudjEuTGlzdFNlcnZlclNlY3JldHNSZXNwb25zZTLfAgoWQWdlbnRSZXBvc2l0b3J5U2VydmljZRJpChRHcmFudEFnZW50UmVwb3NpdG9yeRInLmNvbXBhc3MudjEuR3JhbnRBZ2VudFJlcG9zaXRvcnlSZXF1ZXN0GiguY29tcGFzcy52MS5HcmFudEFnZW50UmVwb3NpdG9yeVJlc3BvbnNlEmwKFVJldm9rZUFnZW50UmVwb3NpdG9yeRIoLmNvbXBhc3MudjEuUmV2b2tlQWdlbnRSZXBvc2l0b3J5UmVxdWVzdBopLmNvbXBhc3MudjEuUmV2b2tlQWdlbnRSZXBvc2l0b3J5UmVzcG9uc2USbAoVTGlzdEFnZW50UmVwb3NpdG9yaWVzEiguY29tcGFzcy52MS5MaXN0QWdlbnRSZXBvc2l0b3JpZXNSZXF1ZXN0GikuY29tcGFzcy52MS5MaXN0QWdlbnRSZXBvc2l0b3JpZXNSZXNwb25zZWIGcHJvdG8z", [file_google_protobuf_timestamp]); /** * @generated from message compass.v1.GrantAgentRepositoryRequest @@ -2607,6 +2607,126 @@ export type ForgeRef = Message<"compass.v1.ForgeRef"> & { export const ForgeRefSchema: GenMessage = /*@__PURE__*/ messageDesc(file_compass_v1_compass, 95); +/** + * The tenant-selected mirror mode for one forge repository or Linear team. + * + * @generated from message compass.v1.ForgeMirrorSetting + */ +export type ForgeMirrorSetting = Message<"compass.v1.ForgeMirrorSetting"> & { + /** + * The provider and host that identify the forge coordinate. + * + * @generated from field: compass.v1.ForgeRef forge = 1; + */ + forge?: ForgeRef | undefined; + + /** + * The repository name or Linear team key on that coordinate. + * + * @generated from field: string repo = 2; + */ + repo: string; + + /** + * The selected mirror policy for the coordinate. + * + * @generated from field: compass.v1.ForgeMirrorMode mode = 3; + */ + mode: ForgeMirrorMode; +}; + +/** + * Describes the message compass.v1.ForgeMirrorSetting. + * Use `create(ForgeMirrorSettingSchema)` to create a new message. + */ +export const ForgeMirrorSettingSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 96); + +/** + * Request to list enabled forge-repository mirror settings. + * + * @generated from message compass.v1.ListForgeMirrorSettingsRequest + */ +export type ListForgeMirrorSettingsRequest = Message<"compass.v1.ListForgeMirrorSettingsRequest"> & { +}; + +/** + * Describes the message compass.v1.ListForgeMirrorSettingsRequest. + * Use `create(ListForgeMirrorSettingsRequestSchema)` to create a new message. + */ +export const ListForgeMirrorSettingsRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 97); + +/** + * The caller's settings for enabled forge repositories. + * + * @generated from message compass.v1.ListForgeMirrorSettingsResponse + */ +export type ListForgeMirrorSettingsResponse = Message<"compass.v1.ListForgeMirrorSettingsResponse"> & { + /** + * Each enabled repository and its selected mirror mode. + * + * @generated from field: repeated compass.v1.ForgeMirrorSetting settings = 1; + */ + settings: ForgeMirrorSetting[]; + + /** + * Whether the authenticated caller can change these settings. + * + * @generated from field: bool can_edit = 2; + */ + canEdit: boolean; +}; + +/** + * Describes the message compass.v1.ListForgeMirrorSettingsResponse. + * Use `create(ListForgeMirrorSettingsResponseSchema)` to create a new message. + */ +export const ListForgeMirrorSettingsResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 98); + +/** + * Request to set a mirror mode for one enabled forge repository. + * + * @generated from message compass.v1.SetForgeMirrorSettingRequest + */ +export type SetForgeMirrorSettingRequest = Message<"compass.v1.SetForgeMirrorSettingRequest"> & { + /** + * The coordinate and mode to set. UNSPECIFIED is invalid. + * + * @generated from field: compass.v1.ForgeMirrorSetting setting = 1; + */ + setting?: ForgeMirrorSetting | undefined; +}; + +/** + * Describes the message compass.v1.SetForgeMirrorSettingRequest. + * Use `create(SetForgeMirrorSettingRequestSchema)` to create a new message. + */ +export const SetForgeMirrorSettingRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 99); + +/** + * The stored setting after a successful update. + * + * @generated from message compass.v1.SetForgeMirrorSettingResponse + */ +export type SetForgeMirrorSettingResponse = Message<"compass.v1.SetForgeMirrorSettingResponse"> & { + /** + * The coordinate and mode that were stored. + * + * @generated from field: compass.v1.ForgeMirrorSetting setting = 1; + */ + setting?: ForgeMirrorSetting | undefined; +}; + +/** + * Describes the message compass.v1.SetForgeMirrorSettingResponse. + * Use `create(SetForgeMirrorSettingResponseSchema)` to create a new message. + */ +export const SetForgeMirrorSettingResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 100); + /** * The board unit: a Compass Issue — the forge issue's fields PLUS the Compass * agent attribution PLUS the Compass machinery, translated from raw forge data @@ -2763,7 +2883,7 @@ export type Issue = Message<"compass.v1.Issue"> & { * Use `create(IssueSchema)` to create a new message. */ export const IssueSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 96); + messageDesc(file_compass_v1_compass, 101); /** * A Compass pull request: the forge PR's fields plus the Compass agent @@ -2891,7 +3011,7 @@ export type PullRequest = Message<"compass.v1.PullRequest"> & { * Use `create(PullRequestSchema)` to create a new message. */ export const PullRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 97); + messageDesc(file_compass_v1_compass, 102); /** * The rolled-up CI + status-check state on a PR head — Compass-owned, populated @@ -2923,7 +3043,7 @@ export type ChecksSummary = Message<"compass.v1.ChecksSummary"> & { * Use `create(ChecksSummarySchema)` to create a new message. */ export const ChecksSummarySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 98); + messageDesc(file_compass_v1_compass, 103); /** * @generated from message compass.v1.Check @@ -2957,7 +3077,7 @@ export type Check = Message<"compass.v1.Check"> & { * Use `create(CheckSchema)` to create a new message. */ export const CheckSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 99); + messageDesc(file_compass_v1_compass, 104); /** * A PR diffstat (files/additions/deletions), carried on PullRequest — a @@ -2987,7 +3107,7 @@ export type ChangedStats = Message<"compass.v1.ChangedStats"> & { * Use `create(ChangedStatsSchema)` to create a new message. */ export const ChangedStatsSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 100); + messageDesc(file_compass_v1_compass, 105); /** * The linked tracker issue — the projection target (DL-032); the @@ -3042,7 +3162,7 @@ export type TrackerRef = Message<"compass.v1.TrackerRef"> & { * Use `create(TrackerRefSchema)` to create a new message. */ export const TrackerRefSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 101); + messageDesc(file_compass_v1_compass, 106); /** * The full review state the right-sidebar PR pane shows — every submitted review @@ -3096,7 +3216,7 @@ export type Review = Message<"compass.v1.Review"> & { * Use `create(ReviewSchema)` to create a new message. */ export const ReviewSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 102); + messageDesc(file_compass_v1_compass, 107); /** * @generated from message compass.v1.ReviewThread @@ -3125,7 +3245,7 @@ export type ReviewThread = Message<"compass.v1.ReviewThread"> & { * Use `create(ReviewThreadSchema)` to create a new message. */ export const ReviewThreadSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 103); + messageDesc(file_compass_v1_compass, 108); /** * @generated from message compass.v1.Comment @@ -3152,7 +3272,7 @@ export type Comment = Message<"compass.v1.Comment"> & { * Use `create(CommentSchema)` to create a new message. */ export const CommentSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 104); + messageDesc(file_compass_v1_compass, 109); /** * The observed state of a pull request in its merge queue. @@ -3187,7 +3307,7 @@ export type MergeQueueEntry = Message<"compass.v1.MergeQueueEntry"> & { * Use `create(MergeQueueEntrySchema)` to create a new message. */ export const MergeQueueEntrySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 105); + messageDesc(file_compass_v1_compass, 110); /** * How a resolved secret is delivered into the agent container. Mirrors @@ -3724,6 +3844,47 @@ export enum ForgeProvider { export const ForgeProviderSchema: GenEnum = /*@__PURE__*/ enumDesc(file_compass_v1_compass, 13); +/** + * Whether asks naming a forge coordinate may be mirrored by an agent or user. + * + * @generated from enum compass.v1.ForgeMirrorMode + */ +export enum ForgeMirrorMode { + /** + * The mode is omitted or invalid. + * + * @generated from enum value: FORGE_MIRROR_MODE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * Mirroring is disabled. No settings row is stored for this mode. + * + * @generated from enum value: FORGE_MIRROR_MODE_OFF = 1; + */ + OFF = 1, + + /** + * A user may click to mirror; agents may not mirror automatically. + * + * @generated from enum value: FORGE_MIRROR_MODE_MANUAL = 2; + */ + MANUAL = 2, + + /** + * A user may click and agents may mirror asks automatically. + * + * @generated from enum value: FORGE_MIRROR_MODE_AUTO = 3; + */ + AUTO = 3, +} + +/** + * Describes the enum compass.v1.ForgeMirrorMode. + */ +export const ForgeMirrorModeSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_compass_v1_compass, 14); + /** * The Compass server service. * @@ -4105,6 +4266,28 @@ export const CompassService: GenService<{ input: typeof DeleteModelRegistryRequestSchema; output: typeof DeleteModelRegistryResponseSchema; }, + /** + * List the tenant's enabled forge-repository mirror settings. Any authenticated + * account may read them; can_edit reports whether the caller may change them. + * + * @generated from rpc compass.v1.CompassService.ListForgeMirrorSettings + */ + listForgeMirrorSettings: { + methodKind: "unary"; + input: typeof ListForgeMirrorSettingsRequestSchema; + output: typeof ListForgeMirrorSettingsResponseSchema; + }, + /** + * Set one tenant's mirror mode for an enabled forge repository. Requires a + * tenant admin; OFF removes the setting row. + * + * @generated from rpc compass.v1.CompassService.SetForgeMirrorSetting + */ + setForgeMirrorSetting: { + methodKind: "unary"; + input: typeof SetForgeMirrorSettingRequestSchema; + output: typeof SetForgeMirrorSettingResponseSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_compass_v1_compass, 0); diff --git a/proto/compass/v1/comms.proto b/proto/compass/v1/comms.proto index 2795da8a7..53510df27 100644 --- a/proto/compass/v1/comms.proto +++ b/proto/compass/v1/comms.proto @@ -111,6 +111,9 @@ service CommsService { // Answer a pending structured ask (D5). Authorized: the caller must be a // member of the channel the ask belongs to. rpc RespondToAsk(RespondToAskRequest) returns (RespondToAskResponse); + // Mirror a visible ask, topic, or message set to an allowed forge ref on the + // calling user's behalf. + rpc MirrorToForge(MirrorToForgeRequest) returns (MirrorToForgeResponse); // Full-text search across the messages the caller may see — the audit/search // property served from the store of record (D1, D12). Results are scoped to @@ -860,6 +863,42 @@ message BoardAsk { bool answered = 6; // When the ask was posted, in Unix milliseconds. int64 posted_at_unix_ms = 7; + // Set by the board read to report this ask's current forge delivery state. + BoardMirrorState mirror_state = 8; + // Set by the board read when the forge comment was delivered. + string mirror_url = 9; +} + +// Delivery state of an ask mirror outbox row. +enum BoardMirrorState { + // No known mirror state was supplied. + BOARD_MIRROR_STATE_UNSPECIFIED = 0; + // The comment is waiting for delivery or retry. + BOARD_MIRROR_STATE_PENDING = 1; + // The forge comment was delivered. + BOARD_MIRROR_STATE_DONE = 2; + // Delivery was refused or exhausted. + BOARD_MIRROR_STATE_FAILED = 3; +} + +// Request to mirror a visible ask, topic, or message set to an allowed forge ref on the calling user's behalf. +message MirrorToForgeRequest { + // The issue or pull request destination. + string ref = 1; + // The ask to mirror; mutually exclusive with the other source fields. + string ask_id = 2; + // The topic to mirror; mutually exclusive with the other source fields. + string topic_id = 3; + // One to fifty message IDs from channel_id, mutually exclusive with other sources. + repeated string message_ids = 4; + // The visible channel containing the chosen source. + string channel_id = 5; +} + +// The forge comment created or updated for the requested source. +message MirrorToForgeResponse { + // The URL of the resulting forge comment. + string comment_url = 1; } // The current board state for one agent. diff --git a/proto/compass/v1/compass.proto b/proto/compass/v1/compass.proto index 63c91877d..4f52b4d59 100644 --- a/proto/compass/v1/compass.proto +++ b/proto/compass/v1/compass.proto @@ -197,6 +197,14 @@ service CompassService { // like PutModelRegistry. Fails closed if the registry being cleared holds any // stable name still referenced by a published profile's models.* map. rpc DeleteModelRegistry(DeleteModelRegistryRequest) returns (DeleteModelRegistryResponse); + + // List the tenant's enabled forge-repository mirror settings. Any authenticated + // account may read them; can_edit reports whether the caller may change them. + rpc ListForgeMirrorSettings(ListForgeMirrorSettingsRequest) returns (ListForgeMirrorSettingsResponse); + + // Set one tenant's mirror mode for an enabled forge repository. Requires a + // tenant admin; OFF removes the setting row. + rpc SetForgeMirrorSetting(SetForgeMirrorSettingRequest) returns (SetForgeMirrorSettingResponse); } // How a resolved secret is delivered into the agent container. Mirrors @@ -1163,6 +1171,51 @@ message ForgeRef { // it is the constant service host, "linear.app" } +// Whether asks naming a forge coordinate may be mirrored by an agent or user. +enum ForgeMirrorMode { + // The mode is omitted or invalid. + FORGE_MIRROR_MODE_UNSPECIFIED = 0; + // Mirroring is disabled. No settings row is stored for this mode. + FORGE_MIRROR_MODE_OFF = 1; + // A user may click to mirror; agents may not mirror automatically. + FORGE_MIRROR_MODE_MANUAL = 2; + // A user may click and agents may mirror asks automatically. + FORGE_MIRROR_MODE_AUTO = 3; +} + +// The tenant-selected mirror mode for one forge repository or Linear team. +message ForgeMirrorSetting { + // The provider and host that identify the forge coordinate. + ForgeRef forge = 1; + // The repository name or Linear team key on that coordinate. + string repo = 2; + // The selected mirror policy for the coordinate. + ForgeMirrorMode mode = 3; +} + +// Request to list enabled forge-repository mirror settings. +message ListForgeMirrorSettingsRequest {} + +// The caller's settings for enabled forge repositories. +message ListForgeMirrorSettingsResponse { + // Each enabled repository and its selected mirror mode. + repeated ForgeMirrorSetting settings = 1; + // Whether the authenticated caller can change these settings. + bool can_edit = 2; +} + +// Request to set a mirror mode for one enabled forge repository. +message SetForgeMirrorSettingRequest { + // The coordinate and mode to set. UNSPECIFIED is invalid. + ForgeMirrorSetting setting = 1; +} + +// The stored setting after a successful update. +message SetForgeMirrorSettingResponse { + // The coordinate and mode that were stored. + ForgeMirrorSetting setting = 1; +} + // The board unit: a Compass Issue — the forge issue's fields PLUS the Compass // agent attribution PLUS the Compass machinery, translated from raw forge data // at server ingestion.