From d0cb457016faaf92648e6ab59e5451f32bd1bff3 Mon Sep 17 00:00:00 2001 From: mintaka Date: Sun, 11 Oct 2026 18:40:12 -0400 Subject: [PATCH 1/3] feat(server): add admin-only RenameAccount RPC (RIG-5151) Handle rename had no write path: the only rename was a raw UPDATE in tests. RenameAccount gives the admin an in-place rename for users and agents, following DL-271: rename in place at both tiers, and a freed handle may be reclaimed at once. Store.RenameHandle updates account_handles (the resolution key) and accounts.handle (the display copy) in one tenant transaction. A unique violation maps to ErrConflict, which the handler returns as AlreadyExists. The handler resolves the account as IssueToken does, refuses the system account, and returns an agent's address owner-qualified. The AdminGate classifies the RPC adminOnly. The product spec gains the requirement and its scenarios. User self-rename is out of scope and stays with its own design. Ledger-impact: none Co-authored-by: Matt Wilkinson --- docs/specs/product/compass.md | 47 +- go/gen/compass/v1/compass.pb.go | 510 +++++++++++------- .../v1/compassv1connect/compass.connect.go | 35 ++ go/internal/auth/admin_gate.go | 5 +- go/internal/auth/admin_gate_test.go | 2 + .../store/account_handles_pgtest_test.go | 158 +++++- go/internal/store/accounts.go | 59 +- go/internal/store/db/accounts.sql.go | 38 ++ go/internal/store/db/querier.go | 2 + go/internal/store/queries/accounts.sql | 10 + go/server/network_door_test.go | 87 +++ go/server/service.go | 56 ++ .../src/gen/compass/v1/compass_pb.ts | 129 +++-- .../src/gen/compass/v1/compass_pb.ts | 129 +++-- proto/compass/v1/compass.proto | 22 + 15 files changed, 1000 insertions(+), 289 deletions(-) diff --git a/docs/specs/product/compass.md b/docs/specs/product/compass.md index 4b8ad83fb..9f286fb16 100644 --- a/docs/specs/product/compass.md +++ b/docs/specs/product/compass.md @@ -61,6 +61,8 @@ accounts, channels, messages, and their event stream), described in turn below. account (the sole public-contract path to a non-bootstrap account's credential). Its admin-gated enforcement and token semantics are served by the network door (see [The network door](#the-network-door)). +- **`RenameAccount`** — an admin-only RPC that renames a user or agent in place. + Agents keep their owner; a freed handle may be reclaimed. The event payload is an extensible `oneof` — `ServerStatus` and `ResyncRequired` plus the agent-session payloads (`AgentSessionStatus`, `AgentMessageChunk`, @@ -101,6 +103,39 @@ the only path by which the server pushes state to a UI. - **Then** the server reports the same Runner id with a higher per-server enrollment sequence. +### Requirement: An admin renames a user or agent account in place + +`RenameAccount` SHALL resolve a bare user handle or an owner-qualified agent +handle without viewer-scoped filtering. It SHALL rename the account's resolution +handle and display copy in one store transaction. The new handle MUST be +valid and bare; an agent's owner does not change. Unknown accounts return +`NotFound`, invalid input returns `InvalidArgument`, and a collision in the +account's handle tier returns `AlreadyExists`. The system account cannot be +renamed. The response returns the account id and its current address, qualified +by the owner's current handle for agents. A freed handle is immediately +available for reuse. + +#### Scenario: Rename an account at either handle tier + +- **Given** an existing user or an agent addressed as `owner/agent` +- **When** an admin renames it to a valid bare handle +- **Then** both handle records change together, the old handle becomes + available, and the response returns the new address + +#### Scenario: Reject a rename that cannot be applied + +- **Given** an admin rename request with an unknown account, invalid new handle, + or a handle already taken in that tier +- **When** the server handles it +- **Then** it returns `NotFound`, `InvalidArgument`, or `AlreadyExists` as + appropriate and leaves both stored handles unchanged + +#### Scenario: The admin cannot rename the system account + +- **Given** the system account +- **When** an admin requests a rename +- **Then** the server returns `PermissionDenied` and retains its handle + ### Requirement: A generated client is the only door to the server Client code SHALL reach the server only through the generated `compass.v1` @@ -1022,8 +1057,8 @@ webview or a native client). ### Requirement: The dev endpoint denies admin-only RPCs The dev endpoint mounts no bearer interceptor, so it never carries an -authenticated caller. Its admin-only RPCs (`IssueToken` and the agent-session -lifecycle RPCs) SHALL therefore fail closed `PermissionDenied`, while the +authenticated caller. Its admin-only RPCs (`IssueToken`, `RenameAccount`, and the +agent-session lifecycle RPCs) SHALL therefore fail closed `PermissionDenied`, while the authenticated-open RPCs (`GetServerInfo`, `SubscribeEvents`) and the ambient-admin comms RPCs remain reachable. This prevents a browser page loaded against a configured dev endpoint from minting a bootstrap-admin token via @@ -1067,9 +1102,9 @@ TLS 1.3. The server SHALL authenticate every network-door RPC with a bearer token: a request with no token or an unresolvable token SHALL be rejected `Unauthenticated`, on both unary and streaming RPCs. The admin-only RPCs -(`IssueToken` and the agent-session lifecycle RPCs) SHALL additionally require -the caller to resolve to the admin account, rejecting a non-admin -`PermissionDenied`; the authenticated-open RPCs (`GetServerInfo`, +(`IssueToken`, `RenameAccount`, and the agent-session lifecycle RPCs) SHALL +additionally require the caller to resolve to the admin account, rejecting a +non-admin `PermissionDenied`; the authenticated-open RPCs (`GetServerInfo`, `SubscribeEvents`) SHALL admit any valid bearer. #### Scenario: A token-less call on the network door is rejected @@ -1081,7 +1116,7 @@ the caller to resolve to the admin account, rejecting a non-admin #### Scenario: A non-admin token calls an admin-only RPC - **Given** a valid non-admin bearer token -- **When** the client calls `IssueToken` over the network door +- **When** the client calls `IssueToken` or `RenameAccount` over the network door - **Then** the server rejects it `PermissionDenied`, while the same token on `GetServerInfo` succeeds. diff --git a/go/gen/compass/v1/compass.pb.go b/go/gen/compass/v1/compass.pb.go index 06a886bdf..2ed25bb63 100644 --- a/go/gen/compass/v1/compass.pb.go +++ b/go/gen/compass/v1/compass.pb.go @@ -4647,6 +4647,117 @@ func (x *IssueTokenResponse) GetToken() string { return "" } +// RenameAccount: the admin-only path to rename a user or agent account. +type RenameAccountRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Current bare user handle or `owner/agent` handle. Empty is INVALID_ARGUMENT; + // bad qualified grammar is INVALID_ARGUMENT, unknown handles are NOT_FOUND, and + // the system account is PERMISSION_DENIED. + AccountHandle string `protobuf:"bytes,1,opt,name=account_handle,json=accountHandle,proto3" json:"account_handle,omitempty"` + // New bare handle; an agent keeps its current owner. Empty, bad grammar, or a + // qualified handle is INVALID_ARGUMENT; a taken handle is ALREADY_EXISTS. + NewHandle string `protobuf:"bytes,2,opt,name=new_handle,json=newHandle,proto3" json:"new_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RenameAccountRequest) Reset() { + *x = RenameAccountRequest{} + mi := &file_compass_v1_compass_proto_msgTypes[65] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RenameAccountRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenameAccountRequest) ProtoMessage() {} + +func (x *RenameAccountRequest) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_compass_proto_msgTypes[65] + 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 RenameAccountRequest.ProtoReflect.Descriptor instead. +func (*RenameAccountRequest) Descriptor() ([]byte, []int) { + return file_compass_v1_compass_proto_rawDescGZIP(), []int{65} +} + +func (x *RenameAccountRequest) GetAccountHandle() string { + if x != nil { + return x.AccountHandle + } + return "" +} + +func (x *RenameAccountRequest) GetNewHandle() string { + if x != nil { + return x.NewHandle + } + return "" +} + +type RenameAccountResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + AccountId string `protobuf:"bytes,1,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"` + // The new address: bare for users, `owner/agent` for agents. + Handle string `protobuf:"bytes,2,opt,name=handle,proto3" json:"handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RenameAccountResponse) Reset() { + *x = RenameAccountResponse{} + mi := &file_compass_v1_compass_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RenameAccountResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenameAccountResponse) ProtoMessage() {} + +func (x *RenameAccountResponse) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_compass_proto_msgTypes[66] + 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 RenameAccountResponse.ProtoReflect.Descriptor instead. +func (*RenameAccountResponse) Descriptor() ([]byte, []int) { + return file_compass_v1_compass_proto_rawDescGZIP(), []int{66} +} + +func (x *RenameAccountResponse) GetAccountId() string { + if x != nil { + return x.AccountId + } + return "" +} + +func (x *RenameAccountResponse) GetHandle() string { + if x != nil { + return x.Handle + } + return "" +} + // RevokeToken: the admin-only path to withdraw a bearer token by its value. type RevokeTokenRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -4661,7 +4772,7 @@ type RevokeTokenRequest struct { func (x *RevokeTokenRequest) Reset() { *x = RevokeTokenRequest{} - mi := &file_compass_v1_compass_proto_msgTypes[65] + mi := &file_compass_v1_compass_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4673,7 +4784,7 @@ func (x *RevokeTokenRequest) String() string { func (*RevokeTokenRequest) ProtoMessage() {} func (x *RevokeTokenRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[65] + mi := &file_compass_v1_compass_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4686,7 +4797,7 @@ func (x *RevokeTokenRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeTokenRequest.ProtoReflect.Descriptor instead. func (*RevokeTokenRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{65} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{67} } func (x *RevokeTokenRequest) GetToken() string { @@ -4704,7 +4815,7 @@ type RevokeTokenResponse struct { func (x *RevokeTokenResponse) Reset() { *x = RevokeTokenResponse{} - mi := &file_compass_v1_compass_proto_msgTypes[66] + mi := &file_compass_v1_compass_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4716,7 +4827,7 @@ func (x *RevokeTokenResponse) String() string { func (*RevokeTokenResponse) ProtoMessage() {} func (x *RevokeTokenResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[66] + mi := &file_compass_v1_compass_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4729,7 +4840,7 @@ func (x *RevokeTokenResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokeTokenResponse.ProtoReflect.Descriptor instead. func (*RevokeTokenResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{66} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{68} } // PutAgentConfig: declare the fleet config bundle. The caller's identity is the @@ -4745,7 +4856,7 @@ type PutAgentConfigRequest struct { func (x *PutAgentConfigRequest) Reset() { *x = PutAgentConfigRequest{} - mi := &file_compass_v1_compass_proto_msgTypes[67] + mi := &file_compass_v1_compass_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4757,7 +4868,7 @@ func (x *PutAgentConfigRequest) String() string { func (*PutAgentConfigRequest) ProtoMessage() {} func (x *PutAgentConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[67] + mi := &file_compass_v1_compass_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4770,7 +4881,7 @@ func (x *PutAgentConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PutAgentConfigRequest.ProtoReflect.Descriptor instead. func (*PutAgentConfigRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{67} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{69} } func (x *PutAgentConfigRequest) GetBundle() []byte { @@ -4792,7 +4903,7 @@ type PutAgentConfigResponse struct { func (x *PutAgentConfigResponse) Reset() { *x = PutAgentConfigResponse{} - mi := &file_compass_v1_compass_proto_msgTypes[68] + mi := &file_compass_v1_compass_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4804,7 +4915,7 @@ func (x *PutAgentConfigResponse) String() string { func (*PutAgentConfigResponse) ProtoMessage() {} func (x *PutAgentConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[68] + mi := &file_compass_v1_compass_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4817,7 +4928,7 @@ func (x *PutAgentConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PutAgentConfigResponse.ProtoReflect.Descriptor instead. func (*PutAgentConfigResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{68} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{70} } func (x *PutAgentConfigResponse) GetVersion() string { @@ -4835,7 +4946,7 @@ type GetAgentConfigInfoRequest struct { func (x *GetAgentConfigInfoRequest) Reset() { *x = GetAgentConfigInfoRequest{} - mi := &file_compass_v1_compass_proto_msgTypes[69] + mi := &file_compass_v1_compass_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4847,7 +4958,7 @@ func (x *GetAgentConfigInfoRequest) String() string { func (*GetAgentConfigInfoRequest) ProtoMessage() {} func (x *GetAgentConfigInfoRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[69] + mi := &file_compass_v1_compass_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4860,7 +4971,7 @@ func (x *GetAgentConfigInfoRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetAgentConfigInfoRequest.ProtoReflect.Descriptor instead. func (*GetAgentConfigInfoRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{69} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{71} } // GetAgentConfigInfo: the current bundle's version and member names by top dir — @@ -4895,7 +5006,7 @@ type GetAgentConfigInfoResponse struct { func (x *GetAgentConfigInfoResponse) Reset() { *x = GetAgentConfigInfoResponse{} - mi := &file_compass_v1_compass_proto_msgTypes[70] + mi := &file_compass_v1_compass_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4907,7 +5018,7 @@ func (x *GetAgentConfigInfoResponse) String() string { func (*GetAgentConfigInfoResponse) ProtoMessage() {} func (x *GetAgentConfigInfoResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[70] + mi := &file_compass_v1_compass_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4920,7 +5031,7 @@ func (x *GetAgentConfigInfoResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetAgentConfigInfoResponse.ProtoReflect.Descriptor instead. func (*GetAgentConfigInfoResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{70} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{72} } func (x *GetAgentConfigInfoResponse) GetVersion() string { @@ -5001,7 +5112,7 @@ type DeleteAgentConfigRequest struct { func (x *DeleteAgentConfigRequest) Reset() { *x = DeleteAgentConfigRequest{} - mi := &file_compass_v1_compass_proto_msgTypes[71] + mi := &file_compass_v1_compass_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5013,7 +5124,7 @@ func (x *DeleteAgentConfigRequest) String() string { func (*DeleteAgentConfigRequest) ProtoMessage() {} func (x *DeleteAgentConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[71] + mi := &file_compass_v1_compass_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5026,7 +5137,7 @@ func (x *DeleteAgentConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteAgentConfigRequest.ProtoReflect.Descriptor instead. func (*DeleteAgentConfigRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{71} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{73} } type DeleteAgentConfigResponse struct { @@ -5037,7 +5148,7 @@ type DeleteAgentConfigResponse struct { func (x *DeleteAgentConfigResponse) Reset() { *x = DeleteAgentConfigResponse{} - mi := &file_compass_v1_compass_proto_msgTypes[72] + mi := &file_compass_v1_compass_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5049,7 +5160,7 @@ func (x *DeleteAgentConfigResponse) String() string { func (*DeleteAgentConfigResponse) ProtoMessage() {} func (x *DeleteAgentConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[72] + mi := &file_compass_v1_compass_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5062,7 +5173,7 @@ func (x *DeleteAgentConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteAgentConfigResponse.ProtoReflect.Descriptor instead. func (*DeleteAgentConfigResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{72} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{74} } // One candidate in a stable name's ordered chain: an upstream (provider, @@ -5080,7 +5191,7 @@ type ModelCandidate struct { func (x *ModelCandidate) Reset() { *x = ModelCandidate{} - mi := &file_compass_v1_compass_proto_msgTypes[73] + mi := &file_compass_v1_compass_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5092,7 +5203,7 @@ func (x *ModelCandidate) String() string { func (*ModelCandidate) ProtoMessage() {} func (x *ModelCandidate) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[73] + mi := &file_compass_v1_compass_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5105,7 +5216,7 @@ func (x *ModelCandidate) ProtoReflect() protoreflect.Message { // Deprecated: Use ModelCandidate.ProtoReflect.Descriptor instead. func (*ModelCandidate) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{73} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{75} } func (x *ModelCandidate) GetProvider() string { @@ -5142,7 +5253,7 @@ type ModelMetadata struct { func (x *ModelMetadata) Reset() { *x = ModelMetadata{} - mi := &file_compass_v1_compass_proto_msgTypes[74] + mi := &file_compass_v1_compass_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5154,7 +5265,7 @@ func (x *ModelMetadata) String() string { func (*ModelMetadata) ProtoMessage() {} func (x *ModelMetadata) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[74] + mi := &file_compass_v1_compass_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5167,7 +5278,7 @@ func (x *ModelMetadata) ProtoReflect() protoreflect.Message { // Deprecated: Use ModelMetadata.ProtoReflect.Descriptor instead. func (*ModelMetadata) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{74} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{76} } func (x *ModelMetadata) GetContextWindow() int64 { @@ -5211,7 +5322,7 @@ type ModelRegistryEntry struct { func (x *ModelRegistryEntry) Reset() { *x = ModelRegistryEntry{} - mi := &file_compass_v1_compass_proto_msgTypes[75] + mi := &file_compass_v1_compass_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5223,7 +5334,7 @@ func (x *ModelRegistryEntry) String() string { func (*ModelRegistryEntry) ProtoMessage() {} func (x *ModelRegistryEntry) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[75] + mi := &file_compass_v1_compass_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5236,7 +5347,7 @@ func (x *ModelRegistryEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use ModelRegistryEntry.ProtoReflect.Descriptor instead. func (*ModelRegistryEntry) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{75} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{77} } func (x *ModelRegistryEntry) GetDisplayName() string { @@ -5271,7 +5382,7 @@ type ModelRegistry struct { func (x *ModelRegistry) Reset() { *x = ModelRegistry{} - mi := &file_compass_v1_compass_proto_msgTypes[76] + mi := &file_compass_v1_compass_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5283,7 +5394,7 @@ func (x *ModelRegistry) String() string { func (*ModelRegistry) ProtoMessage() {} func (x *ModelRegistry) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[76] + mi := &file_compass_v1_compass_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5296,7 +5407,7 @@ func (x *ModelRegistry) ProtoReflect() protoreflect.Message { // Deprecated: Use ModelRegistry.ProtoReflect.Descriptor instead. func (*ModelRegistry) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{76} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{78} } func (x *ModelRegistry) GetEntries() map[string]*ModelRegistryEntry { @@ -5322,7 +5433,7 @@ type PutModelRegistryRequest struct { func (x *PutModelRegistryRequest) Reset() { *x = PutModelRegistryRequest{} - mi := &file_compass_v1_compass_proto_msgTypes[77] + mi := &file_compass_v1_compass_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5334,7 +5445,7 @@ func (x *PutModelRegistryRequest) String() string { func (*PutModelRegistryRequest) ProtoMessage() {} func (x *PutModelRegistryRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[77] + mi := &file_compass_v1_compass_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5347,7 +5458,7 @@ func (x *PutModelRegistryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PutModelRegistryRequest.ProtoReflect.Descriptor instead. func (*PutModelRegistryRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{77} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{79} } func (x *PutModelRegistryRequest) GetRegistry() *ModelRegistry { @@ -5374,7 +5485,7 @@ type PutModelRegistryResponse struct { func (x *PutModelRegistryResponse) Reset() { *x = PutModelRegistryResponse{} - mi := &file_compass_v1_compass_proto_msgTypes[78] + mi := &file_compass_v1_compass_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5386,7 +5497,7 @@ func (x *PutModelRegistryResponse) String() string { func (*PutModelRegistryResponse) ProtoMessage() {} func (x *PutModelRegistryResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[78] + mi := &file_compass_v1_compass_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5399,7 +5510,7 @@ func (x *PutModelRegistryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PutModelRegistryResponse.ProtoReflect.Descriptor instead. func (*PutModelRegistryResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{78} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{80} } func (x *PutModelRegistryResponse) GetVersion() int64 { @@ -5417,7 +5528,7 @@ type GetModelRegistryRequest struct { func (x *GetModelRegistryRequest) Reset() { *x = GetModelRegistryRequest{} - mi := &file_compass_v1_compass_proto_msgTypes[79] + mi := &file_compass_v1_compass_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5429,7 +5540,7 @@ func (x *GetModelRegistryRequest) String() string { func (*GetModelRegistryRequest) ProtoMessage() {} func (x *GetModelRegistryRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[79] + mi := &file_compass_v1_compass_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5442,7 +5553,7 @@ func (x *GetModelRegistryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetModelRegistryRequest.ProtoReflect.Descriptor instead. func (*GetModelRegistryRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{79} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{81} } // GetModelRegistry: the current registry version and payload. An unconfigured @@ -5457,7 +5568,7 @@ type GetModelRegistryResponse struct { func (x *GetModelRegistryResponse) Reset() { *x = GetModelRegistryResponse{} - mi := &file_compass_v1_compass_proto_msgTypes[80] + mi := &file_compass_v1_compass_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5469,7 +5580,7 @@ func (x *GetModelRegistryResponse) String() string { func (*GetModelRegistryResponse) ProtoMessage() {} func (x *GetModelRegistryResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[80] + mi := &file_compass_v1_compass_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5482,7 +5593,7 @@ func (x *GetModelRegistryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetModelRegistryResponse.ProtoReflect.Descriptor instead. func (*GetModelRegistryResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{80} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{82} } func (x *GetModelRegistryResponse) GetVersion() int64 { @@ -5507,7 +5618,7 @@ type DeleteModelRegistryRequest struct { func (x *DeleteModelRegistryRequest) Reset() { *x = DeleteModelRegistryRequest{} - mi := &file_compass_v1_compass_proto_msgTypes[81] + mi := &file_compass_v1_compass_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5519,7 +5630,7 @@ func (x *DeleteModelRegistryRequest) String() string { func (*DeleteModelRegistryRequest) ProtoMessage() {} func (x *DeleteModelRegistryRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[81] + mi := &file_compass_v1_compass_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5532,7 +5643,7 @@ func (x *DeleteModelRegistryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteModelRegistryRequest.ProtoReflect.Descriptor instead. func (*DeleteModelRegistryRequest) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{81} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{83} } type DeleteModelRegistryResponse struct { @@ -5543,7 +5654,7 @@ type DeleteModelRegistryResponse struct { func (x *DeleteModelRegistryResponse) Reset() { *x = DeleteModelRegistryResponse{} - mi := &file_compass_v1_compass_proto_msgTypes[82] + mi := &file_compass_v1_compass_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5555,7 +5666,7 @@ func (x *DeleteModelRegistryResponse) String() string { func (*DeleteModelRegistryResponse) ProtoMessage() {} func (x *DeleteModelRegistryResponse) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[82] + mi := &file_compass_v1_compass_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5568,7 +5679,7 @@ func (x *DeleteModelRegistryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteModelRegistryResponse.ProtoReflect.Descriptor instead. func (*DeleteModelRegistryResponse) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{82} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{84} } // The Compass agent attribution parsed from the owner header at ingestion — a @@ -5587,7 +5698,7 @@ type AgentAttribution struct { func (x *AgentAttribution) Reset() { *x = AgentAttribution{} - mi := &file_compass_v1_compass_proto_msgTypes[83] + mi := &file_compass_v1_compass_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5599,7 +5710,7 @@ func (x *AgentAttribution) String() string { func (*AgentAttribution) ProtoMessage() {} func (x *AgentAttribution) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[83] + mi := &file_compass_v1_compass_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5612,7 +5723,7 @@ func (x *AgentAttribution) ProtoReflect() protoreflect.Message { // Deprecated: Use AgentAttribution.ProtoReflect.Descriptor instead. func (*AgentAttribution) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{83} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{85} } func (x *AgentAttribution) GetAgentHandle() string { @@ -5639,7 +5750,7 @@ type ForgeRef struct { func (x *ForgeRef) Reset() { *x = ForgeRef{} - mi := &file_compass_v1_compass_proto_msgTypes[84] + mi := &file_compass_v1_compass_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5651,7 +5762,7 @@ func (x *ForgeRef) String() string { func (*ForgeRef) ProtoMessage() {} func (x *ForgeRef) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[84] + mi := &file_compass_v1_compass_proto_msgTypes[86] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5664,7 +5775,7 @@ func (x *ForgeRef) ProtoReflect() protoreflect.Message { // Deprecated: Use ForgeRef.ProtoReflect.Descriptor instead. func (*ForgeRef) Descriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{84} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{86} } func (x *ForgeRef) GetProvider() ForgeProvider { @@ -5724,7 +5835,7 @@ type Issue struct { func (x *Issue) Reset() { *x = Issue{} - mi := &file_compass_v1_compass_proto_msgTypes[85] + mi := &file_compass_v1_compass_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5736,7 +5847,7 @@ func (x *Issue) String() string { func (*Issue) ProtoMessage() {} func (x *Issue) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[85] + mi := &file_compass_v1_compass_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5749,7 +5860,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{85} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{87} } func (x *Issue) GetId() string { @@ -5917,7 +6028,7 @@ type PullRequest struct { func (x *PullRequest) Reset() { *x = PullRequest{} - mi := &file_compass_v1_compass_proto_msgTypes[86] + mi := &file_compass_v1_compass_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5929,7 +6040,7 @@ func (x *PullRequest) String() string { func (*PullRequest) ProtoMessage() {} func (x *PullRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[86] + mi := &file_compass_v1_compass_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5942,7 +6053,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{86} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{88} } func (x *PullRequest) GetForge() *ForgeRef { @@ -6084,7 +6195,7 @@ type ChecksSummary struct { func (x *ChecksSummary) Reset() { *x = ChecksSummary{} - mi := &file_compass_v1_compass_proto_msgTypes[87] + mi := &file_compass_v1_compass_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6096,7 +6207,7 @@ func (x *ChecksSummary) String() string { func (*ChecksSummary) ProtoMessage() {} func (x *ChecksSummary) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[87] + mi := &file_compass_v1_compass_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6109,7 +6220,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{87} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{89} } func (x *ChecksSummary) GetHeadSha() string { @@ -6145,7 +6256,7 @@ type Check struct { func (x *Check) Reset() { *x = Check{} - mi := &file_compass_v1_compass_proto_msgTypes[88] + mi := &file_compass_v1_compass_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6157,7 +6268,7 @@ func (x *Check) String() string { func (*Check) ProtoMessage() {} func (x *Check) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[88] + mi := &file_compass_v1_compass_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6170,7 +6281,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{88} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{90} } func (x *Check) GetName() string { @@ -6214,7 +6325,7 @@ type ChangedStats struct { func (x *ChangedStats) Reset() { *x = ChangedStats{} - mi := &file_compass_v1_compass_proto_msgTypes[89] + mi := &file_compass_v1_compass_proto_msgTypes[91] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6226,7 +6337,7 @@ func (x *ChangedStats) String() string { func (*ChangedStats) ProtoMessage() {} func (x *ChangedStats) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[89] + mi := &file_compass_v1_compass_proto_msgTypes[91] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6239,7 +6350,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{89} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{91} } func (x *ChangedStats) GetFiles() uint32 { @@ -6279,7 +6390,7 @@ type TrackerRef struct { func (x *TrackerRef) Reset() { *x = TrackerRef{} - mi := &file_compass_v1_compass_proto_msgTypes[90] + mi := &file_compass_v1_compass_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6291,7 +6402,7 @@ func (x *TrackerRef) String() string { func (*TrackerRef) ProtoMessage() {} func (x *TrackerRef) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[90] + mi := &file_compass_v1_compass_proto_msgTypes[92] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6304,7 +6415,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{90} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{92} } func (x *TrackerRef) GetKind() string { @@ -6369,7 +6480,7 @@ type Review struct { func (x *Review) Reset() { *x = Review{} - mi := &file_compass_v1_compass_proto_msgTypes[91] + mi := &file_compass_v1_compass_proto_msgTypes[93] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6381,7 +6492,7 @@ func (x *Review) String() string { func (*Review) ProtoMessage() {} func (x *Review) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[91] + mi := &file_compass_v1_compass_proto_msgTypes[93] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6394,7 +6505,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{91} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{93} } func (x *Review) GetAuthor() string { @@ -6443,7 +6554,7 @@ type ReviewThread struct { func (x *ReviewThread) Reset() { *x = ReviewThread{} - mi := &file_compass_v1_compass_proto_msgTypes[92] + mi := &file_compass_v1_compass_proto_msgTypes[94] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6455,7 +6566,7 @@ func (x *ReviewThread) String() string { func (*ReviewThread) ProtoMessage() {} func (x *ReviewThread) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[92] + mi := &file_compass_v1_compass_proto_msgTypes[94] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6468,7 +6579,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{92} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{94} } func (x *ReviewThread) GetPath() string { @@ -6503,7 +6614,7 @@ type Comment struct { func (x *Comment) Reset() { *x = Comment{} - mi := &file_compass_v1_compass_proto_msgTypes[93] + mi := &file_compass_v1_compass_proto_msgTypes[95] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6515,7 +6626,7 @@ func (x *Comment) String() string { func (*Comment) ProtoMessage() {} func (x *Comment) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[93] + mi := &file_compass_v1_compass_proto_msgTypes[95] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6528,7 +6639,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{93} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{95} } func (x *Comment) GetAuthor() string { @@ -6567,7 +6678,7 @@ type MergeQueueEntry struct { func (x *MergeQueueEntry) Reset() { *x = MergeQueueEntry{} - mi := &file_compass_v1_compass_proto_msgTypes[94] + mi := &file_compass_v1_compass_proto_msgTypes[96] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6579,7 +6690,7 @@ func (x *MergeQueueEntry) String() string { func (*MergeQueueEntry) ProtoMessage() {} func (x *MergeQueueEntry) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[94] + mi := &file_compass_v1_compass_proto_msgTypes[96] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6592,7 +6703,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{94} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{96} } func (x *MergeQueueEntry) GetProvider() string { @@ -6855,7 +6966,15 @@ const file_compass_v1_compass_proto_rawDesc = "" + "\x11IssueTokenRequest\x12%\n" + "\x0eaccount_handle\x18\x01 \x01(\tR\raccountHandle\"*\n" + "\x12IssueTokenResponse\x12\x14\n" + - "\x05token\x18\x01 \x01(\tR\x05token\"/\n" + + "\x05token\x18\x01 \x01(\tR\x05token\"\\\n" + + "\x14RenameAccountRequest\x12%\n" + + "\x0eaccount_handle\x18\x01 \x01(\tR\raccountHandle\x12\x1d\n" + + "\n" + + "new_handle\x18\x02 \x01(\tR\tnewHandle\"N\n" + + "\x15RenameAccountResponse\x12\x1d\n" + + "\n" + + "account_id\x18\x01 \x01(\tR\taccountId\x12\x16\n" + + "\x06handle\x18\x02 \x01(\tR\x06handle\"/\n" + "\x12RevokeTokenRequest\x12\x19\n" + "\x05token\x18\x01 \x01(\tB\x03\x80\x01\x01R\x05token\"\x15\n" + "\x13RevokeTokenResponse\"/\n" + @@ -7079,7 +7198,7 @@ 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\x81\x12\n" + + "\x15FORGE_PROVIDER_LINEAR\x10\x042\xd7\x12\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\x12Q\n" + @@ -7100,7 +7219,8 @@ const file_compass_v1_compass_proto_rawDesc = "" + "\x0eGetAgentStatus\x12!.compass.v1.GetAgentStatusRequest\x1a\".compass.v1.GetAgentStatusResponse\x12b\n" + "\x15SubscribeAgentSession\x12(.compass.v1.SubscribeAgentSessionRequest\x1a\x1d.compass.v1.AgentSessionFrame0\x01\x12K\n" + "\n" + - "IssueToken\x12\x1d.compass.v1.IssueTokenRequest\x1a\x1e.compass.v1.IssueTokenResponse\x12N\n" + + "IssueToken\x12\x1d.compass.v1.IssueTokenRequest\x1a\x1e.compass.v1.IssueTokenResponse\x12T\n" + + "\rRenameAccount\x12 .compass.v1.RenameAccountRequest\x1a!.compass.v1.RenameAccountResponse\x12N\n" + "\vRevokeToken\x12\x1e.compass.v1.RevokeTokenRequest\x1a\x1f.compass.v1.RevokeTokenResponse\x12W\n" + "\x0ePutAgentConfig\x12!.compass.v1.PutAgentConfigRequest\x1a\".compass.v1.PutAgentConfigResponse\x12c\n" + "\x12GetAgentConfigInfo\x12%.compass.v1.GetAgentConfigInfoRequest\x1a&.compass.v1.GetAgentConfigInfoResponse\x12`\n" + @@ -7127,7 +7247,7 @@ func file_compass_v1_compass_proto_rawDescGZIP() []byte { } var file_compass_v1_compass_proto_enumTypes = make([]protoimpl.EnumInfo, 14) -var file_compass_v1_compass_proto_msgTypes = make([]protoimpl.MessageInfo, 96) +var file_compass_v1_compass_proto_msgTypes = make([]protoimpl.MessageInfo, 98) var file_compass_v1_compass_proto_goTypes = []any{ (SecretDelivery)(0), // 0: compass.v1.SecretDelivery (SecretKind)(0), // 1: compass.v1.SecretKind @@ -7208,38 +7328,40 @@ var file_compass_v1_compass_proto_goTypes = []any{ (*GetAgentStatusResponse)(nil), // 76: compass.v1.GetAgentStatusResponse (*IssueTokenRequest)(nil), // 77: compass.v1.IssueTokenRequest (*IssueTokenResponse)(nil), // 78: compass.v1.IssueTokenResponse - (*RevokeTokenRequest)(nil), // 79: compass.v1.RevokeTokenRequest - (*RevokeTokenResponse)(nil), // 80: compass.v1.RevokeTokenResponse - (*PutAgentConfigRequest)(nil), // 81: compass.v1.PutAgentConfigRequest - (*PutAgentConfigResponse)(nil), // 82: compass.v1.PutAgentConfigResponse - (*GetAgentConfigInfoRequest)(nil), // 83: compass.v1.GetAgentConfigInfoRequest - (*GetAgentConfigInfoResponse)(nil), // 84: compass.v1.GetAgentConfigInfoResponse - (*DeleteAgentConfigRequest)(nil), // 85: compass.v1.DeleteAgentConfigRequest - (*DeleteAgentConfigResponse)(nil), // 86: compass.v1.DeleteAgentConfigResponse - (*ModelCandidate)(nil), // 87: compass.v1.ModelCandidate - (*ModelMetadata)(nil), // 88: compass.v1.ModelMetadata - (*ModelRegistryEntry)(nil), // 89: compass.v1.ModelRegistryEntry - (*ModelRegistry)(nil), // 90: compass.v1.ModelRegistry - (*PutModelRegistryRequest)(nil), // 91: compass.v1.PutModelRegistryRequest - (*PutModelRegistryResponse)(nil), // 92: compass.v1.PutModelRegistryResponse - (*GetModelRegistryRequest)(nil), // 93: compass.v1.GetModelRegistryRequest - (*GetModelRegistryResponse)(nil), // 94: compass.v1.GetModelRegistryResponse - (*DeleteModelRegistryRequest)(nil), // 95: compass.v1.DeleteModelRegistryRequest - (*DeleteModelRegistryResponse)(nil), // 96: compass.v1.DeleteModelRegistryResponse - (*AgentAttribution)(nil), // 97: compass.v1.AgentAttribution - (*ForgeRef)(nil), // 98: compass.v1.ForgeRef - (*Issue)(nil), // 99: compass.v1.Issue - (*PullRequest)(nil), // 100: compass.v1.PullRequest - (*ChecksSummary)(nil), // 101: compass.v1.ChecksSummary - (*Check)(nil), // 102: compass.v1.Check - (*ChangedStats)(nil), // 103: compass.v1.ChangedStats - (*TrackerRef)(nil), // 104: compass.v1.TrackerRef - (*Review)(nil), // 105: compass.v1.Review - (*ReviewThread)(nil), // 106: compass.v1.ReviewThread - (*Comment)(nil), // 107: compass.v1.Comment - (*MergeQueueEntry)(nil), // 108: compass.v1.MergeQueueEntry - nil, // 109: compass.v1.ModelRegistry.EntriesEntry - (*timestamppb.Timestamp)(nil), // 110: google.protobuf.Timestamp + (*RenameAccountRequest)(nil), // 79: compass.v1.RenameAccountRequest + (*RenameAccountResponse)(nil), // 80: compass.v1.RenameAccountResponse + (*RevokeTokenRequest)(nil), // 81: compass.v1.RevokeTokenRequest + (*RevokeTokenResponse)(nil), // 82: compass.v1.RevokeTokenResponse + (*PutAgentConfigRequest)(nil), // 83: compass.v1.PutAgentConfigRequest + (*PutAgentConfigResponse)(nil), // 84: compass.v1.PutAgentConfigResponse + (*GetAgentConfigInfoRequest)(nil), // 85: compass.v1.GetAgentConfigInfoRequest + (*GetAgentConfigInfoResponse)(nil), // 86: compass.v1.GetAgentConfigInfoResponse + (*DeleteAgentConfigRequest)(nil), // 87: compass.v1.DeleteAgentConfigRequest + (*DeleteAgentConfigResponse)(nil), // 88: compass.v1.DeleteAgentConfigResponse + (*ModelCandidate)(nil), // 89: compass.v1.ModelCandidate + (*ModelMetadata)(nil), // 90: compass.v1.ModelMetadata + (*ModelRegistryEntry)(nil), // 91: compass.v1.ModelRegistryEntry + (*ModelRegistry)(nil), // 92: compass.v1.ModelRegistry + (*PutModelRegistryRequest)(nil), // 93: compass.v1.PutModelRegistryRequest + (*PutModelRegistryResponse)(nil), // 94: compass.v1.PutModelRegistryResponse + (*GetModelRegistryRequest)(nil), // 95: compass.v1.GetModelRegistryRequest + (*GetModelRegistryResponse)(nil), // 96: compass.v1.GetModelRegistryResponse + (*DeleteModelRegistryRequest)(nil), // 97: compass.v1.DeleteModelRegistryRequest + (*DeleteModelRegistryResponse)(nil), // 98: compass.v1.DeleteModelRegistryResponse + (*AgentAttribution)(nil), // 99: compass.v1.AgentAttribution + (*ForgeRef)(nil), // 100: compass.v1.ForgeRef + (*Issue)(nil), // 101: compass.v1.Issue + (*PullRequest)(nil), // 102: compass.v1.PullRequest + (*ChecksSummary)(nil), // 103: compass.v1.ChecksSummary + (*Check)(nil), // 104: compass.v1.Check + (*ChangedStats)(nil), // 105: compass.v1.ChangedStats + (*TrackerRef)(nil), // 106: compass.v1.TrackerRef + (*Review)(nil), // 107: compass.v1.Review + (*ReviewThread)(nil), // 108: compass.v1.ReviewThread + (*Comment)(nil), // 109: compass.v1.Comment + (*MergeQueueEntry)(nil), // 110: compass.v1.MergeQueueEntry + nil, // 111: compass.v1.ModelRegistry.EntriesEntry + (*timestamppb.Timestamp)(nil), // 112: google.protobuf.Timestamp } var file_compass_v1_compass_proto_depIdxs = []int32{ 0, // 0: compass.v1.SetSecretRequest.delivery:type_name -> compass.v1.SecretDelivery @@ -7259,9 +7381,9 @@ var file_compass_v1_compass_proto_depIdxs = []int32{ 44, // 14: compass.v1.SubscribeEventsResponse.agent_message_chunk:type_name -> compass.v1.AgentMessageChunk 45, // 15: compass.v1.SubscribeEventsResponse.agent_tool_call:type_name -> compass.v1.AgentToolCall 46, // 16: compass.v1.SubscribeEventsResponse.agent_plan:type_name -> compass.v1.AgentPlan - 99, // 17: compass.v1.SubscribeEventsResponse.issue:type_name -> compass.v1.Issue - 99, // 18: compass.v1.ListBoardIssuesResponse.issues:type_name -> compass.v1.Issue - 99, // 19: compass.v1.SearchIssuesResponse.issues:type_name -> compass.v1.Issue + 101, // 17: compass.v1.SubscribeEventsResponse.issue:type_name -> compass.v1.Issue + 101, // 18: compass.v1.ListBoardIssuesResponse.issues:type_name -> compass.v1.Issue + 101, // 19: compass.v1.SearchIssuesResponse.issues:type_name -> compass.v1.Issue 4, // 20: compass.v1.ServerStatus.state:type_name -> compass.v1.ServerState 7, // 21: compass.v1.AgentSessionStatus.state:type_name -> compass.v1.AgentSessionState 5, // 22: compass.v1.AgentSessionStatus.runtime_tier:type_name -> compass.v1.RuntimeTier @@ -7287,29 +7409,29 @@ var file_compass_v1_compass_proto_depIdxs = []int32{ 48, // 42: compass.v1.AgentSessionFrame.event:type_name -> compass.v1.SessionEvent 7, // 43: compass.v1.AgentSessionFrame.state:type_name -> compass.v1.AgentSessionState 43, // 44: compass.v1.GetAgentStatusResponse.statuses:type_name -> compass.v1.AgentSessionStatus - 87, // 45: compass.v1.ModelRegistryEntry.candidates:type_name -> compass.v1.ModelCandidate - 88, // 46: compass.v1.ModelRegistryEntry.metadata:type_name -> compass.v1.ModelMetadata - 109, // 47: compass.v1.ModelRegistry.entries:type_name -> compass.v1.ModelRegistry.EntriesEntry - 90, // 48: compass.v1.PutModelRegistryRequest.registry:type_name -> compass.v1.ModelRegistry - 90, // 49: compass.v1.GetModelRegistryResponse.registry:type_name -> compass.v1.ModelRegistry + 89, // 45: compass.v1.ModelRegistryEntry.candidates:type_name -> compass.v1.ModelCandidate + 90, // 46: compass.v1.ModelRegistryEntry.metadata:type_name -> compass.v1.ModelMetadata + 111, // 47: compass.v1.ModelRegistry.entries:type_name -> compass.v1.ModelRegistry.EntriesEntry + 92, // 48: compass.v1.PutModelRegistryRequest.registry:type_name -> compass.v1.ModelRegistry + 92, // 49: compass.v1.GetModelRegistryResponse.registry:type_name -> compass.v1.ModelRegistry 13, // 50: compass.v1.ForgeRef.provider:type_name -> compass.v1.ForgeProvider - 98, // 51: compass.v1.Issue.forge:type_name -> compass.v1.ForgeRef - 97, // 52: compass.v1.Issue.agent:type_name -> compass.v1.AgentAttribution - 110, // 53: compass.v1.Issue.updated_at:type_name -> google.protobuf.Timestamp + 100, // 51: compass.v1.Issue.forge:type_name -> compass.v1.ForgeRef + 99, // 52: compass.v1.Issue.agent:type_name -> compass.v1.AgentAttribution + 112, // 53: compass.v1.Issue.updated_at:type_name -> google.protobuf.Timestamp 12, // 54: compass.v1.Issue.state:type_name -> compass.v1.IssueState - 100, // 55: compass.v1.Issue.prs:type_name -> compass.v1.PullRequest - 104, // 56: compass.v1.Issue.tracker:type_name -> compass.v1.TrackerRef - 98, // 57: compass.v1.PullRequest.forge:type_name -> compass.v1.ForgeRef - 97, // 58: compass.v1.PullRequest.agent:type_name -> compass.v1.AgentAttribution - 103, // 59: compass.v1.PullRequest.changed:type_name -> compass.v1.ChangedStats - 101, // 60: compass.v1.PullRequest.checks:type_name -> compass.v1.ChecksSummary - 105, // 61: compass.v1.PullRequest.reviews:type_name -> compass.v1.Review - 106, // 62: compass.v1.PullRequest.threads:type_name -> compass.v1.ReviewThread - 108, // 63: compass.v1.PullRequest.merge_queue:type_name -> compass.v1.MergeQueueEntry - 102, // 64: compass.v1.ChecksSummary.checks:type_name -> compass.v1.Check - 107, // 65: compass.v1.ReviewThread.comments:type_name -> compass.v1.Comment - 110, // 66: compass.v1.MergeQueueEntry.changed_at:type_name -> google.protobuf.Timestamp - 89, // 67: compass.v1.ModelRegistry.EntriesEntry.value:type_name -> compass.v1.ModelRegistryEntry + 102, // 55: compass.v1.Issue.prs:type_name -> compass.v1.PullRequest + 106, // 56: compass.v1.Issue.tracker:type_name -> compass.v1.TrackerRef + 100, // 57: compass.v1.PullRequest.forge:type_name -> compass.v1.ForgeRef + 99, // 58: compass.v1.PullRequest.agent:type_name -> compass.v1.AgentAttribution + 105, // 59: compass.v1.PullRequest.changed:type_name -> compass.v1.ChangedStats + 103, // 60: compass.v1.PullRequest.checks:type_name -> compass.v1.ChecksSummary + 107, // 61: compass.v1.PullRequest.reviews:type_name -> compass.v1.Review + 108, // 62: compass.v1.PullRequest.threads:type_name -> compass.v1.ReviewThread + 110, // 63: compass.v1.PullRequest.merge_queue:type_name -> compass.v1.MergeQueueEntry + 104, // 64: compass.v1.ChecksSummary.checks:type_name -> compass.v1.Check + 109, // 65: compass.v1.ReviewThread.comments:type_name -> compass.v1.Comment + 112, // 66: compass.v1.MergeQueueEntry.changed_at:type_name -> google.protobuf.Timestamp + 91, // 67: compass.v1.ModelRegistry.EntriesEntry.value:type_name -> compass.v1.ModelRegistryEntry 24, // 68: compass.v1.CompassService.GetServerInfo:input_type -> compass.v1.GetServerInfoRequest 27, // 69: compass.v1.CompassService.WhoAmI:input_type -> compass.v1.WhoAmIRequest 29, // 70: compass.v1.CompassService.GetTourState:input_type -> compass.v1.GetTourStateRequest @@ -7328,48 +7450,50 @@ var file_compass_v1_compass_proto_depIdxs = []int32{ 75, // 83: compass.v1.CompassService.GetAgentStatus:input_type -> compass.v1.GetAgentStatusRequest 59, // 84: compass.v1.CompassService.SubscribeAgentSession:input_type -> compass.v1.SubscribeAgentSessionRequest 77, // 85: compass.v1.CompassService.IssueToken:input_type -> compass.v1.IssueTokenRequest - 79, // 86: compass.v1.CompassService.RevokeToken:input_type -> compass.v1.RevokeTokenRequest - 81, // 87: compass.v1.CompassService.PutAgentConfig:input_type -> compass.v1.PutAgentConfigRequest - 83, // 88: compass.v1.CompassService.GetAgentConfigInfo:input_type -> compass.v1.GetAgentConfigInfoRequest - 85, // 89: compass.v1.CompassService.DeleteAgentConfig:input_type -> compass.v1.DeleteAgentConfigRequest - 91, // 90: compass.v1.CompassService.PutModelRegistry:input_type -> compass.v1.PutModelRegistryRequest - 93, // 91: compass.v1.CompassService.GetModelRegistry:input_type -> compass.v1.GetModelRegistryRequest - 95, // 92: compass.v1.CompassService.DeleteModelRegistry:input_type -> compass.v1.DeleteModelRegistryRequest - 14, // 93: compass.v1.SecretsService.SetSecret:input_type -> compass.v1.SetSecretRequest - 16, // 94: compass.v1.SecretsService.ListSecrets:input_type -> compass.v1.ListSecretsRequest - 19, // 95: compass.v1.SecretsService.DeleteSecret:input_type -> compass.v1.DeleteSecretRequest - 21, // 96: compass.v1.SecretsService.ListServerSecrets:input_type -> compass.v1.ListServerSecretsRequest - 25, // 97: compass.v1.CompassService.GetServerInfo:output_type -> compass.v1.GetServerInfoResponse - 28, // 98: compass.v1.CompassService.WhoAmI:output_type -> compass.v1.WhoAmIResponse - 30, // 99: compass.v1.CompassService.GetTourState:output_type -> compass.v1.GetTourStateResponse - 32, // 100: compass.v1.CompassService.ClaimTourStart:output_type -> compass.v1.ClaimTourStartResponse - 34, // 101: compass.v1.CompassService.SetTourState:output_type -> compass.v1.SetTourStateResponse - 36, // 102: compass.v1.CompassService.SubscribeEvents:output_type -> compass.v1.SubscribeEventsResponse - 38, // 103: compass.v1.CompassService.ListBoardIssues:output_type -> compass.v1.ListBoardIssuesResponse - 40, // 104: compass.v1.CompassService.SearchIssues:output_type -> compass.v1.SearchIssuesResponse - 62, // 105: compass.v1.CompassService.ProvisionAgentWorkspace:output_type -> compass.v1.ProvisionAgentWorkspaceResponse - 66, // 106: compass.v1.CompassService.StartAgentSession:output_type -> compass.v1.StartAgentSessionResponse - 68, // 107: compass.v1.CompassService.SpawnAgent:output_type -> compass.v1.SpawnAgentResponse - 70, // 108: compass.v1.CompassService.StopAgentSession:output_type -> compass.v1.StopAgentSessionResponse - 72, // 109: compass.v1.CompassService.SkipBatchWindow:output_type -> compass.v1.SkipBatchWindowResponse - 64, // 110: compass.v1.CompassService.RemoveAgentWorkspace:output_type -> compass.v1.RemoveAgentWorkspaceResponse - 74, // 111: compass.v1.CompassService.ReloadAgentSession:output_type -> compass.v1.ReloadAgentSessionResponse - 76, // 112: compass.v1.CompassService.GetAgentStatus:output_type -> compass.v1.GetAgentStatusResponse - 60, // 113: compass.v1.CompassService.SubscribeAgentSession:output_type -> compass.v1.AgentSessionFrame - 78, // 114: compass.v1.CompassService.IssueToken:output_type -> compass.v1.IssueTokenResponse - 80, // 115: compass.v1.CompassService.RevokeToken:output_type -> compass.v1.RevokeTokenResponse - 82, // 116: compass.v1.CompassService.PutAgentConfig:output_type -> compass.v1.PutAgentConfigResponse - 84, // 117: compass.v1.CompassService.GetAgentConfigInfo:output_type -> compass.v1.GetAgentConfigInfoResponse - 86, // 118: compass.v1.CompassService.DeleteAgentConfig:output_type -> compass.v1.DeleteAgentConfigResponse - 92, // 119: compass.v1.CompassService.PutModelRegistry:output_type -> compass.v1.PutModelRegistryResponse - 94, // 120: compass.v1.CompassService.GetModelRegistry:output_type -> compass.v1.GetModelRegistryResponse - 96, // 121: compass.v1.CompassService.DeleteModelRegistry:output_type -> compass.v1.DeleteModelRegistryResponse - 15, // 122: compass.v1.SecretsService.SetSecret:output_type -> compass.v1.SetSecretResponse - 17, // 123: compass.v1.SecretsService.ListSecrets:output_type -> compass.v1.ListSecretsResponse - 20, // 124: compass.v1.SecretsService.DeleteSecret:output_type -> compass.v1.DeleteSecretResponse - 22, // 125: compass.v1.SecretsService.ListServerSecrets:output_type -> compass.v1.ListServerSecretsResponse - 97, // [97:126] is the sub-list for method output_type - 68, // [68:97] is the sub-list for method input_type + 79, // 86: compass.v1.CompassService.RenameAccount:input_type -> compass.v1.RenameAccountRequest + 81, // 87: compass.v1.CompassService.RevokeToken:input_type -> compass.v1.RevokeTokenRequest + 83, // 88: compass.v1.CompassService.PutAgentConfig:input_type -> compass.v1.PutAgentConfigRequest + 85, // 89: compass.v1.CompassService.GetAgentConfigInfo:input_type -> compass.v1.GetAgentConfigInfoRequest + 87, // 90: compass.v1.CompassService.DeleteAgentConfig:input_type -> compass.v1.DeleteAgentConfigRequest + 93, // 91: compass.v1.CompassService.PutModelRegistry:input_type -> compass.v1.PutModelRegistryRequest + 95, // 92: compass.v1.CompassService.GetModelRegistry:input_type -> compass.v1.GetModelRegistryRequest + 97, // 93: compass.v1.CompassService.DeleteModelRegistry:input_type -> compass.v1.DeleteModelRegistryRequest + 14, // 94: compass.v1.SecretsService.SetSecret:input_type -> compass.v1.SetSecretRequest + 16, // 95: compass.v1.SecretsService.ListSecrets:input_type -> compass.v1.ListSecretsRequest + 19, // 96: compass.v1.SecretsService.DeleteSecret:input_type -> compass.v1.DeleteSecretRequest + 21, // 97: compass.v1.SecretsService.ListServerSecrets:input_type -> compass.v1.ListServerSecretsRequest + 25, // 98: compass.v1.CompassService.GetServerInfo:output_type -> compass.v1.GetServerInfoResponse + 28, // 99: compass.v1.CompassService.WhoAmI:output_type -> compass.v1.WhoAmIResponse + 30, // 100: compass.v1.CompassService.GetTourState:output_type -> compass.v1.GetTourStateResponse + 32, // 101: compass.v1.CompassService.ClaimTourStart:output_type -> compass.v1.ClaimTourStartResponse + 34, // 102: compass.v1.CompassService.SetTourState:output_type -> compass.v1.SetTourStateResponse + 36, // 103: compass.v1.CompassService.SubscribeEvents:output_type -> compass.v1.SubscribeEventsResponse + 38, // 104: compass.v1.CompassService.ListBoardIssues:output_type -> compass.v1.ListBoardIssuesResponse + 40, // 105: compass.v1.CompassService.SearchIssues:output_type -> compass.v1.SearchIssuesResponse + 62, // 106: compass.v1.CompassService.ProvisionAgentWorkspace:output_type -> compass.v1.ProvisionAgentWorkspaceResponse + 66, // 107: compass.v1.CompassService.StartAgentSession:output_type -> compass.v1.StartAgentSessionResponse + 68, // 108: compass.v1.CompassService.SpawnAgent:output_type -> compass.v1.SpawnAgentResponse + 70, // 109: compass.v1.CompassService.StopAgentSession:output_type -> compass.v1.StopAgentSessionResponse + 72, // 110: compass.v1.CompassService.SkipBatchWindow:output_type -> compass.v1.SkipBatchWindowResponse + 64, // 111: compass.v1.CompassService.RemoveAgentWorkspace:output_type -> compass.v1.RemoveAgentWorkspaceResponse + 74, // 112: compass.v1.CompassService.ReloadAgentSession:output_type -> compass.v1.ReloadAgentSessionResponse + 76, // 113: compass.v1.CompassService.GetAgentStatus:output_type -> compass.v1.GetAgentStatusResponse + 60, // 114: compass.v1.CompassService.SubscribeAgentSession:output_type -> compass.v1.AgentSessionFrame + 78, // 115: compass.v1.CompassService.IssueToken:output_type -> compass.v1.IssueTokenResponse + 80, // 116: compass.v1.CompassService.RenameAccount:output_type -> compass.v1.RenameAccountResponse + 82, // 117: compass.v1.CompassService.RevokeToken:output_type -> compass.v1.RevokeTokenResponse + 84, // 118: compass.v1.CompassService.PutAgentConfig:output_type -> compass.v1.PutAgentConfigResponse + 86, // 119: compass.v1.CompassService.GetAgentConfigInfo:output_type -> compass.v1.GetAgentConfigInfoResponse + 88, // 120: compass.v1.CompassService.DeleteAgentConfig:output_type -> compass.v1.DeleteAgentConfigResponse + 94, // 121: compass.v1.CompassService.PutModelRegistry:output_type -> compass.v1.PutModelRegistryResponse + 96, // 122: compass.v1.CompassService.GetModelRegistry:output_type -> compass.v1.GetModelRegistryResponse + 98, // 123: compass.v1.CompassService.DeleteModelRegistry:output_type -> compass.v1.DeleteModelRegistryResponse + 15, // 124: compass.v1.SecretsService.SetSecret:output_type -> compass.v1.SetSecretResponse + 17, // 125: compass.v1.SecretsService.ListSecrets:output_type -> compass.v1.ListSecretsResponse + 20, // 126: compass.v1.SecretsService.DeleteSecret:output_type -> compass.v1.DeleteSecretResponse + 22, // 127: compass.v1.SecretsService.ListServerSecrets:output_type -> compass.v1.ListServerSecretsResponse + 98, // [98:128] is the sub-list for method output_type + 68, // [68:98] is the sub-list for method input_type 68, // [68:68] is the sub-list for extension type_name 68, // [68:68] is the sub-list for extension extendee 0, // [0:68] is the sub-list for field type_name @@ -7410,7 +7534,7 @@ func file_compass_v1_compass_proto_init() { 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: 96, + NumMessages: 98, NumExtensions: 0, NumServices: 2, }, diff --git a/go/gen/compass/v1/compassv1connect/compass.connect.go b/go/gen/compass/v1/compassv1connect/compass.connect.go index 1a0dc3c7c..d681251d4 100644 --- a/go/gen/compass/v1/compassv1connect/compass.connect.go +++ b/go/gen/compass/v1/compassv1connect/compass.connect.go @@ -96,6 +96,9 @@ const ( // CompassServiceIssueTokenProcedure is the fully-qualified name of the CompassService's IssueToken // RPC. CompassServiceIssueTokenProcedure = "/compass.v1.CompassService/IssueToken" + // CompassServiceRenameAccountProcedure is the fully-qualified name of the CompassService's + // RenameAccount RPC. + CompassServiceRenameAccountProcedure = "/compass.v1.CompassService/RenameAccount" // CompassServiceRevokeTokenProcedure is the fully-qualified name of the CompassService's // RevokeToken RPC. CompassServiceRevokeTokenProcedure = "/compass.v1.CompassService/RevokeToken" @@ -236,6 +239,10 @@ type CompassServiceClient interface { // non-bootstrap account's token (the bootstrap admin token is issued // out-of-band at first start). IssueToken(context.Context, *connect.Request[v1.IssueTokenRequest]) (*connect.Response[v1.IssueTokenResponse], error) + // Admin-gated: only an admin may rename an account; non-admin callers get + // permission_denied. Freed handles may be reclaimed; the owner of an agent + // does not change. + RenameAccount(context.Context, *connect.Request[v1.RenameAccountRequest]) (*connect.Response[v1.RenameAccountResponse], error) // Admin-gated: only an admin may revoke a token; a non-admin caller gets // permission_denied. Revocation is BY TOKEN VALUE (per-hash), not by // account: the caller presents the token plaintext and the server hashes it, @@ -406,6 +413,12 @@ func NewCompassServiceClient(httpClient connect.HTTPClient, baseURL string, opts connect.WithSchema(compassServiceMethods.ByName("IssueToken")), connect.WithClientOptions(opts...), ), + renameAccount: connect.NewClient[v1.RenameAccountRequest, v1.RenameAccountResponse]( + httpClient, + baseURL+CompassServiceRenameAccountProcedure, + connect.WithSchema(compassServiceMethods.ByName("RenameAccount")), + connect.WithClientOptions(opts...), + ), revokeToken: connect.NewClient[v1.RevokeTokenRequest, v1.RevokeTokenResponse]( httpClient, baseURL+CompassServiceRevokeTokenProcedure, @@ -471,6 +484,7 @@ type compassServiceClient struct { getAgentStatus *connect.Client[v1.GetAgentStatusRequest, v1.GetAgentStatusResponse] subscribeAgentSession *connect.Client[v1.SubscribeAgentSessionRequest, v1.AgentSessionFrame] issueToken *connect.Client[v1.IssueTokenRequest, v1.IssueTokenResponse] + renameAccount *connect.Client[v1.RenameAccountRequest, v1.RenameAccountResponse] revokeToken *connect.Client[v1.RevokeTokenRequest, v1.RevokeTokenResponse] putAgentConfig *connect.Client[v1.PutAgentConfigRequest, v1.PutAgentConfigResponse] getAgentConfigInfo *connect.Client[v1.GetAgentConfigInfoRequest, v1.GetAgentConfigInfoResponse] @@ -570,6 +584,11 @@ func (c *compassServiceClient) IssueToken(ctx context.Context, req *connect.Requ return c.issueToken.CallUnary(ctx, req) } +// RenameAccount calls compass.v1.CompassService.RenameAccount. +func (c *compassServiceClient) RenameAccount(ctx context.Context, req *connect.Request[v1.RenameAccountRequest]) (*connect.Response[v1.RenameAccountResponse], error) { + return c.renameAccount.CallUnary(ctx, req) +} + // RevokeToken calls compass.v1.CompassService.RevokeToken. func (c *compassServiceClient) RevokeToken(ctx context.Context, req *connect.Request[v1.RevokeTokenRequest]) (*connect.Response[v1.RevokeTokenResponse], error) { return c.revokeToken.CallUnary(ctx, req) @@ -710,6 +729,10 @@ type CompassServiceHandler interface { // non-bootstrap account's token (the bootstrap admin token is issued // out-of-band at first start). IssueToken(context.Context, *connect.Request[v1.IssueTokenRequest]) (*connect.Response[v1.IssueTokenResponse], error) + // Admin-gated: only an admin may rename an account; non-admin callers get + // permission_denied. Freed handles may be reclaimed; the owner of an agent + // does not change. + RenameAccount(context.Context, *connect.Request[v1.RenameAccountRequest]) (*connect.Response[v1.RenameAccountResponse], error) // Admin-gated: only an admin may revoke a token; a non-admin caller gets // permission_denied. Revocation is BY TOKEN VALUE (per-hash), not by // account: the caller presents the token plaintext and the server hashes it, @@ -876,6 +899,12 @@ func NewCompassServiceHandler(svc CompassServiceHandler, opts ...connect.Handler connect.WithSchema(compassServiceMethods.ByName("IssueToken")), connect.WithHandlerOptions(opts...), ) + compassServiceRenameAccountHandler := connect.NewUnaryHandler( + CompassServiceRenameAccountProcedure, + svc.RenameAccount, + connect.WithSchema(compassServiceMethods.ByName("RenameAccount")), + connect.WithHandlerOptions(opts...), + ) compassServiceRevokeTokenHandler := connect.NewUnaryHandler( CompassServiceRevokeTokenProcedure, svc.RevokeToken, @@ -956,6 +985,8 @@ func NewCompassServiceHandler(svc CompassServiceHandler, opts ...connect.Handler compassServiceSubscribeAgentSessionHandler.ServeHTTP(w, r) case CompassServiceIssueTokenProcedure: compassServiceIssueTokenHandler.ServeHTTP(w, r) + case CompassServiceRenameAccountProcedure: + compassServiceRenameAccountHandler.ServeHTTP(w, r) case CompassServiceRevokeTokenProcedure: compassServiceRevokeTokenHandler.ServeHTTP(w, r) case CompassServicePutAgentConfigProcedure: @@ -1051,6 +1082,10 @@ func (UnimplementedCompassServiceHandler) IssueToken(context.Context, *connect.R return nil, connect.NewError(connect.CodeUnimplemented, errors.New("compass.v1.CompassService.IssueToken is not implemented")) } +func (UnimplementedCompassServiceHandler) RenameAccount(context.Context, *connect.Request[v1.RenameAccountRequest]) (*connect.Response[v1.RenameAccountResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("compass.v1.CompassService.RenameAccount is not implemented")) +} + func (UnimplementedCompassServiceHandler) RevokeToken(context.Context, *connect.Request[v1.RevokeTokenRequest]) (*connect.Response[v1.RevokeTokenResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("compass.v1.CompassService.RevokeToken is not implemented")) } diff --git a/go/internal/auth/admin_gate.go b/go/internal/auth/admin_gate.go index 9bdad7236..45db8bc2f 100644 --- a/go/internal/auth/admin_gate.go +++ b/go/internal/auth/admin_gate.go @@ -46,8 +46,8 @@ func (authenticatedOpen) isPrivilege() {} // method as open. func classifyProcedure(procedure string) (privilege, bool) { switch procedure { - // Privileged CompassService agent-session RPCs + token issuance + container - // teardown + fleet config writes: admin only. RemoveAgentWorkspace, PutAgentConfig, + // Privileged CompassService agent-session RPCs + token issuance + account rename + + // container teardown + fleet config writes: admin only. RemoveAgentWorkspace, PutAgentConfig, // DeleteAgentConfig are operator-scoped; SpawnAgent (composite Provision+Start) is // strictly more powerful, so admin-gated like them (DL-171). case compassv1connect.CompassServiceProvisionAgentWorkspaceProcedure, @@ -58,6 +58,7 @@ func classifyProcedure(procedure string) (privilege, bool) { compassv1connect.CompassServiceReloadAgentSessionProcedure, compassv1connect.CompassServiceGetAgentStatusProcedure, compassv1connect.CompassServiceIssueTokenProcedure, + compassv1connect.CompassServiceRenameAccountProcedure, compassv1connect.CompassServiceRevokeTokenProcedure, compassv1connect.CompassServicePutAgentConfigProcedure, compassv1connect.CompassServicePutModelRegistryProcedure, diff --git a/go/internal/auth/admin_gate_test.go b/go/internal/auth/admin_gate_test.go index 7cda47f1b..8fdc76f59 100644 --- a/go/internal/auth/admin_gate_test.go +++ b/go/internal/auth/admin_gate_test.go @@ -96,6 +96,7 @@ func TestAdminGateGatesFrozenAdminOnlyRPCs(t *testing.T) { {"ReloadAgentSession", compassv1connect.CompassServiceReloadAgentSessionProcedure}, {"GetAgentStatus", compassv1connect.CompassServiceGetAgentStatusProcedure}, {"IssueToken", compassv1connect.CompassServiceIssueTokenProcedure}, + {"RenameAccount", compassv1connect.CompassServiceRenameAccountProcedure}, {"RevokeToken", compassv1connect.CompassServiceRevokeTokenProcedure}, {"RemoveAgentWorkspace", compassv1connect.CompassServiceRemoveAgentWorkspaceProcedure}, } @@ -180,6 +181,7 @@ func TestClassifyProcedureClassifiesKnownAndUnknownProcedures(t *testing.T) { {"ReloadAgentSession", compassv1connect.CompassServiceReloadAgentSessionProcedure, true, true}, {"GetAgentStatus", compassv1connect.CompassServiceGetAgentStatusProcedure, true, true}, {"IssueToken", compassv1connect.CompassServiceIssueTokenProcedure, true, true}, + {"RenameAccount", compassv1connect.CompassServiceRenameAccountProcedure, true, true}, {"RemoveAgentWorkspace", compassv1connect.CompassServiceRemoveAgentWorkspaceProcedure, true, true}, {"PutAgentConfig", compassv1connect.CompassServicePutAgentConfigProcedure, true, true}, {"DeleteAgentConfig", compassv1connect.CompassServiceDeleteAgentConfigProcedure, true, true}, diff --git a/go/internal/store/account_handles_pgtest_test.go b/go/internal/store/account_handles_pgtest_test.go index b421840d4..bdde0daf3 100644 --- a/go/internal/store/account_handles_pgtest_test.go +++ b/go/internal/store/account_handles_pgtest_test.go @@ -8,9 +8,8 @@ package store // are globally unique, agent handles are unique only per owner, an agent handle // may overlap a global user handle, and both tiers rename-in-place and reclaim. // The uniqueness is enforced by the two partial-unique indexes authored into -// 0001_init.sql; CreateUser/CreateAgent write the rows, and a rename/reclaim is a -// direct UPDATE/DELETE against the row (no store rename API exists yet — the -// forward-looking write path is the index-level UPDATE these tests exercise). +// 0001_init.sql; CreateUser/CreateAgent write the rows and RenameHandle renames +// them in place. // // T2 — AccountsByHandles: owner-qualified and bare resolution, disambiguation by // owner, bare-agent defaulting to callerOwner, the atomic all-or-nothing miss @@ -111,54 +110,52 @@ func TestAgentHandleMayOverlapUserHandle(t *testing.T) { } } -// TestHandleRenameInPlaceBothTiers: an in-place rename (UPDATE account_handles) -// frees the old handle and claims the new one, for both a user (global index) -// and an agent (per-owner index). Simulates the forward-looking rename write path -// (no store rename API exists yet) against the resolution index directly. +// TestHandleRenameInPlaceBothTiers: rename updates both the resolution key and +// the accounts.handle display copy, while freeing the previous handle. func TestHandleRenameInPlaceBothTiers(t *testing.T) { - ctx := context.Background() + ctx := t.Context() s := newTestStore(t) user := mustUser(t, s, "oldname") owner := mustUser(t, s, "owner") agent := mustAgent(t, s, owner.ID, "old-agent") // User rename (global tier). - if _, err := s.pool.Exec(ctx, - "UPDATE account_handles SET handle = $2 WHERE account_id = $1", string(user.ID), "newname"); err != nil { - t.Fatalf("rename user handle: %v", err) + got, err := s.RenameHandle(ctx, user.ID, "newname") + if err != nil || got.Handle != "newname" { + t.Fatalf("RenameHandle(user, newname) = (%+v, %v), want handle newname", got, err) } if _, err := s.UserByHandle(ctx, "oldname"); err == nil { t.Fatal("old user handle still resolves after rename") } - got, err := s.UserByHandle(ctx, "newname") + got, err = s.UserByHandle(ctx, "newname") if err != nil || got.ID != user.ID { t.Fatalf("UserByHandle(newname) = (%v, %v), want %q", got.ID, err, user.ID) } + assertAccountDisplayHandle(t, ctx, s, user.ID, "newname") // Agent rename (per-owner tier). - if _, err := s.pool.Exec(ctx, - "UPDATE account_handles SET handle = $2 WHERE account_id = $1", string(agent.ID), "new-agent"); err != nil { - t.Fatalf("rename agent handle: %v", err) + gotA, err := s.RenameHandle(ctx, agent.ID, "new-agent") + if err != nil || gotA.Handle != "new-agent" { + t.Fatalf("RenameHandle(agent, new-agent) = (%+v, %v), want handle new-agent", gotA, err) } if _, err := s.AgentByHandle(ctx, owner.ID, "old-agent"); err == nil { t.Fatal("old agent handle still resolves after rename") } - gotA, err := s.AgentByHandle(ctx, owner.ID, "new-agent") + gotA, err = s.AgentByHandle(ctx, owner.ID, "new-agent") if err != nil || gotA.ID != agent.ID { t.Fatalf("AgentByHandle(owner, new-agent) = (%v, %v), want %q", gotA.ID, err, agent.ID) } + assertAccountDisplayHandle(t, ctx, s, agent.ID, "new-agent") } -// TestHandleReclaimBothTiers: a freed handle (rename away) may be re-registered -// by a NEW account, for both tiers — no tombstone, no reservation. +// TestHandleReclaimBothTiers: a freed handle may be re-registered at either tier. func TestHandleReclaimBothTiers(t *testing.T) { - ctx := context.Background() + ctx := t.Context() s := newTestStore(t) user := mustUser(t, s, "matt") // Free the user handle by renaming it away, then a new user reclaims "matt". - if _, err := s.pool.Exec(ctx, - "UPDATE account_handles SET handle = $2 WHERE account_id = $1", string(user.ID), "matt-old"); err != nil { - t.Fatalf("rename away: %v", err) + if _, err := s.RenameHandle(ctx, user.ID, "matt-old"); err != nil { + t.Fatalf("rename user away: %v", err) } reclaimer, err := s.CreateUser(ctx, NewUser{Handle: "matt", DisplayName: "New Matt"}) if err != nil { @@ -168,12 +165,12 @@ func TestHandleReclaimBothTiers(t *testing.T) { if err != nil || got.ID != reclaimer.ID { t.Fatalf("UserByHandle(matt) after reclaim = (%v, %v), want %q", got.ID, err, reclaimer.ID) } + assertAccountDisplayHandle(t, ctx, s, user.ID, "matt-old") // Agent tier: free `owner/ux`, a new agent reclaims it under the same owner. owner := mustUser(t, s, "owner") agent := mustAgent(t, s, owner.ID, "ux") - if _, err := s.pool.Exec(ctx, - "UPDATE account_handles SET handle = $2 WHERE account_id = $1", string(agent.ID), "ux-old"); err != nil { + if _, err := s.RenameHandle(ctx, agent.ID, "ux-old"); err != nil { t.Fatalf("rename agent away: %v", err) } reAgent, err := s.CreateAgent(ctx, owner.ID, NewAgent{Handle: "ux", DisplayName: "New UX"}) @@ -184,6 +181,119 @@ func TestHandleReclaimBothTiers(t *testing.T) { if err != nil || gotA.ID != reAgent.ID { t.Fatalf("AgentByHandle(owner, ux) after reclaim = (%v, %v), want %q", gotA.ID, err, reAgent.ID) } + assertAccountDisplayHandle(t, ctx, s, agent.ID, "ux-old") +} + +// TestRenameHandleCollisionsRespectHandleTiers proves conflicts remain local to +// each namespace and a failed update leaves both accounts unchanged. +func TestRenameHandleCollisionsRespectHandleTiers(t *testing.T) { + ctx := t.Context() + s := newTestStore(t) + createUser := func(handle string) Account { + t.Helper() + account, err := s.CreateUser(ctx, NewUser{Handle: handle, DisplayName: handle}) + if err != nil { + t.Fatalf("CreateUser(%q): %v", handle, err) + } + return account + } + createAgent := func(owner AccountID, handle string) Account { + t.Helper() + account, err := s.CreateAgent(ctx, owner, NewAgent{Handle: handle, DisplayName: handle}) + if err != nil { + t.Fatalf("CreateAgent(%q): %v", handle, err) + } + return account + } + assertBothHandles := func(account Account, want string) { + t.Helper() + if got, err := s.AccountHandle(ctx, account.ID); err != nil || got != want { + t.Fatalf("AccountHandle(%q) = (%q, %v), want %q", account.ID, got, err, want) + } + assertAccountDisplayHandle(t, ctx, s, account.ID, want) + } + + t.Run("user collision rolls back", func(t *testing.T) { + user := createUser("user-before") + taken := createUser("user-taken") + if _, err := s.RenameHandle(ctx, user.ID, taken.Handle); !errors.Is(err, ErrConflict) { + t.Fatalf("RenameHandle(user, taken) = %v, want ErrConflict", err) + } + assertBothHandles(user, "user-before") + assertBothHandles(taken, "user-taken") + }) + + t.Run("same owner agent collision rolls back", func(t *testing.T) { + owner := createUser("same-owner") + agent := createAgent(owner.ID, "agent-before") + sibling := createAgent(owner.ID, "agent-taken") + if _, err := s.RenameHandle(ctx, agent.ID, sibling.Handle); !errors.Is(err, ErrConflict) { + t.Fatalf("RenameHandle(agent, sibling handle) = %v, want ErrConflict", err) + } + assertBothHandles(agent, "agent-before") + assertBothHandles(sibling, "agent-taken") + }) + + t.Run("different owner agent handle succeeds", func(t *testing.T) { + ownerA := createUser("owner-a") + ownerB := createUser("owner-b") + agent := createAgent(ownerA.ID, "agent-a") + other := createAgent(ownerB.ID, "agent-b") + got, err := s.RenameHandle(ctx, agent.ID, other.Handle) + if err != nil || got.Handle != other.Handle { + t.Fatalf("RenameHandle(agent, different owner handle) = (%+v, %v)", got, err) + } + assertBothHandles(agent, "agent-b") + assertBothHandles(other, "agent-b") + }) + + t.Run("agent may use user handle", func(t *testing.T) { + user := createUser("shared-name") + owner := createUser("agent-owner") + agent := createAgent(owner.ID, "agent-name") + got, err := s.RenameHandle(ctx, agent.ID, user.Handle) + if err != nil || got.Handle != user.Handle { + t.Fatalf("RenameHandle(agent, user handle) = (%+v, %v)", got, err) + } + assertBothHandles(user, "shared-name") + assertBothHandles(agent, "shared-name") + }) +} + +// TestRenameHandleRejectsInvalidHandleAndUnknownID checks validation precedes +// the lookup and valid renames still report missing accounts. +func TestRenameHandleRejectsInvalidHandleAndUnknownID(t *testing.T) { + ctx := t.Context() + s := newTestStore(t) + user := mustUser(t, s, "rename-target") + if _, err := s.RenameHandle(ctx, user.ID, "owner/agent"); !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("RenameHandle(qualified handle) = %v, want ErrInvalidArgument", err) + } + if _, err := s.RenameHandle(ctx, "missing-account", "valid-name"); !errors.Is(err, ErrNotFound) { + t.Fatalf("RenameHandle(unknown id) = %v, want ErrNotFound", err) + } +} + +func TestRenameHandleCurrentHandleIsNoop(t *testing.T) { + ctx := t.Context() + s := newTestStore(t) + user := mustUser(t, s, "same-name") + got, err := s.RenameHandle(ctx, user.ID, user.Handle) + if err != nil || got.ID != user.ID || got.Handle != user.Handle { + t.Fatalf("RenameHandle(current handle) = (%+v, %v), want unchanged account", got, err) + } + assertAccountDisplayHandle(t, ctx, s, user.ID, user.Handle) +} + +func assertAccountDisplayHandle(t *testing.T, ctx context.Context, s *Store, id AccountID, want string) { + t.Helper() + var got string + if err := s.pool.QueryRow(ctx, "SELECT handle FROM accounts WHERE id = $1", string(id)).Scan(&got); err != nil { + t.Fatalf("read accounts.handle for %q: %v", id, err) + } + if got != want { + t.Fatalf("accounts.handle for %q = %q, want %q", id, got, want) + } } // TestAccountHandleReadsResolutionIndex: AccountHandle returns the diff --git a/go/internal/store/accounts.go b/go/internal/store/accounts.go index ac5421f00..1f63233a6 100644 --- a/go/internal/store/accounts.go +++ b/go/internal/store/accounts.go @@ -64,14 +64,22 @@ func insertAccountHandle(ctx context.Context, q *db.Queries, accountID, handle s Handle: handle, Column3: string(ownerUserID), }); err != nil { - if pgErrIs(err, pgUniqueViolation) { - return fmt.Errorf("%w: handle %q already taken", ErrConflict, handle) + if mapped := mapAccountHandleUniqueViolation(err, handle); mapped != nil { + return mapped } return fmt.Errorf("store: insert account_handle: %w", err) } return nil } +// mapAccountHandleUniqueViolation turns a handle unique violation into ErrConflict; nil otherwise. +func mapAccountHandleUniqueViolation(err error, handle string) error { + if pgErrIs(err, pgUniqueViolation) { + return fmt.Errorf("%w: handle %q already taken", ErrConflict, handle) + } + return nil +} + // CreateUser inserts a human account (a regular member; admin elevation is a // separate path, comms.proto:39-42) and returns it with its server-assigned id. // A duplicate handle is ErrConflict. @@ -509,6 +517,53 @@ func (s *Store) AccountHandle(ctx context.Context, id AccountID) (string, error) return handle, nil } +// RenameHandle changes an account's resolution and display handles atomically. +// A handle freed by the rename is immediately available for reuse. +func (s *Store) RenameHandle(ctx context.Context, id AccountID, newHandle string) (Account, error) { + if err := validateHandle(newHandle); err != nil { + return Account{}, err + } + + tx, err := s.beginTenantTx(ctx) + if err != nil { + return Account{}, fmt.Errorf("store: begin rename handle: %w", err) + } + defer func() { _ = tx.Rollback(ctx) }() + qtx := s.q.WithTx(tx) + + currentHandle, err := qtx.GetAccountHandle(ctx, string(id)) + if err != nil { + if noRows(err) { + return Account{}, fmt.Errorf("%w: account %q", ErrNotFound, id) + } + return Account{}, fmt.Errorf("store: get handle before rename: %w", err) + } + if currentHandle != newHandle { + rows, err := qtx.RenameAccountHandle(ctx, db.RenameAccountHandleParams{AccountID: string(id), Handle: newHandle}) + if err != nil { + if mapped := mapAccountHandleUniqueViolation(err, newHandle); mapped != nil { + return Account{}, mapped + } + return Account{}, fmt.Errorf("store: update account handle: %w", err) + } + if rows == 0 { + return Account{}, fmt.Errorf("%w: account %q", ErrNotFound, id) + } + rows, err = qtx.RenameAccountDisplayHandle(ctx, db.RenameAccountDisplayHandleParams{ID: string(id), Handle: newHandle}) + if err != nil { + return Account{}, fmt.Errorf("store: update account display handle: %w", err) + } + if rows == 0 { + return Account{}, fmt.Errorf("%w: account %q", ErrNotFound, id) + } + } + + if err := tx.Commit(ctx); err != nil { + return Account{}, fmt.Errorf("store: commit rename handle: %w", err) + } + return s.GetAccount(ctx, id) +} + // ResolveOwner resolves a caller to the user account it acts under. It mirrors // the caller-owner resolution ReparentAgent applies inline (clause 0): an agent // caller resolves to its agent_accounts.owner_user_id, while a user caller has diff --git a/go/internal/store/db/accounts.sql.go b/go/internal/store/db/accounts.sql.go index f2303277a..fe765480f 100644 --- a/go/internal/store/db/accounts.sql.go +++ b/go/internal/store/db/accounts.sql.go @@ -459,6 +459,44 @@ func (q *Queries) ListVisibleAccounts(ctx context.Context, id string) ([]ListVis return items, nil } +const renameAccountDisplayHandle = `-- name: RenameAccountDisplayHandle :execrows +UPDATE accounts +SET handle = $2 +WHERE id = $1 +` + +type RenameAccountDisplayHandleParams struct { + ID string + Handle string +} + +func (q *Queries) RenameAccountDisplayHandle(ctx context.Context, arg RenameAccountDisplayHandleParams) (int64, error) { + result, err := q.db.Exec(ctx, renameAccountDisplayHandle, arg.ID, arg.Handle) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} + +const renameAccountHandle = `-- name: RenameAccountHandle :execrows +UPDATE account_handles +SET handle = $2 +WHERE account_id = $1 +` + +type RenameAccountHandleParams struct { + AccountID string + Handle string +} + +func (q *Queries) RenameAccountHandle(ctx context.Context, arg RenameAccountHandleParams) (int64, error) { + result, err := q.db.Exec(ctx, renameAccountHandle, arg.AccountID, arg.Handle) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} + const resolveGlobalHandles = `-- name: ResolveGlobalHandles :many SELECT ah.handle, ah.account_id FROM account_handles ah diff --git a/go/internal/store/db/querier.go b/go/internal/store/db/querier.go index 03ce70e5a..0ef48f24f 100644 --- a/go/internal/store/db/querier.go +++ b/go/internal/store/db/querier.go @@ -582,6 +582,8 @@ type Querier interface { // even when the previous one was already consumed. RecordStateTransition(ctx context.Context, arg RecordStateTransitionParams) error RemarkSafetyValveSuperseded(ctx context.Context, arg RemarkSafetyValveSupersededParams) error + RenameAccountDisplayHandle(ctx context.Context, arg RenameAccountDisplayHandleParams) (int64, error) + RenameAccountHandle(ctx context.Context, arg RenameAccountHandleParams) (int64, error) RenameTopic(ctx context.Context, arg RenameTopicParams) error RequireAgentSessionOwner(ctx context.Context, arg RequireAgentSessionOwnerParams) (bool, error) RequireAgentSessionSubscriber(ctx context.Context, arg RequireAgentSessionSubscriberParams) (bool, error) diff --git a/go/internal/store/queries/accounts.sql b/go/internal/store/queries/accounts.sql index 17d7d9e3c..98e29351e 100644 --- a/go/internal/store/queries/accounts.sql +++ b/go/internal/store/queries/accounts.sql @@ -34,6 +34,16 @@ VALUES ($1, $2, $3, $4, $5, NULLIF($6, '')); INSERT INTO account_handles (account_id, handle, owner_user_id) VALUES ($1, $2, NULLIF($3, '')); +-- name: RenameAccountHandle :execrows +UPDATE account_handles +SET handle = $2 +WHERE account_id = $1; + +-- name: RenameAccountDisplayHandle :execrows +UPDATE accounts +SET handle = $2 +WHERE id = $1; + -- name: InsertHomeChannel :exec INSERT INTO channels (id, name, group_id, kind) VALUES ($1, $2, NULL, $3); diff --git a/go/server/network_door_test.go b/go/server/network_door_test.go index 04add05a7..bef793aab 100644 --- a/go/server/network_door_test.go +++ b/go/server/network_door_test.go @@ -842,6 +842,93 @@ func TestIssueTokenHandlerInputContract(t *testing.T) { }) } +func TestRenameAccountHandlerInputContract(t *testing.T) { + ctx := t.Context() + st, _, member := newNetworkStore(t) + owner, err := st.GetAccount(ctx, member) + if err != nil { + t.Fatalf("GetAccount(member): %v", err) + } + agent, err := st.CreateAgent(ctx, member, store.NewAgent{Handle: "rename-agent", DisplayName: "Rename Agent"}) + if err != nil { + t.Fatalf("CreateAgent(member/rename-agent): %v", err) + } + _, err = st.CreateUser(ctx, store.NewUser{Handle: "rename-taken", DisplayName: "Taken"}) + if err != nil { + t.Fatalf("CreateUser(rename-taken): %v", err) + } + bus := events.NewBus[busPayload]() + t.Cleanup(bus.Close) + client := newH2CClient(t, newH2CTestServer(t, newService("test", bus, st, nil, nil, nil, nil))) + + resp, err := client.RenameAccount(ctx, connect.NewRequest(&compassv1.RenameAccountRequest{ + AccountHandle: owner.Handle, + NewHandle: "renamed-member", + })) + if err != nil || resp.Msg.GetAccountId() != string(member) || resp.Msg.GetHandle() != "renamed-member" { + t.Fatalf("RenameAccount(user) = (%+v, %v), want id %q and handle renamed-member", resp, err, member) + } + resp, err = client.RenameAccount(ctx, connect.NewRequest(&compassv1.RenameAccountRequest{ + AccountHandle: "renamed-member/rename-agent", + NewHandle: "renamed-agent", + })) + if err != nil || resp.Msg.GetAccountId() != string(agent.ID) || resp.Msg.GetHandle() != "renamed-member/renamed-agent" { + t.Fatalf("RenameAccount(agent) = (%+v, %v), want qualified renamed handle", resp, err) + } + + tests := []struct { + name string + req *compassv1.RenameAccountRequest + code connect.Code + }{ + { + name: "conflict maps to AlreadyExists", + req: &compassv1.RenameAccountRequest{AccountHandle: "renamed-member", NewHandle: "rename-taken"}, + code: connect.CodeAlreadyExists, + }, + { + name: "empty handle maps to InvalidArgument", + req: &compassv1.RenameAccountRequest{AccountHandle: "", NewHandle: "valid-name"}, + code: connect.CodeInvalidArgument, + }, + { + name: "invalid new handle maps to InvalidArgument", + req: &compassv1.RenameAccountRequest{AccountHandle: "renamed-member", NewHandle: "Bad Handle"}, + code: connect.CodeInvalidArgument, + }, + { + name: "qualified new handle maps to InvalidArgument", + req: &compassv1.RenameAccountRequest{AccountHandle: "renamed-member", NewHandle: "other/agent"}, + code: connect.CodeInvalidArgument, + }, + { + name: "malformed qualified account handle maps to InvalidArgument", + req: &compassv1.RenameAccountRequest{AccountHandle: "owner/agent/nested", NewHandle: "valid-name"}, + code: connect.CodeInvalidArgument, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := client.RenameAccount(ctx, connect.NewRequest(tc.req)) + if got := connect.CodeOf(err); got != tc.code { + t.Fatalf("RenameAccount error code = %v, want %v", got, tc.code) + } + }) + } + + system, err := st.EnsureSystemAccount(ctx) + if err != nil { + t.Fatalf("EnsureSystemAccount: %v", err) + } + _, err = client.RenameAccount(ctx, connect.NewRequest(&compassv1.RenameAccountRequest{ + AccountHandle: system.Handle, + NewHandle: "renamed-system", + })) + if code := connect.CodeOf(err); code != connect.CodePermissionDenied { + t.Fatalf("RenameAccount(system account) = %v, want PermissionDenied", code) + } +} + // TestServeWithListenWritesAdminToken0600: on a --listen start, Serve mints the // bootstrap-admin bearer token and persists it under StateDir at mode exactly // 0600. The file's token must authenticate as the admin over the network door — diff --git a/go/server/service.go b/go/server/service.go index b604160d3..c12bac65b 100644 --- a/go/server/service.go +++ b/go/server/service.go @@ -435,6 +435,62 @@ func (s *service) IssueToken( return connect.NewResponse(&compassv1.IssueTokenResponse{Token: token}), nil } +// RenameAccount changes an account's handle through the admin-only operator door. +func (s *service) RenameAccount( + ctx context.Context, + req *connect.Request[compassv1.RenameAccountRequest], +) (*connect.Response[compassv1.RenameAccountResponse], error) { + raw := req.Msg.GetAccountHandle() + newHandle := req.Msg.GetNewHandle() + if raw == "" || newHandle == "" { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("account_handle and new_handle are required")) + } + qh := store.ParseQualifiedHandle(raw) + if qh.Malformed() { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("account_handle has invalid qualified-handle syntax")) + } + var acc store.Account + var err error + if qh.Qualified() { + acc, err = s.resolveQualifiedAgent(ctx, raw) + } else if acc, err = s.store.UserByHandle(ctx, raw); err != nil { + err = handleLookupError(raw, err) + } + if err != nil { + return nil, err + } + if acc.System != nil { + return nil, connect.NewError(connect.CodePermissionDenied, + fmt.Errorf("account %q is the system account and cannot be renamed", raw)) + } + + renamed, err := s.store.RenameHandle(ctx, acc.ID, newHandle) + if err != nil { + switch { + case errors.Is(err, store.ErrInvalidArgument): + return nil, connect.NewError(connect.CodeInvalidArgument, err) + case errors.Is(err, store.ErrConflict): + return nil, connect.NewError(connect.CodeAlreadyExists, err) + case errors.Is(err, store.ErrNotFound): + return nil, connect.NewError(connect.CodeNotFound, err) + default: + return nil, connect.NewError(connect.CodeInternal, err) + } + } + handle := renamed.Handle + if renamed.Agent != nil { + owner, err := s.store.GetAccount(ctx, renamed.Agent.OwnerUserID) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } + handle = owner.Handle + "/" + renamed.Handle + } + return connect.NewResponse(&compassv1.RenameAccountResponse{ + AccountId: string(renamed.ID), + Handle: handle, + }), nil +} + // RevokeToken withdraws a bearer token by its presented plaintext (network door, // admin-gated by classifyProcedure). The CLI sends the plaintext over the // authenticated door and this handler hashes it (auth.RevokeToken → hashToken): 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 617787dd6..7a6297d40 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("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEi0AEKEFNldFNlY3JldFJlcXVlc3QSDAoEbmFtZRgBIAEoCRISCgV2YWx1ZRgCIAEoCUIDgAEBEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkSJgoFc2NvcGUYByABKA4yFy5jb21wYXNzLnYxLlNlY3JldFNjb3BlIhMKEVNldFNlY3JldFJlc3BvbnNlIhQKEkxpc3RTZWNyZXRzUmVxdWVzdCJAChNMaXN0U2VjcmV0c1Jlc3BvbnNlEikKB3NlY3JldHMYASADKAsyGC5jb21wYXNzLnYxLlNlY3JldFN0YXR1cyKgAQoMU2VjcmV0U3RhdHVzEgwKBG5hbWUYASABKAkSDgoGaXNfc2V0GAIgASgIEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkiSwoTRGVsZXRlU2VjcmV0UmVxdWVzdBIMCgRuYW1lGAEgASgJEiYKBXNjb3BlGAIgASgOMhcuY29tcGFzcy52MS5TZWNyZXRTY29wZSIWChREZWxldGVTZWNyZXRSZXNwb25zZSIaChhMaXN0U2VydmVyU2VjcmV0c1JlcXVlc3QiUwoZTGlzdFNlcnZlclNlY3JldHNSZXNwb25zZRI2Cg5zZXJ2ZXJfc2VjcmV0cxgBIAMoCzIeLmNvbXBhc3MudjEuU2VydmVyU2VjcmV0U3RhdHVzIjIKElNlcnZlclNlY3JldFN0YXR1cxIMCgRuYW1lGAEgASgJEg4KBmlzX3NldBgCIAEoCCIWChRHZXRTZXJ2ZXJJbmZvUmVxdWVzdCKAAQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSEwoLYXBpX3ZlcnNpb24YAiABKAkSCwoDcmV2GAMgASgJEjQKEGVucm9sbGVkX3J1bm5lcnMYBCADKAsyGi5jb21wYXNzLnYxLkVucm9sbGVkUnVubmVyIjAKDkVucm9sbGVkUnVubmVyEgoKAmlkGAEgASgJEhIKCmVucm9sbG1lbnQYAiABKAQiDwoNV2hvQW1JUmVxdWVzdCIkCg5XaG9BbUlSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJIhUKE0dldFRvdXJTdGF0ZVJlcXVlc3QiUQoUR2V0VG91clN0YXRlUmVzcG9uc2USKAoHb3V0Y29tZRgBIAEoDjIXLmNvbXBhc3MudjEuVG91ck91dGNvbWUSDwoHc3RlcF9pZBgCIAEoCSIoChVDbGFpbVRvdXJTdGFydFJlcXVlc3QSDwoHc3RlcF9pZBgBIAEoCSIpChZDbGFpbVRvdXJTdGFydFJlc3BvbnNlEg8KB2NsYWltZWQYASABKAgiUAoTU2V0VG91clN0YXRlUmVxdWVzdBIoCgdvdXRjb21lGAEgASgOMhcuY29tcGFzcy52MS5Ub3VyT3V0Y29tZRIPCgdzdGVwX2lkGAIgASgJIhYKFFNldFRvdXJTdGF0ZVJlc3BvbnNlIkMKFlN1YnNjcmliZUV2ZW50c1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEIuIDChdTdWJzY3JpYmVFdmVudHNSZXNwb25zZRILCgNzZXEYASABKAQSEgoKYXRfdW5peF9tcxgCIAEoAxIWCg5pbnN0YW5jZV9lcG9jaBgDIAEoBBIUCgxzbmFwc2hvdF9zZXEYBCABKAQSMQoNc2VydmVyX3N0YXR1cxgKIAEoCzIYLmNvbXBhc3MudjEuU2VydmVyU3RhdHVzSAASNQoPcmVzeW5jX3JlcXVpcmVkGAsgASgLMhouY29tcGFzcy52MS5SZXN5bmNSZXF1aXJlZEgAEj4KFGFnZW50X3Nlc3Npb25fc3RhdHVzGAwgASgLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXNIABI8ChNhZ2VudF9tZXNzYWdlX2NodW5rGA0gASgLMh0uY29tcGFzcy52MS5BZ2VudE1lc3NhZ2VDaHVua0gAEjQKD2FnZW50X3Rvb2xfY2FsbBgOIAEoCzIZLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbEgAEisKCmFnZW50X3BsYW4YDyABKAsyFS5jb21wYXNzLnYxLkFnZW50UGxhbkgAEiIKBWlzc3VlGBAgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAQgkKB3BheWxvYWQiLgoWTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPAoXTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSJJChNTZWFyY2hJc3N1ZXNSZXF1ZXN0Eg0KBXF1ZXJ5GAEgASgJEg0KBWxpbWl0GAIgASgNEhQKDHNuYXBzaG90X3NlcRgDIAEoBCI5ChRTZWFyY2hJc3N1ZXNSZXNwb25zZRIhCgZpc3N1ZXMYASADKAsyES5jb21wYXNzLnYxLklzc3VlIjYKDFNlcnZlclN0YXR1cxImCgVzdGF0ZRgBIAEoDjIXLmNvbXBhc3MudjEuU2VydmVyU3RhdGUiEAoOUmVzeW5jUmVxdWlyZWQi0gEKEkFnZW50U2Vzc2lvblN0YXR1cxISCgpzZXNzaW9uX2lkGAEgASgJEiwKBXN0YXRlGAIgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZRIYChBhZ2VudF9hY2NvdW50X2lkGAMgASgJEi0KDHJ1bnRpbWVfdGllchgEIAEoDjIXLmNvbXBhc3MudjEuUnVudGltZVRpZXISMQoOZWdyZXNzX3Bvc3R1cmUYBSABKA4yGS5jb21wYXNzLnYxLkVncmVzc1Bvc3R1cmUiSQoRQWdlbnRNZXNzYWdlQ2h1bmsSEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgR0ZXh0GAIgASgJEhIKCmlzX3Rob3VnaHQYAyABKAgieQoNQWdlbnRUb29sQ2FsbBISCgpzZXNzaW9uX2lkGAEgASgJEhQKDHRvb2xfY2FsbF9pZBgCIAEoCRINCgV0aXRsZRgDIAEoCRIvCgZzdGF0dXMYBCABKA4yHy5jb21wYXNzLnYxLkFnZW50VG9vbENhbGxTdGF0dXMiTAoJQWdlbnRQbGFuEhIKCnNlc3Npb25faWQYASABKAkSKwoHZW50cmllcxgCIAMoCzIaLmNvbXBhc3MudjEuQWdlbnRQbGFuRW50cnkiUwoOQWdlbnRQbGFuRW50cnkSDwoHY29udGVudBgBIAEoCRIwCgZzdGF0dXMYAiABKA4yIC5jb21wYXNzLnYxLkFnZW50UGxhbkVudHJ5U3RhdHVzIpkECgxTZXNzaW9uRXZlbnQSEAoIZXZlbnRfaWQYASABKAkSEgoKYXRfdW5peF9tcxgCIAEoAxI6Cg5hc3Npc3RhbnRfdGV4dBgDIAEoCzIgLmNvbXBhc3MudjEuU2Vzc2lvbkFzc2lzdGFudFRleHRIABIvCgh0aGlua2luZxgEIAEoCzIbLmNvbXBhc3MudjEuU2Vzc2lvblRoaW5raW5nSAASMAoJdG9vbF9jYWxsGAUgASgLMhsuY29tcGFzcy52MS5TZXNzaW9uVG9vbENhbGxIABI9ChB0b29sX2NhbGxfdXBkYXRlGAYgASgLMiEuY29tcGFzcy52MS5TZXNzaW9uVG9vbENhbGxVcGRhdGVIABInCgRwbGFuGAcgASgLMhcuY29tcGFzcy52MS5TZXNzaW9uUGxhbkgAEisKBm5vdGljZRgIIAEoCzIZLmNvbXBhc3MudjEuU2Vzc2lvbk5vdGljZUgAEjkKEXNlc3Npb25faW5qZWN0aW9uGAkgASgLMhwuY29tcGFzcy52MS5TZXNzaW9uSW5qZWN0aW9uSAASMQoNc2Vzc2lvbl9lcnJvchgKIAEoCzIYLmNvbXBhc3MudjEuU2Vzc2lvbkVycm9ySAASOAoNYmF0Y2hfcGVuZGluZxgLIAEoCzIfLmNvbXBhc3MudjEuU2Vzc2lvbkJhdGNoUGVuZGluZ0gAQgcKBWV2ZW50IjgKFFNlc3Npb25Bc3Npc3RhbnRUZXh0EgwKBHRleHQYASABKAkSEgoKbWVzc2FnZV9pZBgCIAEoCSIzCg9TZXNzaW9uVGhpbmtpbmcSDAoEdGV4dBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJImcKD1Nlc3Npb25Ub29sQ2FsbBIUCgx0b29sX2NhbGxfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSLwoGc3RhdHVzGAMgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIpoBChVTZXNzaW9uVG9vbENhbGxVcGRhdGUSFAoMdG9vbF9jYWxsX2lkGAEgASgJEi8KBnN0YXR1cxgCIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cxIOCgZvdXRwdXQYAyABKAkSKgoFZGlmZnMYBCADKAsyGy5jb21wYXNzLnYxLlNlc3Npb25GaWxlRGlmZiJVCg9TZXNzaW9uRmlsZURpZmYSDAoEcGF0aBgBIAEoCRIVCghvbGRfdGV4dBgCIAEoCUgAiAEBEhAKCG5ld190ZXh0GAMgASgJQgsKCV9vbGRfdGV4dCI6CgtTZXNzaW9uUGxhbhIrCgdlbnRyaWVzGAEgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSI5Cg1TZXNzaW9uTm90aWNlEgwKBHRleHQYASABKAkSEQoEbGluaxgCIAEoCUgAiAEBQgcKBV9saW5rIoMBChBTZXNzaW9uSW5qZWN0aW9uEjEKB29wX2tpbmQYASABKA4yIC5jb21wYXNzLnYxLlNlc3Npb25JbmplY3Rpb25LaW5kEhIKCm1lc3NhZ2VfaWQYAiABKAkSEwoLZnJvbV9oYW5kbGUYAyABKAkSEwoLdHJhY2VwYXJlbnQYBCABKAkiawoMU2Vzc2lvbkVycm9yEioKBGtpbmQYASABKA4yHC5jb21wYXNzLnYxLlNlc3Npb25FcnJvcktpbmQSDwoHbWVzc2FnZRgCIAEoCRITCgZzdGF0dXMYAyABKAVIAIgBAUIJCgdfc3RhdHVzIj4KE1Nlc3Npb25CYXRjaFBlbmRpbmcSDQoFY291bnQYASABKA0SGAoQZmlyZXNfYXRfdW5peF9tcxgCIAEoAyIyChxTdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkilQEKEUFnZW50U2Vzc2lvbkZyYW1lEhIKCnNlc3Npb25faWQYASABKAkSJwoFZXZlbnQYAiABKAsyGC5jb21wYXNzLnYxLlNlc3Npb25FdmVudBIsCgVzdGF0ZRgDIAEoDjIdLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdGUSFQoNdHVybl9zZXF1ZW5jZRgEIAEoBCJwCh5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJEg8KB3BlcnNvbmEYAyABKAkSDAoEcm9sZRgEIAEoCSI5Ch9Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEhYKDmNvbnRhaW5lcl9uYW1lGAEgASgJIlAKG1JlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgCIAEoCSIeChxSZW1vdmVBZ2VudFdvcmtzcGFjZVJlc3BvbnNlImMKGFN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFyZXN1bWVfc2Vzc2lvbl9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiLwoZU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIloKEVNwYXduQWdlbnRSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiQAoSU3Bhd25BZ2VudFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSFgoOY29udGFpbmVyX25hbWUYAiABKAkiLQoXU3RvcEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIaChhTdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2UiLAoWU2tpcEJhdGNoV2luZG93UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIhkKF1NraXBCYXRjaFdpbmRvd1Jlc3BvbnNlIi8KGVJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIwChpSZWxvYWRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIisKFUdldEFnZW50U3RhdHVzUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJInwKFkdldEFnZW50U3RhdHVzUmVzcG9uc2USMAoIc3RhdHVzZXMYASADKAsyHi5jb21wYXNzLnYxLkFnZW50U2Vzc2lvblN0YXR1cxIcCg9ydW5uZXJfZW5yb2xsZWQYAiABKAhIAIgBAUISChBfcnVubmVyX2Vucm9sbGVkIisKEUlzc3VlVG9rZW5SZXF1ZXN0EhYKDmFjY291bnRfaGFuZGxlGAEgASgJIiMKEklzc3VlVG9rZW5SZXNwb25zZRINCgV0b2tlbhgBIAEoCSIoChJSZXZva2VUb2tlblJlcXVlc3QSEgoFdG9rZW4YASABKAlCA4ABASIVChNSZXZva2VUb2tlblJlc3BvbnNlIicKFVB1dEFnZW50Q29uZmlnUmVxdWVzdBIOCgZidW5kbGUYASABKAwiKQoWUHV0QWdlbnRDb25maWdSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJIhsKGUdldEFnZW50Q29uZmlnSW5mb1JlcXVlc3Qi2gEKGkdldEFnZW50Q29uZmlnSW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSDgoGc2tpbGxzGAIgAygJEhIKCmV4dGVuc2lvbnMYAyADKAkSEwoLbWNwX3NlcnZlcnMYBCADKAkSFAoMaGFzX3NldHRpbmdzGAUgASgIEhUKDWhhc19hZ2VudHNfbWQYBiABKAgSDQoFcnVsZXMYByADKAkSEQoJc3ViYWdlbnRzGAggAygJEhIKCmhhc19tb2RlbHMYCSABKAgSDwoHcHJvbXB0cxgKIAMoCSIaChhEZWxldGVBZ2VudENvbmZpZ1JlcXVlc3QiGwoZRGVsZXRlQWdlbnRDb25maWdSZXNwb25zZSI0Cg5Nb2RlbENhbmRpZGF0ZRIQCghwcm92aWRlchgBIAEoCRIQCghtb2RlbF9pZBgCIAEoCSJxCg1Nb2RlbE1ldGFkYXRhEhYKDmNvbnRleHRfd2luZG93GAEgASgDEhwKFGlucHV0X2Nvc3RfbWljcm9fdXNkGAIgASgDEh0KFW91dHB1dF9jb3N0X21pY3JvX3VzZBgDIAEoAxILCgNhcGkYBCABKAkihwEKEk1vZGVsUmVnaXN0cnlFbnRyeRIUCgxkaXNwbGF5X25hbWUYASABKAkSLgoKY2FuZGlkYXRlcxgCIAMoCzIaLmNvbXBhc3MudjEuTW9kZWxDYW5kaWRhdGUSKwoIbWV0YWRhdGEYAyABKAsyGS5jb21wYXNzLnYxLk1vZGVsTWV0YWRhdGEimAEKDU1vZGVsUmVnaXN0cnkSNwoHZW50cmllcxgBIAMoCzImLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeS5FbnRyaWVzRW50cnkaTgoMRW50cmllc0VudHJ5EgsKA2tleRgBIAEoCRItCgV2YWx1ZRgCIAEoCzIeLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeUVudHJ5OgI4ASJgChdQdXRNb2RlbFJlZ2lzdHJ5UmVxdWVzdBIrCghyZWdpc3RyeRgBIAEoCzIZLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeRIYChBleHBlY3RlZF92ZXJzaW9uGAIgASgDIisKGFB1dE1vZGVsUmVnaXN0cnlSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgDIhkKF0dldE1vZGVsUmVnaXN0cnlSZXF1ZXN0IlgKGEdldE1vZGVsUmVnaXN0cnlSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgDEisKCHJlZ2lzdHJ5GAIgASgLMhkuY29tcGFzcy52MS5Nb2RlbFJlZ2lzdHJ5IhwKGkRlbGV0ZU1vZGVsUmVnaXN0cnlSZXF1ZXN0Ih0KG0RlbGV0ZU1vZGVsUmVnaXN0cnlSZXNwb25zZSI+ChBBZ2VudEF0dHJpYnV0aW9uEhQKDGFnZW50X2hhbmRsZRgBIAEoCRIUCgxvd25lcl9oYW5kbGUYAiABKAkiRQoIRm9yZ2VSZWYSKwoIcHJvdmlkZXIYASABKA4yGS5jb21wYXNzLnYxLkZvcmdlUHJvdmlkZXISDAoEaG9zdBgCIAEoCSLUAwoFSXNzdWUSCgoCaWQYASABKAkSIwoFZm9yZ2UYAiABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAyABKAkSDgoGbnVtYmVyGAQgASgNEg0KBXRpdGxlGAUgASgJEgwKBGJvZHkYBiABKAkSEwoLZm9yZ2Vfc3RhdGUYByABKAkSCwoDdXJsGAggASgJEisKBWFnZW50GAkgASgLMhwuY29tcGFzcy52MS5BZ2VudEF0dHJpYnV0aW9uEhUKDWZvcmdlX2FjY291bnQYCiABKAkSDgoGbGFiZWxzGAsgAygJEi4KCnVwZGF0ZWRfYXQYEyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEiUKBXN0YXRlGAwgASgOMhYuY29tcGFzcy52MS5Jc3N1ZVN0YXRlEhAKCHByaW9yaXR5GA0gASgJEhAKCGFzc2lnbmVlGA4gASgJEg8KB3N1bW1hcnkYDyABKAkSDgoGYnJhbmNoGBAgASgJEiQKA3BycxgRIAMoCzIXLmNvbXBhc3MudjEuUHVsbFJlcXVlc3QSJwoHdHJhY2tlchgSIAEoCzIWLmNvbXBhc3MudjEuVHJhY2tlclJlZiL7AwoLUHVsbFJlcXVlc3QSIwoFZm9yZ2UYASABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAiABKAkSDgoGbnVtYmVyGAMgASgNEg0KBXRpdGxlGAQgASgJEhMKC2ZvcmdlX3N0YXRlGAUgASgJEgsKA3VybBgGIAEoCRIQCghoZWFkX3JlZhgHIAEoCRIQCghiYXNlX3JlZhgIIAEoCRIrCgVhZ2VudBgJIAEoCzIcLmNvbXBhc3MudjEuQWdlbnRBdHRyaWJ1dGlvbhIVCg1mb3JnZV9hY2NvdW50GAogASgJEg0KBWRyYWZ0GAsgASgIEikKB2NoYW5nZWQYDCABKAsyGC5jb21wYXNzLnYxLkNoYW5nZWRTdGF0cxIpCgZjaGVja3MYDSABKAsyGS5jb21wYXNzLnYxLkNoZWNrc1N1bW1hcnkSIwoHcmV2aWV3cxgOIAMoCzISLmNvbXBhc3MudjEuUmV2aWV3EikKB3RocmVhZHMYDyADKAsyGC5jb21wYXNzLnYxLlJldmlld1RocmVhZBIQCghoZWFkX3NoYRgQIAEoCRIXCg9tZXJnZWFibGVfc3RhdGUYESABKAkSMAoLbWVyZ2VfcXVldWUYEiABKAsyGy5jb21wYXNzLnYxLk1lcmdlUXVldWVFbnRyeSJTCg1DaGVja3NTdW1tYXJ5EhAKCGhlYWRfc2hhGAEgASgJEg0KBXN0YXRlGAIgASgJEiEKBmNoZWNrcxgDIAMoCzIRLmNvbXBhc3MudjEuQ2hlY2siQwoFQ2hlY2sSDAoEbmFtZRgBIAEoCRINCgVzdGF0ZRgCIAEoCRILCgN1cmwYAyABKAkSEAoIcmVxdWlyZWQYBCABKAgiQwoMQ2hhbmdlZFN0YXRzEg0KBWZpbGVzGAEgASgNEhEKCWFkZGl0aW9ucxgCIAEoDRIRCglkZWxldGlvbnMYAyABKA0iaQoKVHJhY2tlclJlZhIMCgRraW5kGAEgASgJEgoKAmlkGAIgASgJEg4KBnN0YXR1cxgDIAEoCRILCgN1cmwYBCABKAkSEAoIc3RhdGVfaWQYBSABKAkSEgoKc3RhdGVfdHlwZRgGIAEoCSJaCgZSZXZpZXcSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIPCgd2ZXJkaWN0GAMgASgJEgwKBGJvZHkYBCABKAkSEQoJY29tbWl0X2lkGAUgASgJIlUKDFJldmlld1RocmVhZBIMCgRwYXRoGAEgASgJEhAKCHJlc29sdmVkGAIgASgIEiUKCGNvbW1lbnRzGAMgAygLMhMuY29tcGFzcy52MS5Db21tZW50IjcKB0NvbW1lbnQSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIMCgRib2R5GAMgASgJImIKD01lcmdlUXVldWVFbnRyeRIQCghwcm92aWRlchgBIAEoCRINCgVzdGF0ZRgCIAEoCRIuCgpjaGFuZ2VkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCpkCg5TZWNyZXREZWxpdmVyeRIfChtTRUNSRVRfREVMSVZFUllfVU5TUEVDSUZJRUQQABIYChRTRUNSRVRfREVMSVZFUllfRklMRRABEhcKE1NFQ1JFVF9ERUxJVkVSWV9FTlYQAipwCgpTZWNyZXRLaW5kEhsKF1NFQ1JFVF9LSU5EX1VOU1BFQ0lGSUVEEAASFwoTU0VDUkVUX0tJTkRfR0VORVJJQxABEhgKFFNFQ1JFVF9LSU5EX1BST1ZJREVSEAISEgoOU0VDUkVUX0tJTkRfR0gQAypbCgtTZWNyZXRTY29wZRIcChhTRUNSRVRfU0NPUEVfVU5TUEVDSUZJRUQQABIVChFTRUNSRVRfU0NPUEVfVVNFUhABEhcKE1NFQ1JFVF9TQ09QRV9URU5BTlQQAip9CgtUb3VyT3V0Y29tZRIcChhUT1VSX09VVENPTUVfVU5TUEVDSUZJRUQQABIYChRUT1VSX09VVENPTUVfU1RBUlRFRBABEhoKFlRPVVJfT1VUQ09NRV9ESVNNSVNTRUQQAhIaChZUT1VSX09VVENPTUVfQ09NUExFVEVEEAMqQwoLU2VydmVyU3RhdGUSHAoYU0VSVkVSX1NUQVRFX1VOU1BFQ0lGSUVEEAASFgoSU0VSVkVSX1NUQVRFX1JFQURZEAEqlwEKC1J1bnRpbWVUaWVyEhwKGFJVTlRJTUVfVElFUl9VTlNQRUNJRklFRBAAEhcKE1JVTlRJTUVfVElFUl9QT0RNQU4QARIYChRSVU5USU1FX1RJRVJfTUlDUk9WTRACEiAKHFJVTlRJTUVfVElFUl9BUFBMRV9DT05UQUlORVIQAxIVChFSVU5USU1FX1RJRVJfSE9TVBAEKmgKDUVncmVzc1Bvc3R1cmUSHgoaRUdSRVNTX1BPU1RVUkVfVU5TUEVDSUZJRUQQABIYChRFR1JFU1NfUE9TVFVSRV9BUk1FRBABEh0KGUVHUkVTU19QT1NUVVJFX1VORU5GT1JDRUQQAiqCAgoRQWdlbnRTZXNzaW9uU3RhdGUSIwofQUdFTlRfU0VTU0lPTl9TVEFURV9VTlNQRUNJRklFRBAAEiAKHEFHRU5UX1NFU1NJT05fU1RBVEVfU1RBUlRJTkcQARIdChlBR0VOVF9TRVNTSU9OX1NUQVRFX1JFQURZEAISHwobQUdFTlRfU0VTU0lPTl9TVEFURV9XT1JLSU5HEAMSHwobQUdFTlRfU0VTU0lPTl9TVEFURV9TVE9QUEVEEAQSHwobQUdFTlRfU0VTU0lPTl9TVEFURV9FUlJPUkVEEAUSJAogQUdFTlRfU0VTU0lPTl9TVEFURV9ESVNDT05ORUNURUQQBirSAQoTQWdlbnRUb29sQ2FsbFN0YXR1cxImCiJBR0VOVF9UT09MX0NBTExfU1RBVFVTX1VOU1BFQ0lGSUVEEAASIgoeQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19QRU5ESU5HEAESJgoiQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19JTl9QUk9HUkVTUxACEiQKIEFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfQ09NUExFVEVEEAMSIQodQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19GQUlMRUQQBCq0AQoUQWdlbnRQbGFuRW50cnlTdGF0dXMSJwojQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfVU5TUEVDSUZJRUQQABIjCh9BR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19QRU5ESU5HEAESJwojQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfSU5fUFJPR1JFU1MQAhIlCiFBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19DT01QTEVURUQQAyqEAQoUU2Vzc2lvbkluamVjdGlvbktpbmQSJgoiU0VTU0lPTl9JTkpFQ1RJT05fS0lORF9VTlNQRUNJRklFRBAAEiAKHFNFU1NJT05fSU5KRUNUSU9OX0tJTkRfU1RFRVIQARIiCh5TRVNTSU9OX0lOSkVDVElPTl9LSU5EX0RFTElWRVIQAip0ChBTZXNzaW9uRXJyb3JLaW5kEiIKHlNFU1NJT05fRVJST1JfS0lORF9VTlNQRUNJRklFRBAAEhwKGFNFU1NJT05fRVJST1JfS0lORF9FUlJPUhABEh4KGlNFU1NJT05fRVJST1JfS0lORF9BQk9SVEVEEAIq8QEKCklzc3VlU3RhdGUSGwoXSVNTVUVfU1RBVEVfVU5TUEVDSUZJRUQQABIXChNJU1NVRV9TVEFURV9CQUNLTE9HEAESFAoQSVNTVUVfU1RBVEVfVE9ETxACEhYKEklTU1VFX1NUQVRFX1FVRVVFRBADEhcKE0lTU1VFX1NUQVRFX0JMT0NLRUQQBBIbChdJU1NVRV9TVEFURV9JTl9QUk9HUkVTUxAFEhkKFUlTU1VFX1NUQVRFX0lOX1JFVklFVxAGEhQKEElTU1VFX1NUQVRFX0RPTkUQBxIYChRJU1NVRV9TVEFURV9BUkNISVZFRBAIKpwBCg1Gb3JnZVByb3ZpZGVyEh4KGkZPUkdFX1BST1ZJREVSX1VOU1BFQ0lGSUVEEAASGQoVRk9SR0VfUFJPVklERVJfR0lUSFVCEAESGQoVRk9SR0VfUFJPVklERVJfR0lUTEFCEAISGgoWRk9SR0VfUFJPVklERVJfRk9SR0VKTxADEhkKFUZPUkdFX1BST1ZJREVSX0xJTkVBUhAEMoESCg5Db21wYXNzU2VydmljZRJUCg1HZXRTZXJ2ZXJJbmZvEiAuY29tcGFzcy52MS5HZXRTZXJ2ZXJJbmZvUmVxdWVzdBohLmNvbXBhc3MudjEuR2V0U2VydmVySW5mb1Jlc3BvbnNlEj8KBldob0FtSRIZLmNvbXBhc3MudjEuV2hvQW1JUmVxdWVzdBoaLmNvbXBhc3MudjEuV2hvQW1JUmVzcG9uc2USUQoMR2V0VG91clN0YXRlEh8uY29tcGFzcy52MS5HZXRUb3VyU3RhdGVSZXF1ZXN0GiAuY29tcGFzcy52MS5HZXRUb3VyU3RhdGVSZXNwb25zZRJXCg5DbGFpbVRvdXJTdGFydBIhLmNvbXBhc3MudjEuQ2xhaW1Ub3VyU3RhcnRSZXF1ZXN0GiIuY29tcGFzcy52MS5DbGFpbVRvdXJTdGFydFJlc3BvbnNlElEKDFNldFRvdXJTdGF0ZRIfLmNvbXBhc3MudjEuU2V0VG91clN0YXRlUmVxdWVzdBogLmNvbXBhc3MudjEuU2V0VG91clN0YXRlUmVzcG9uc2USXAoPU3Vic2NyaWJlRXZlbnRzEiIuY29tcGFzcy52MS5TdWJzY3JpYmVFdmVudHNSZXF1ZXN0GiMuY29tcGFzcy52MS5TdWJzY3JpYmVFdmVudHNSZXNwb25zZTABEloKD0xpc3RCb2FyZElzc3VlcxIiLmNvbXBhc3MudjEuTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBojLmNvbXBhc3MudjEuTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USUQoMU2VhcmNoSXNzdWVzEh8uY29tcGFzcy52MS5TZWFyY2hJc3N1ZXNSZXF1ZXN0GiAuY29tcGFzcy52MS5TZWFyY2hJc3N1ZXNSZXNwb25zZRJyChdQcm92aXNpb25BZ2VudFdvcmtzcGFjZRIqLmNvbXBhc3MudjEuUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0GisuY29tcGFzcy52MS5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEmAKEVN0YXJ0QWdlbnRTZXNzaW9uEiQuY29tcGFzcy52MS5TdGFydEFnZW50U2Vzc2lvblJlcXVlc3QaJS5jb21wYXNzLnYxLlN0YXJ0QWdlbnRTZXNzaW9uUmVzcG9uc2USSwoKU3Bhd25BZ2VudBIdLmNvbXBhc3MudjEuU3Bhd25BZ2VudFJlcXVlc3QaHi5jb21wYXNzLnYxLlNwYXduQWdlbnRSZXNwb25zZRJdChBTdG9wQWdlbnRTZXNzaW9uEiMuY29tcGFzcy52MS5TdG9wQWdlbnRTZXNzaW9uUmVxdWVzdBokLmNvbXBhc3MudjEuU3RvcEFnZW50U2Vzc2lvblJlc3BvbnNlEloKD1NraXBCYXRjaFdpbmRvdxIiLmNvbXBhc3MudjEuU2tpcEJhdGNoV2luZG93UmVxdWVzdBojLmNvbXBhc3MudjEuU2tpcEJhdGNoV2luZG93UmVzcG9uc2USaQoUUmVtb3ZlQWdlbnRXb3Jrc3BhY2USJy5jb21wYXNzLnYxLlJlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBooLmNvbXBhc3MudjEuUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJjChJSZWxvYWRBZ2VudFNlc3Npb24SJS5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QaJi5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlc3BvbnNlElcKDkdldEFnZW50U3RhdHVzEiEuY29tcGFzcy52MS5HZXRBZ2VudFN0YXR1c1JlcXVlc3QaIi5jb21wYXNzLnYxLkdldEFnZW50U3RhdHVzUmVzcG9uc2USYgoVU3Vic2NyaWJlQWdlbnRTZXNzaW9uEiguY29tcGFzcy52MS5TdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0Gh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25GcmFtZTABEksKCklzc3VlVG9rZW4SHS5jb21wYXNzLnYxLklzc3VlVG9rZW5SZXF1ZXN0Gh4uY29tcGFzcy52MS5Jc3N1ZVRva2VuUmVzcG9uc2USTgoLUmV2b2tlVG9rZW4SHi5jb21wYXNzLnYxLlJldm9rZVRva2VuUmVxdWVzdBofLmNvbXBhc3MudjEuUmV2b2tlVG9rZW5SZXNwb25zZRJXCg5QdXRBZ2VudENvbmZpZxIhLmNvbXBhc3MudjEuUHV0QWdlbnRDb25maWdSZXF1ZXN0GiIuY29tcGFzcy52MS5QdXRBZ2VudENvbmZpZ1Jlc3BvbnNlEmMKEkdldEFnZW50Q29uZmlnSW5mbxIlLmNvbXBhc3MudjEuR2V0QWdlbnRDb25maWdJbmZvUmVxdWVzdBomLmNvbXBhc3MudjEuR2V0QWdlbnRDb25maWdJbmZvUmVzcG9uc2USYAoRRGVsZXRlQWdlbnRDb25maWcSJC5jb21wYXNzLnYxLkRlbGV0ZUFnZW50Q29uZmlnUmVxdWVzdBolLmNvbXBhc3MudjEuRGVsZXRlQWdlbnRDb25maWdSZXNwb25zZRJdChBQdXRNb2RlbFJlZ2lzdHJ5EiMuY29tcGFzcy52MS5QdXRNb2RlbFJlZ2lzdHJ5UmVxdWVzdBokLmNvbXBhc3MudjEuUHV0TW9kZWxSZWdpc3RyeVJlc3BvbnNlEl0KEEdldE1vZGVsUmVnaXN0cnkSIy5jb21wYXNzLnYxLkdldE1vZGVsUmVnaXN0cnlSZXF1ZXN0GiQuY29tcGFzcy52MS5HZXRNb2RlbFJlZ2lzdHJ5UmVzcG9uc2USZgoTRGVsZXRlTW9kZWxSZWdpc3RyeRImLmNvbXBhc3MudjEuRGVsZXRlTW9kZWxSZWdpc3RyeVJlcXVlc3QaJy5jb21wYXNzLnYxLkRlbGV0ZU1vZGVsUmVnaXN0cnlSZXNwb25zZTLfAgoOU2VjcmV0c1NlcnZpY2USSAoJU2V0U2VjcmV0EhwuY29tcGFzcy52MS5TZXRTZWNyZXRSZXF1ZXN0Gh0uY29tcGFzcy52MS5TZXRTZWNyZXRSZXNwb25zZRJOCgtMaXN0U2VjcmV0cxIeLmNvbXBhc3MudjEuTGlzdFNlY3JldHNSZXF1ZXN0Gh8uY29tcGFzcy52MS5MaXN0U2VjcmV0c1Jlc3BvbnNlElEKDERlbGV0ZVNlY3JldBIfLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVxdWVzdBogLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVzcG9uc2USYAoRTGlzdFNlcnZlclNlY3JldHMSJC5jb21wYXNzLnYxLkxpc3RTZXJ2ZXJTZWNyZXRzUmVxdWVzdBolLmNvbXBhc3MudjEuTGlzdFNlcnZlclNlY3JldHNSZXNwb25zZWIGcHJvdG8z", [file_google_protobuf_timestamp]); + fileDesc("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEi0AEKEFNldFNlY3JldFJlcXVlc3QSDAoEbmFtZRgBIAEoCRISCgV2YWx1ZRgCIAEoCUIDgAEBEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkSJgoFc2NvcGUYByABKA4yFy5jb21wYXNzLnYxLlNlY3JldFNjb3BlIhMKEVNldFNlY3JldFJlc3BvbnNlIhQKEkxpc3RTZWNyZXRzUmVxdWVzdCJAChNMaXN0U2VjcmV0c1Jlc3BvbnNlEikKB3NlY3JldHMYASADKAsyGC5jb21wYXNzLnYxLlNlY3JldFN0YXR1cyKgAQoMU2VjcmV0U3RhdHVzEgwKBG5hbWUYASABKAkSDgoGaXNfc2V0GAIgASgIEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkiSwoTRGVsZXRlU2VjcmV0UmVxdWVzdBIMCgRuYW1lGAEgASgJEiYKBXNjb3BlGAIgASgOMhcuY29tcGFzcy52MS5TZWNyZXRTY29wZSIWChREZWxldGVTZWNyZXRSZXNwb25zZSIaChhMaXN0U2VydmVyU2VjcmV0c1JlcXVlc3QiUwoZTGlzdFNlcnZlclNlY3JldHNSZXNwb25zZRI2Cg5zZXJ2ZXJfc2VjcmV0cxgBIAMoCzIeLmNvbXBhc3MudjEuU2VydmVyU2VjcmV0U3RhdHVzIjIKElNlcnZlclNlY3JldFN0YXR1cxIMCgRuYW1lGAEgASgJEg4KBmlzX3NldBgCIAEoCCIWChRHZXRTZXJ2ZXJJbmZvUmVxdWVzdCKAAQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSEwoLYXBpX3ZlcnNpb24YAiABKAkSCwoDcmV2GAMgASgJEjQKEGVucm9sbGVkX3J1bm5lcnMYBCADKAsyGi5jb21wYXNzLnYxLkVucm9sbGVkUnVubmVyIjAKDkVucm9sbGVkUnVubmVyEgoKAmlkGAEgASgJEhIKCmVucm9sbG1lbnQYAiABKAQiDwoNV2hvQW1JUmVxdWVzdCIkCg5XaG9BbUlSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJIhUKE0dldFRvdXJTdGF0ZVJlcXVlc3QiUQoUR2V0VG91clN0YXRlUmVzcG9uc2USKAoHb3V0Y29tZRgBIAEoDjIXLmNvbXBhc3MudjEuVG91ck91dGNvbWUSDwoHc3RlcF9pZBgCIAEoCSIoChVDbGFpbVRvdXJTdGFydFJlcXVlc3QSDwoHc3RlcF9pZBgBIAEoCSIpChZDbGFpbVRvdXJTdGFydFJlc3BvbnNlEg8KB2NsYWltZWQYASABKAgiUAoTU2V0VG91clN0YXRlUmVxdWVzdBIoCgdvdXRjb21lGAEgASgOMhcuY29tcGFzcy52MS5Ub3VyT3V0Y29tZRIPCgdzdGVwX2lkGAIgASgJIhYKFFNldFRvdXJTdGF0ZVJlc3BvbnNlIkMKFlN1YnNjcmliZUV2ZW50c1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEIuIDChdTdWJzY3JpYmVFdmVudHNSZXNwb25zZRILCgNzZXEYASABKAQSEgoKYXRfdW5peF9tcxgCIAEoAxIWCg5pbnN0YW5jZV9lcG9jaBgDIAEoBBIUCgxzbmFwc2hvdF9zZXEYBCABKAQSMQoNc2VydmVyX3N0YXR1cxgKIAEoCzIYLmNvbXBhc3MudjEuU2VydmVyU3RhdHVzSAASNQoPcmVzeW5jX3JlcXVpcmVkGAsgASgLMhouY29tcGFzcy52MS5SZXN5bmNSZXF1aXJlZEgAEj4KFGFnZW50X3Nlc3Npb25fc3RhdHVzGAwgASgLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXNIABI8ChNhZ2VudF9tZXNzYWdlX2NodW5rGA0gASgLMh0uY29tcGFzcy52MS5BZ2VudE1lc3NhZ2VDaHVua0gAEjQKD2FnZW50X3Rvb2xfY2FsbBgOIAEoCzIZLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbEgAEisKCmFnZW50X3BsYW4YDyABKAsyFS5jb21wYXNzLnYxLkFnZW50UGxhbkgAEiIKBWlzc3VlGBAgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAQgkKB3BheWxvYWQiLgoWTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPAoXTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSJJChNTZWFyY2hJc3N1ZXNSZXF1ZXN0Eg0KBXF1ZXJ5GAEgASgJEg0KBWxpbWl0GAIgASgNEhQKDHNuYXBzaG90X3NlcRgDIAEoBCI5ChRTZWFyY2hJc3N1ZXNSZXNwb25zZRIhCgZpc3N1ZXMYASADKAsyES5jb21wYXNzLnYxLklzc3VlIjYKDFNlcnZlclN0YXR1cxImCgVzdGF0ZRgBIAEoDjIXLmNvbXBhc3MudjEuU2VydmVyU3RhdGUiEAoOUmVzeW5jUmVxdWlyZWQi0gEKEkFnZW50U2Vzc2lvblN0YXR1cxISCgpzZXNzaW9uX2lkGAEgASgJEiwKBXN0YXRlGAIgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZRIYChBhZ2VudF9hY2NvdW50X2lkGAMgASgJEi0KDHJ1bnRpbWVfdGllchgEIAEoDjIXLmNvbXBhc3MudjEuUnVudGltZVRpZXISMQoOZWdyZXNzX3Bvc3R1cmUYBSABKA4yGS5jb21wYXNzLnYxLkVncmVzc1Bvc3R1cmUiSQoRQWdlbnRNZXNzYWdlQ2h1bmsSEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgR0ZXh0GAIgASgJEhIKCmlzX3Rob3VnaHQYAyABKAgieQoNQWdlbnRUb29sQ2FsbBISCgpzZXNzaW9uX2lkGAEgASgJEhQKDHRvb2xfY2FsbF9pZBgCIAEoCRINCgV0aXRsZRgDIAEoCRIvCgZzdGF0dXMYBCABKA4yHy5jb21wYXNzLnYxLkFnZW50VG9vbENhbGxTdGF0dXMiTAoJQWdlbnRQbGFuEhIKCnNlc3Npb25faWQYASABKAkSKwoHZW50cmllcxgCIAMoCzIaLmNvbXBhc3MudjEuQWdlbnRQbGFuRW50cnkiUwoOQWdlbnRQbGFuRW50cnkSDwoHY29udGVudBgBIAEoCRIwCgZzdGF0dXMYAiABKA4yIC5jb21wYXNzLnYxLkFnZW50UGxhbkVudHJ5U3RhdHVzIpkECgxTZXNzaW9uRXZlbnQSEAoIZXZlbnRfaWQYASABKAkSEgoKYXRfdW5peF9tcxgCIAEoAxI6Cg5hc3Npc3RhbnRfdGV4dBgDIAEoCzIgLmNvbXBhc3MudjEuU2Vzc2lvbkFzc2lzdGFudFRleHRIABIvCgh0aGlua2luZxgEIAEoCzIbLmNvbXBhc3MudjEuU2Vzc2lvblRoaW5raW5nSAASMAoJdG9vbF9jYWxsGAUgASgLMhsuY29tcGFzcy52MS5TZXNzaW9uVG9vbENhbGxIABI9ChB0b29sX2NhbGxfdXBkYXRlGAYgASgLMiEuY29tcGFzcy52MS5TZXNzaW9uVG9vbENhbGxVcGRhdGVIABInCgRwbGFuGAcgASgLMhcuY29tcGFzcy52MS5TZXNzaW9uUGxhbkgAEisKBm5vdGljZRgIIAEoCzIZLmNvbXBhc3MudjEuU2Vzc2lvbk5vdGljZUgAEjkKEXNlc3Npb25faW5qZWN0aW9uGAkgASgLMhwuY29tcGFzcy52MS5TZXNzaW9uSW5qZWN0aW9uSAASMQoNc2Vzc2lvbl9lcnJvchgKIAEoCzIYLmNvbXBhc3MudjEuU2Vzc2lvbkVycm9ySAASOAoNYmF0Y2hfcGVuZGluZxgLIAEoCzIfLmNvbXBhc3MudjEuU2Vzc2lvbkJhdGNoUGVuZGluZ0gAQgcKBWV2ZW50IjgKFFNlc3Npb25Bc3Npc3RhbnRUZXh0EgwKBHRleHQYASABKAkSEgoKbWVzc2FnZV9pZBgCIAEoCSIzCg9TZXNzaW9uVGhpbmtpbmcSDAoEdGV4dBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJImcKD1Nlc3Npb25Ub29sQ2FsbBIUCgx0b29sX2NhbGxfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSLwoGc3RhdHVzGAMgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIpoBChVTZXNzaW9uVG9vbENhbGxVcGRhdGUSFAoMdG9vbF9jYWxsX2lkGAEgASgJEi8KBnN0YXR1cxgCIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cxIOCgZvdXRwdXQYAyABKAkSKgoFZGlmZnMYBCADKAsyGy5jb21wYXNzLnYxLlNlc3Npb25GaWxlRGlmZiJVCg9TZXNzaW9uRmlsZURpZmYSDAoEcGF0aBgBIAEoCRIVCghvbGRfdGV4dBgCIAEoCUgAiAEBEhAKCG5ld190ZXh0GAMgASgJQgsKCV9vbGRfdGV4dCI6CgtTZXNzaW9uUGxhbhIrCgdlbnRyaWVzGAEgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSI5Cg1TZXNzaW9uTm90aWNlEgwKBHRleHQYASABKAkSEQoEbGluaxgCIAEoCUgAiAEBQgcKBV9saW5rIoMBChBTZXNzaW9uSW5qZWN0aW9uEjEKB29wX2tpbmQYASABKA4yIC5jb21wYXNzLnYxLlNlc3Npb25JbmplY3Rpb25LaW5kEhIKCm1lc3NhZ2VfaWQYAiABKAkSEwoLZnJvbV9oYW5kbGUYAyABKAkSEwoLdHJhY2VwYXJlbnQYBCABKAkiawoMU2Vzc2lvbkVycm9yEioKBGtpbmQYASABKA4yHC5jb21wYXNzLnYxLlNlc3Npb25FcnJvcktpbmQSDwoHbWVzc2FnZRgCIAEoCRITCgZzdGF0dXMYAyABKAVIAIgBAUIJCgdfc3RhdHVzIj4KE1Nlc3Npb25CYXRjaFBlbmRpbmcSDQoFY291bnQYASABKA0SGAoQZmlyZXNfYXRfdW5peF9tcxgCIAEoAyIyChxTdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkilQEKEUFnZW50U2Vzc2lvbkZyYW1lEhIKCnNlc3Npb25faWQYASABKAkSJwoFZXZlbnQYAiABKAsyGC5jb21wYXNzLnYxLlNlc3Npb25FdmVudBIsCgVzdGF0ZRgDIAEoDjIdLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdGUSFQoNdHVybl9zZXF1ZW5jZRgEIAEoBCJwCh5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJEg8KB3BlcnNvbmEYAyABKAkSDAoEcm9sZRgEIAEoCSI5Ch9Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEhYKDmNvbnRhaW5lcl9uYW1lGAEgASgJIlAKG1JlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgCIAEoCSIeChxSZW1vdmVBZ2VudFdvcmtzcGFjZVJlc3BvbnNlImMKGFN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFyZXN1bWVfc2Vzc2lvbl9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiLwoZU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIloKEVNwYXduQWdlbnRSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiQAoSU3Bhd25BZ2VudFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSFgoOY29udGFpbmVyX25hbWUYAiABKAkiLQoXU3RvcEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIaChhTdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2UiLAoWU2tpcEJhdGNoV2luZG93UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIhkKF1NraXBCYXRjaFdpbmRvd1Jlc3BvbnNlIi8KGVJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIwChpSZWxvYWRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIisKFUdldEFnZW50U3RhdHVzUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJInwKFkdldEFnZW50U3RhdHVzUmVzcG9uc2USMAoIc3RhdHVzZXMYASADKAsyHi5jb21wYXNzLnYxLkFnZW50U2Vzc2lvblN0YXR1cxIcCg9ydW5uZXJfZW5yb2xsZWQYAiABKAhIAIgBAUISChBfcnVubmVyX2Vucm9sbGVkIisKEUlzc3VlVG9rZW5SZXF1ZXN0EhYKDmFjY291bnRfaGFuZGxlGAEgASgJIiMKEklzc3VlVG9rZW5SZXNwb25zZRINCgV0b2tlbhgBIAEoCSJCChRSZW5hbWVBY2NvdW50UmVxdWVzdBIWCg5hY2NvdW50X2hhbmRsZRgBIAEoCRISCgpuZXdfaGFuZGxlGAIgASgJIjsKFVJlbmFtZUFjY291bnRSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJEg4KBmhhbmRsZRgCIAEoCSIoChJSZXZva2VUb2tlblJlcXVlc3QSEgoFdG9rZW4YASABKAlCA4ABASIVChNSZXZva2VUb2tlblJlc3BvbnNlIicKFVB1dEFnZW50Q29uZmlnUmVxdWVzdBIOCgZidW5kbGUYASABKAwiKQoWUHV0QWdlbnRDb25maWdSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJIhsKGUdldEFnZW50Q29uZmlnSW5mb1JlcXVlc3Qi2gEKGkdldEFnZW50Q29uZmlnSW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSDgoGc2tpbGxzGAIgAygJEhIKCmV4dGVuc2lvbnMYAyADKAkSEwoLbWNwX3NlcnZlcnMYBCADKAkSFAoMaGFzX3NldHRpbmdzGAUgASgIEhUKDWhhc19hZ2VudHNfbWQYBiABKAgSDQoFcnVsZXMYByADKAkSEQoJc3ViYWdlbnRzGAggAygJEhIKCmhhc19tb2RlbHMYCSABKAgSDwoHcHJvbXB0cxgKIAMoCSIaChhEZWxldGVBZ2VudENvbmZpZ1JlcXVlc3QiGwoZRGVsZXRlQWdlbnRDb25maWdSZXNwb25zZSI0Cg5Nb2RlbENhbmRpZGF0ZRIQCghwcm92aWRlchgBIAEoCRIQCghtb2RlbF9pZBgCIAEoCSJxCg1Nb2RlbE1ldGFkYXRhEhYKDmNvbnRleHRfd2luZG93GAEgASgDEhwKFGlucHV0X2Nvc3RfbWljcm9fdXNkGAIgASgDEh0KFW91dHB1dF9jb3N0X21pY3JvX3VzZBgDIAEoAxILCgNhcGkYBCABKAkihwEKEk1vZGVsUmVnaXN0cnlFbnRyeRIUCgxkaXNwbGF5X25hbWUYASABKAkSLgoKY2FuZGlkYXRlcxgCIAMoCzIaLmNvbXBhc3MudjEuTW9kZWxDYW5kaWRhdGUSKwoIbWV0YWRhdGEYAyABKAsyGS5jb21wYXNzLnYxLk1vZGVsTWV0YWRhdGEimAEKDU1vZGVsUmVnaXN0cnkSNwoHZW50cmllcxgBIAMoCzImLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeS5FbnRyaWVzRW50cnkaTgoMRW50cmllc0VudHJ5EgsKA2tleRgBIAEoCRItCgV2YWx1ZRgCIAEoCzIeLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeUVudHJ5OgI4ASJgChdQdXRNb2RlbFJlZ2lzdHJ5UmVxdWVzdBIrCghyZWdpc3RyeRgBIAEoCzIZLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeRIYChBleHBlY3RlZF92ZXJzaW9uGAIgASgDIisKGFB1dE1vZGVsUmVnaXN0cnlSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgDIhkKF0dldE1vZGVsUmVnaXN0cnlSZXF1ZXN0IlgKGEdldE1vZGVsUmVnaXN0cnlSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgDEisKCHJlZ2lzdHJ5GAIgASgLMhkuY29tcGFzcy52MS5Nb2RlbFJlZ2lzdHJ5IhwKGkRlbGV0ZU1vZGVsUmVnaXN0cnlSZXF1ZXN0Ih0KG0RlbGV0ZU1vZGVsUmVnaXN0cnlSZXNwb25zZSI+ChBBZ2VudEF0dHJpYnV0aW9uEhQKDGFnZW50X2hhbmRsZRgBIAEoCRIUCgxvd25lcl9oYW5kbGUYAiABKAkiRQoIRm9yZ2VSZWYSKwoIcHJvdmlkZXIYASABKA4yGS5jb21wYXNzLnYxLkZvcmdlUHJvdmlkZXISDAoEaG9zdBgCIAEoCSLUAwoFSXNzdWUSCgoCaWQYASABKAkSIwoFZm9yZ2UYAiABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAyABKAkSDgoGbnVtYmVyGAQgASgNEg0KBXRpdGxlGAUgASgJEgwKBGJvZHkYBiABKAkSEwoLZm9yZ2Vfc3RhdGUYByABKAkSCwoDdXJsGAggASgJEisKBWFnZW50GAkgASgLMhwuY29tcGFzcy52MS5BZ2VudEF0dHJpYnV0aW9uEhUKDWZvcmdlX2FjY291bnQYCiABKAkSDgoGbGFiZWxzGAsgAygJEi4KCnVwZGF0ZWRfYXQYEyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEiUKBXN0YXRlGAwgASgOMhYuY29tcGFzcy52MS5Jc3N1ZVN0YXRlEhAKCHByaW9yaXR5GA0gASgJEhAKCGFzc2lnbmVlGA4gASgJEg8KB3N1bW1hcnkYDyABKAkSDgoGYnJhbmNoGBAgASgJEiQKA3BycxgRIAMoCzIXLmNvbXBhc3MudjEuUHVsbFJlcXVlc3QSJwoHdHJhY2tlchgSIAEoCzIWLmNvbXBhc3MudjEuVHJhY2tlclJlZiL7AwoLUHVsbFJlcXVlc3QSIwoFZm9yZ2UYASABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAiABKAkSDgoGbnVtYmVyGAMgASgNEg0KBXRpdGxlGAQgASgJEhMKC2ZvcmdlX3N0YXRlGAUgASgJEgsKA3VybBgGIAEoCRIQCghoZWFkX3JlZhgHIAEoCRIQCghiYXNlX3JlZhgIIAEoCRIrCgVhZ2VudBgJIAEoCzIcLmNvbXBhc3MudjEuQWdlbnRBdHRyaWJ1dGlvbhIVCg1mb3JnZV9hY2NvdW50GAogASgJEg0KBWRyYWZ0GAsgASgIEikKB2NoYW5nZWQYDCABKAsyGC5jb21wYXNzLnYxLkNoYW5nZWRTdGF0cxIpCgZjaGVja3MYDSABKAsyGS5jb21wYXNzLnYxLkNoZWNrc1N1bW1hcnkSIwoHcmV2aWV3cxgOIAMoCzISLmNvbXBhc3MudjEuUmV2aWV3EikKB3RocmVhZHMYDyADKAsyGC5jb21wYXNzLnYxLlJldmlld1RocmVhZBIQCghoZWFkX3NoYRgQIAEoCRIXCg9tZXJnZWFibGVfc3RhdGUYESABKAkSMAoLbWVyZ2VfcXVldWUYEiABKAsyGy5jb21wYXNzLnYxLk1lcmdlUXVldWVFbnRyeSJTCg1DaGVja3NTdW1tYXJ5EhAKCGhlYWRfc2hhGAEgASgJEg0KBXN0YXRlGAIgASgJEiEKBmNoZWNrcxgDIAMoCzIRLmNvbXBhc3MudjEuQ2hlY2siQwoFQ2hlY2sSDAoEbmFtZRgBIAEoCRINCgVzdGF0ZRgCIAEoCRILCgN1cmwYAyABKAkSEAoIcmVxdWlyZWQYBCABKAgiQwoMQ2hhbmdlZFN0YXRzEg0KBWZpbGVzGAEgASgNEhEKCWFkZGl0aW9ucxgCIAEoDRIRCglkZWxldGlvbnMYAyABKA0iaQoKVHJhY2tlclJlZhIMCgRraW5kGAEgASgJEgoKAmlkGAIgASgJEg4KBnN0YXR1cxgDIAEoCRILCgN1cmwYBCABKAkSEAoIc3RhdGVfaWQYBSABKAkSEgoKc3RhdGVfdHlwZRgGIAEoCSJaCgZSZXZpZXcSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIPCgd2ZXJkaWN0GAMgASgJEgwKBGJvZHkYBCABKAkSEQoJY29tbWl0X2lkGAUgASgJIlUKDFJldmlld1RocmVhZBIMCgRwYXRoGAEgASgJEhAKCHJlc29sdmVkGAIgASgIEiUKCGNvbW1lbnRzGAMgAygLMhMuY29tcGFzcy52MS5Db21tZW50IjcKB0NvbW1lbnQSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIMCgRib2R5GAMgASgJImIKD01lcmdlUXVldWVFbnRyeRIQCghwcm92aWRlchgBIAEoCRINCgVzdGF0ZRgCIAEoCRIuCgpjaGFuZ2VkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCpkCg5TZWNyZXREZWxpdmVyeRIfChtTRUNSRVRfREVMSVZFUllfVU5TUEVDSUZJRUQQABIYChRTRUNSRVRfREVMSVZFUllfRklMRRABEhcKE1NFQ1JFVF9ERUxJVkVSWV9FTlYQAipwCgpTZWNyZXRLaW5kEhsKF1NFQ1JFVF9LSU5EX1VOU1BFQ0lGSUVEEAASFwoTU0VDUkVUX0tJTkRfR0VORVJJQxABEhgKFFNFQ1JFVF9LSU5EX1BST1ZJREVSEAISEgoOU0VDUkVUX0tJTkRfR0gQAypbCgtTZWNyZXRTY29wZRIcChhTRUNSRVRfU0NPUEVfVU5TUEVDSUZJRUQQABIVChFTRUNSRVRfU0NPUEVfVVNFUhABEhcKE1NFQ1JFVF9TQ09QRV9URU5BTlQQAip9CgtUb3VyT3V0Y29tZRIcChhUT1VSX09VVENPTUVfVU5TUEVDSUZJRUQQABIYChRUT1VSX09VVENPTUVfU1RBUlRFRBABEhoKFlRPVVJfT1VUQ09NRV9ESVNNSVNTRUQQAhIaChZUT1VSX09VVENPTUVfQ09NUExFVEVEEAMqQwoLU2VydmVyU3RhdGUSHAoYU0VSVkVSX1NUQVRFX1VOU1BFQ0lGSUVEEAASFgoSU0VSVkVSX1NUQVRFX1JFQURZEAEqlwEKC1J1bnRpbWVUaWVyEhwKGFJVTlRJTUVfVElFUl9VTlNQRUNJRklFRBAAEhcKE1JVTlRJTUVfVElFUl9QT0RNQU4QARIYChRSVU5USU1FX1RJRVJfTUlDUk9WTRACEiAKHFJVTlRJTUVfVElFUl9BUFBMRV9DT05UQUlORVIQAxIVChFSVU5USU1FX1RJRVJfSE9TVBAEKmgKDUVncmVzc1Bvc3R1cmUSHgoaRUdSRVNTX1BPU1RVUkVfVU5TUEVDSUZJRUQQABIYChRFR1JFU1NfUE9TVFVSRV9BUk1FRBABEh0KGUVHUkVTU19QT1NUVVJFX1VORU5GT1JDRUQQAiqCAgoRQWdlbnRTZXNzaW9uU3RhdGUSIwofQUdFTlRfU0VTU0lPTl9TVEFURV9VTlNQRUNJRklFRBAAEiAKHEFHRU5UX1NFU1NJT05fU1RBVEVfU1RBUlRJTkcQARIdChlBR0VOVF9TRVNTSU9OX1NUQVRFX1JFQURZEAISHwobQUdFTlRfU0VTU0lPTl9TVEFURV9XT1JLSU5HEAMSHwobQUdFTlRfU0VTU0lPTl9TVEFURV9TVE9QUEVEEAQSHwobQUdFTlRfU0VTU0lPTl9TVEFURV9FUlJPUkVEEAUSJAogQUdFTlRfU0VTU0lPTl9TVEFURV9ESVNDT05ORUNURUQQBirSAQoTQWdlbnRUb29sQ2FsbFN0YXR1cxImCiJBR0VOVF9UT09MX0NBTExfU1RBVFVTX1VOU1BFQ0lGSUVEEAASIgoeQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19QRU5ESU5HEAESJgoiQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19JTl9QUk9HUkVTUxACEiQKIEFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfQ09NUExFVEVEEAMSIQodQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19GQUlMRUQQBCq0AQoUQWdlbnRQbGFuRW50cnlTdGF0dXMSJwojQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfVU5TUEVDSUZJRUQQABIjCh9BR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19QRU5ESU5HEAESJwojQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfSU5fUFJPR1JFU1MQAhIlCiFBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19DT01QTEVURUQQAyqEAQoUU2Vzc2lvbkluamVjdGlvbktpbmQSJgoiU0VTU0lPTl9JTkpFQ1RJT05fS0lORF9VTlNQRUNJRklFRBAAEiAKHFNFU1NJT05fSU5KRUNUSU9OX0tJTkRfU1RFRVIQARIiCh5TRVNTSU9OX0lOSkVDVElPTl9LSU5EX0RFTElWRVIQAip0ChBTZXNzaW9uRXJyb3JLaW5kEiIKHlNFU1NJT05fRVJST1JfS0lORF9VTlNQRUNJRklFRBAAEhwKGFNFU1NJT05fRVJST1JfS0lORF9FUlJPUhABEh4KGlNFU1NJT05fRVJST1JfS0lORF9BQk9SVEVEEAIq8QEKCklzc3VlU3RhdGUSGwoXSVNTVUVfU1RBVEVfVU5TUEVDSUZJRUQQABIXChNJU1NVRV9TVEFURV9CQUNLTE9HEAESFAoQSVNTVUVfU1RBVEVfVE9ETxACEhYKEklTU1VFX1NUQVRFX1FVRVVFRBADEhcKE0lTU1VFX1NUQVRFX0JMT0NLRUQQBBIbChdJU1NVRV9TVEFURV9JTl9QUk9HUkVTUxAFEhkKFUlTU1VFX1NUQVRFX0lOX1JFVklFVxAGEhQKEElTU1VFX1NUQVRFX0RPTkUQBxIYChRJU1NVRV9TVEFURV9BUkNISVZFRBAIKpwBCg1Gb3JnZVByb3ZpZGVyEh4KGkZPUkdFX1BST1ZJREVSX1VOU1BFQ0lGSUVEEAASGQoVRk9SR0VfUFJPVklERVJfR0lUSFVCEAESGQoVRk9SR0VfUFJPVklERVJfR0lUTEFCEAISGgoWRk9SR0VfUFJPVklERVJfRk9SR0VKTxADEhkKFUZPUkdFX1BST1ZJREVSX0xJTkVBUhAEMtcSCg5Db21wYXNzU2VydmljZRJUCg1HZXRTZXJ2ZXJJbmZvEiAuY29tcGFzcy52MS5HZXRTZXJ2ZXJJbmZvUmVxdWVzdBohLmNvbXBhc3MudjEuR2V0U2VydmVySW5mb1Jlc3BvbnNlEj8KBldob0FtSRIZLmNvbXBhc3MudjEuV2hvQW1JUmVxdWVzdBoaLmNvbXBhc3MudjEuV2hvQW1JUmVzcG9uc2USUQoMR2V0VG91clN0YXRlEh8uY29tcGFzcy52MS5HZXRUb3VyU3RhdGVSZXF1ZXN0GiAuY29tcGFzcy52MS5HZXRUb3VyU3RhdGVSZXNwb25zZRJXCg5DbGFpbVRvdXJTdGFydBIhLmNvbXBhc3MudjEuQ2xhaW1Ub3VyU3RhcnRSZXF1ZXN0GiIuY29tcGFzcy52MS5DbGFpbVRvdXJTdGFydFJlc3BvbnNlElEKDFNldFRvdXJTdGF0ZRIfLmNvbXBhc3MudjEuU2V0VG91clN0YXRlUmVxdWVzdBogLmNvbXBhc3MudjEuU2V0VG91clN0YXRlUmVzcG9uc2USXAoPU3Vic2NyaWJlRXZlbnRzEiIuY29tcGFzcy52MS5TdWJzY3JpYmVFdmVudHNSZXF1ZXN0GiMuY29tcGFzcy52MS5TdWJzY3JpYmVFdmVudHNSZXNwb25zZTABEloKD0xpc3RCb2FyZElzc3VlcxIiLmNvbXBhc3MudjEuTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBojLmNvbXBhc3MudjEuTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USUQoMU2VhcmNoSXNzdWVzEh8uY29tcGFzcy52MS5TZWFyY2hJc3N1ZXNSZXF1ZXN0GiAuY29tcGFzcy52MS5TZWFyY2hJc3N1ZXNSZXNwb25zZRJyChdQcm92aXNpb25BZ2VudFdvcmtzcGFjZRIqLmNvbXBhc3MudjEuUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0GisuY29tcGFzcy52MS5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEmAKEVN0YXJ0QWdlbnRTZXNzaW9uEiQuY29tcGFzcy52MS5TdGFydEFnZW50U2Vzc2lvblJlcXVlc3QaJS5jb21wYXNzLnYxLlN0YXJ0QWdlbnRTZXNzaW9uUmVzcG9uc2USSwoKU3Bhd25BZ2VudBIdLmNvbXBhc3MudjEuU3Bhd25BZ2VudFJlcXVlc3QaHi5jb21wYXNzLnYxLlNwYXduQWdlbnRSZXNwb25zZRJdChBTdG9wQWdlbnRTZXNzaW9uEiMuY29tcGFzcy52MS5TdG9wQWdlbnRTZXNzaW9uUmVxdWVzdBokLmNvbXBhc3MudjEuU3RvcEFnZW50U2Vzc2lvblJlc3BvbnNlEloKD1NraXBCYXRjaFdpbmRvdxIiLmNvbXBhc3MudjEuU2tpcEJhdGNoV2luZG93UmVxdWVzdBojLmNvbXBhc3MudjEuU2tpcEJhdGNoV2luZG93UmVzcG9uc2USaQoUUmVtb3ZlQWdlbnRXb3Jrc3BhY2USJy5jb21wYXNzLnYxLlJlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBooLmNvbXBhc3MudjEuUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJjChJSZWxvYWRBZ2VudFNlc3Npb24SJS5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QaJi5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlc3BvbnNlElcKDkdldEFnZW50U3RhdHVzEiEuY29tcGFzcy52MS5HZXRBZ2VudFN0YXR1c1JlcXVlc3QaIi5jb21wYXNzLnYxLkdldEFnZW50U3RhdHVzUmVzcG9uc2USYgoVU3Vic2NyaWJlQWdlbnRTZXNzaW9uEiguY29tcGFzcy52MS5TdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0Gh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25GcmFtZTABEksKCklzc3VlVG9rZW4SHS5jb21wYXNzLnYxLklzc3VlVG9rZW5SZXF1ZXN0Gh4uY29tcGFzcy52MS5Jc3N1ZVRva2VuUmVzcG9uc2USVAoNUmVuYW1lQWNjb3VudBIgLmNvbXBhc3MudjEuUmVuYW1lQWNjb3VudFJlcXVlc3QaIS5jb21wYXNzLnYxLlJlbmFtZUFjY291bnRSZXNwb25zZRJOCgtSZXZva2VUb2tlbhIeLmNvbXBhc3MudjEuUmV2b2tlVG9rZW5SZXF1ZXN0Gh8uY29tcGFzcy52MS5SZXZva2VUb2tlblJlc3BvbnNlElcKDlB1dEFnZW50Q29uZmlnEiEuY29tcGFzcy52MS5QdXRBZ2VudENvbmZpZ1JlcXVlc3QaIi5jb21wYXNzLnYxLlB1dEFnZW50Q29uZmlnUmVzcG9uc2USYwoSR2V0QWdlbnRDb25maWdJbmZvEiUuY29tcGFzcy52MS5HZXRBZ2VudENvbmZpZ0luZm9SZXF1ZXN0GiYuY29tcGFzcy52MS5HZXRBZ2VudENvbmZpZ0luZm9SZXNwb25zZRJgChFEZWxldGVBZ2VudENvbmZpZxIkLmNvbXBhc3MudjEuRGVsZXRlQWdlbnRDb25maWdSZXF1ZXN0GiUuY29tcGFzcy52MS5EZWxldGVBZ2VudENvbmZpZ1Jlc3BvbnNlEl0KEFB1dE1vZGVsUmVnaXN0cnkSIy5jb21wYXNzLnYxLlB1dE1vZGVsUmVnaXN0cnlSZXF1ZXN0GiQuY29tcGFzcy52MS5QdXRNb2RlbFJlZ2lzdHJ5UmVzcG9uc2USXQoQR2V0TW9kZWxSZWdpc3RyeRIjLmNvbXBhc3MudjEuR2V0TW9kZWxSZWdpc3RyeVJlcXVlc3QaJC5jb21wYXNzLnYxLkdldE1vZGVsUmVnaXN0cnlSZXNwb25zZRJmChNEZWxldGVNb2RlbFJlZ2lzdHJ5EiYuY29tcGFzcy52MS5EZWxldGVNb2RlbFJlZ2lzdHJ5UmVxdWVzdBonLmNvbXBhc3MudjEuRGVsZXRlTW9kZWxSZWdpc3RyeVJlc3BvbnNlMt8CCg5TZWNyZXRzU2VydmljZRJICglTZXRTZWNyZXQSHC5jb21wYXNzLnYxLlNldFNlY3JldFJlcXVlc3QaHS5jb21wYXNzLnYxLlNldFNlY3JldFJlc3BvbnNlEk4KC0xpc3RTZWNyZXRzEh4uY29tcGFzcy52MS5MaXN0U2VjcmV0c1JlcXVlc3QaHy5jb21wYXNzLnYxLkxpc3RTZWNyZXRzUmVzcG9uc2USUQoMRGVsZXRlU2VjcmV0Eh8uY29tcGFzcy52MS5EZWxldGVTZWNyZXRSZXF1ZXN0GiAuY29tcGFzcy52MS5EZWxldGVTZWNyZXRSZXNwb25zZRJgChFMaXN0U2VydmVyU2VjcmV0cxIkLmNvbXBhc3MudjEuTGlzdFNlcnZlclNlY3JldHNSZXF1ZXN0GiUuY29tcGFzcy52MS5MaXN0U2VydmVyU2VjcmV0c1Jlc3BvbnNlYgZwcm90bzM", [file_google_protobuf_timestamp]); /** * @generated from message compass.v1.SetSecretRequest @@ -1875,6 +1875,61 @@ export type IssueTokenResponse = Message<"compass.v1.IssueTokenResponse"> & { export const IssueTokenResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_compass_v1_compass, 64); +/** + * RenameAccount: the admin-only path to rename a user or agent account. + * + * @generated from message compass.v1.RenameAccountRequest + */ +export type RenameAccountRequest = Message<"compass.v1.RenameAccountRequest"> & { + /** + * Current bare user handle or `owner/agent` handle. Empty is INVALID_ARGUMENT; + * bad qualified grammar is INVALID_ARGUMENT, unknown handles are NOT_FOUND, and + * the system account is PERMISSION_DENIED. + * + * @generated from field: string account_handle = 1; + */ + accountHandle: string; + + /** + * New bare handle; an agent keeps its current owner. Empty, bad grammar, or a + * qualified handle is INVALID_ARGUMENT; a taken handle is ALREADY_EXISTS. + * + * @generated from field: string new_handle = 2; + */ + newHandle: string; +}; + +/** + * Describes the message compass.v1.RenameAccountRequest. + * Use `create(RenameAccountRequestSchema)` to create a new message. + */ +export const RenameAccountRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 65); + +/** + * @generated from message compass.v1.RenameAccountResponse + */ +export type RenameAccountResponse = Message<"compass.v1.RenameAccountResponse"> & { + /** + * @generated from field: string account_id = 1; + */ + accountId: string; + + /** + * The new address: bare for users, `owner/agent` for agents. + * + * @generated from field: string handle = 2; + */ + handle: string; +}; + +/** + * Describes the message compass.v1.RenameAccountResponse. + * Use `create(RenameAccountResponseSchema)` to create a new message. + */ +export const RenameAccountResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 66); + /** * RevokeToken: the admin-only path to withdraw a bearer token by its value. * @@ -1897,7 +1952,7 @@ export type RevokeTokenRequest = Message<"compass.v1.RevokeTokenRequest"> & { * Use `create(RevokeTokenRequestSchema)` to create a new message. */ export const RevokeTokenRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 65); + messageDesc(file_compass_v1_compass, 67); /** * @generated from message compass.v1.RevokeTokenResponse @@ -1910,7 +1965,7 @@ export type RevokeTokenResponse = Message<"compass.v1.RevokeTokenResponse"> & { * Use `create(RevokeTokenResponseSchema)` to create a new message. */ export const RevokeTokenResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 66); + messageDesc(file_compass_v1_compass, 68); /** * PutAgentConfig: declare the fleet config bundle. The caller's identity is the @@ -1933,7 +1988,7 @@ export type PutAgentConfigRequest = Message<"compass.v1.PutAgentConfigRequest"> * Use `create(PutAgentConfigRequestSchema)` to create a new message. */ export const PutAgentConfigRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 67); + messageDesc(file_compass_v1_compass, 69); /** * @generated from message compass.v1.PutAgentConfigResponse @@ -1954,7 +2009,7 @@ export type PutAgentConfigResponse = Message<"compass.v1.PutAgentConfigResponse" * Use `create(PutAgentConfigResponseSchema)` to create a new message. */ export const PutAgentConfigResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 68); + messageDesc(file_compass_v1_compass, 70); /** * @generated from message compass.v1.GetAgentConfigInfoRequest @@ -1967,7 +2022,7 @@ export type GetAgentConfigInfoRequest = Message<"compass.v1.GetAgentConfigInfoRe * Use `create(GetAgentConfigInfoRequestSchema)` to create a new message. */ export const GetAgentConfigInfoRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 69); + messageDesc(file_compass_v1_compass, 71); /** * GetAgentConfigInfo: the current bundle's version and member names by top dir — @@ -2054,7 +2109,7 @@ export type GetAgentConfigInfoResponse = Message<"compass.v1.GetAgentConfigInfoR * Use `create(GetAgentConfigInfoResponseSchema)` to create a new message. */ export const GetAgentConfigInfoResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 70); + messageDesc(file_compass_v1_compass, 72); /** * @generated from message compass.v1.DeleteAgentConfigRequest @@ -2067,7 +2122,7 @@ export type DeleteAgentConfigRequest = Message<"compass.v1.DeleteAgentConfigRequ * Use `create(DeleteAgentConfigRequestSchema)` to create a new message. */ export const DeleteAgentConfigRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 71); + messageDesc(file_compass_v1_compass, 73); /** * @generated from message compass.v1.DeleteAgentConfigResponse @@ -2080,7 +2135,7 @@ export type DeleteAgentConfigResponse = Message<"compass.v1.DeleteAgentConfigRes * Use `create(DeleteAgentConfigResponseSchema)` to create a new message. */ export const DeleteAgentConfigResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 72); + messageDesc(file_compass_v1_compass, 74); /** * One candidate in a stable name's ordered chain: an upstream (provider, @@ -2108,7 +2163,7 @@ export type ModelCandidate = Message<"compass.v1.ModelCandidate"> & { * Use `create(ModelCandidateSchema)` to create a new message. */ export const ModelCandidateSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 73); + messageDesc(file_compass_v1_compass, 75); /** * The listing metadata a stable name carries, taken from its primary candidate @@ -2152,7 +2207,7 @@ export type ModelMetadata = Message<"compass.v1.ModelMetadata"> & { * Use `create(ModelMetadataSchema)` to create a new message. */ export const ModelMetadataSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 74); + messageDesc(file_compass_v1_compass, 76); /** * One stable name's registry entry: a human display name, the ordered candidate @@ -2182,7 +2237,7 @@ export type ModelRegistryEntry = Message<"compass.v1.ModelRegistryEntry"> & { * Use `create(ModelRegistryEntrySchema)` to create a new message. */ export const ModelRegistryEntrySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 75); + messageDesc(file_compass_v1_compass, 77); /** * The fleet model registry payload: the stable-name → entry map. The map key is @@ -2202,7 +2257,7 @@ export type ModelRegistry = Message<"compass.v1.ModelRegistry"> & { * Use `create(ModelRegistrySchema)` to create a new message. */ export const ModelRegistrySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 76); + messageDesc(file_compass_v1_compass, 78); /** * PutModelRegistry: declare the fleet model registry. The caller's identity is @@ -2233,7 +2288,7 @@ export type PutModelRegistryRequest = Message<"compass.v1.PutModelRegistryReques * Use `create(PutModelRegistryRequestSchema)` to create a new message. */ export const PutModelRegistryRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 77); + messageDesc(file_compass_v1_compass, 79); /** * @generated from message compass.v1.PutModelRegistryResponse @@ -2252,7 +2307,7 @@ export type PutModelRegistryResponse = Message<"compass.v1.PutModelRegistryRespo * Use `create(PutModelRegistryResponseSchema)` to create a new message. */ export const PutModelRegistryResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 78); + messageDesc(file_compass_v1_compass, 80); /** * @generated from message compass.v1.GetModelRegistryRequest @@ -2265,7 +2320,7 @@ export type GetModelRegistryRequest = Message<"compass.v1.GetModelRegistryReques * Use `create(GetModelRegistryRequestSchema)` to create a new message. */ export const GetModelRegistryRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 79); + messageDesc(file_compass_v1_compass, 81); /** * GetModelRegistry: the current registry version and payload. An unconfigured @@ -2290,7 +2345,7 @@ export type GetModelRegistryResponse = Message<"compass.v1.GetModelRegistryRespo * Use `create(GetModelRegistryResponseSchema)` to create a new message. */ export const GetModelRegistryResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 80); + messageDesc(file_compass_v1_compass, 82); /** * @generated from message compass.v1.DeleteModelRegistryRequest @@ -2303,7 +2358,7 @@ export type DeleteModelRegistryRequest = Message<"compass.v1.DeleteModelRegistry * Use `create(DeleteModelRegistryRequestSchema)` to create a new message. */ export const DeleteModelRegistryRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 81); + messageDesc(file_compass_v1_compass, 83); /** * @generated from message compass.v1.DeleteModelRegistryResponse @@ -2316,7 +2371,7 @@ export type DeleteModelRegistryResponse = Message<"compass.v1.DeleteModelRegistr * Use `create(DeleteModelRegistryResponseSchema)` to create a new message. */ export const DeleteModelRegistryResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 82); + messageDesc(file_compass_v1_compass, 84); /** * The Compass agent attribution parsed from the owner header at ingestion — a @@ -2349,7 +2404,7 @@ export type AgentAttribution = Message<"compass.v1.AgentAttribution"> & { * Use `create(AgentAttributionSchema)` to create a new message. */ export const AgentAttributionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 83); + messageDesc(file_compass_v1_compass, 85); /** * @generated from message compass.v1.ForgeRef @@ -2373,7 +2428,7 @@ export type ForgeRef = Message<"compass.v1.ForgeRef"> & { * Use `create(ForgeRefSchema)` to create a new message. */ export const ForgeRefSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 84); + messageDesc(file_compass_v1_compass, 86); /** * The board unit: a Compass Issue — the forge issue's fields PLUS the Compass @@ -2531,7 +2586,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, 85); + messageDesc(file_compass_v1_compass, 87); /** * A Compass pull request: the forge PR's fields plus the Compass agent @@ -2659,7 +2714,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, 86); + messageDesc(file_compass_v1_compass, 88); /** * The rolled-up CI + status-check state on a PR head — Compass-owned, populated @@ -2691,7 +2746,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, 87); + messageDesc(file_compass_v1_compass, 89); /** * @generated from message compass.v1.Check @@ -2725,7 +2780,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, 88); + messageDesc(file_compass_v1_compass, 90); /** * A PR diffstat (files/additions/deletions), carried on PullRequest — a @@ -2755,7 +2810,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, 89); + messageDesc(file_compass_v1_compass, 91); /** * The linked tracker issue — the projection target (DL-032); the @@ -2810,7 +2865,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, 90); + messageDesc(file_compass_v1_compass, 92); /** * The full review state the right-sidebar PR pane shows — every submitted review @@ -2864,7 +2919,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, 91); + messageDesc(file_compass_v1_compass, 93); /** * @generated from message compass.v1.ReviewThread @@ -2893,7 +2948,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, 92); + messageDesc(file_compass_v1_compass, 94); /** * @generated from message compass.v1.Comment @@ -2920,7 +2975,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, 93); + messageDesc(file_compass_v1_compass, 95); /** * The observed state of a pull request in its merge queue. @@ -2955,7 +3010,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, 94); + messageDesc(file_compass_v1_compass, 96); /** * How a resolved secret is delivered into the agent container. Mirrors @@ -3743,6 +3798,18 @@ export const CompassService: GenService<{ input: typeof IssueTokenRequestSchema; output: typeof IssueTokenResponseSchema; }, + /** + * Admin-gated: only an admin may rename an account; non-admin callers get + * permission_denied. Freed handles may be reclaimed; the owner of an agent + * does not change. + * + * @generated from rpc compass.v1.CompassService.RenameAccount + */ + renameAccount: { + methodKind: "unary"; + input: typeof RenameAccountRequestSchema; + output: typeof RenameAccountResponseSchema; + }, /** * Admin-gated: only an admin may revoke a token; a non-admin caller gets * permission_denied. Revocation is BY TOKEN VALUE (per-hash), not by 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 617787dd6..7a6297d40 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("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEi0AEKEFNldFNlY3JldFJlcXVlc3QSDAoEbmFtZRgBIAEoCRISCgV2YWx1ZRgCIAEoCUIDgAEBEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkSJgoFc2NvcGUYByABKA4yFy5jb21wYXNzLnYxLlNlY3JldFNjb3BlIhMKEVNldFNlY3JldFJlc3BvbnNlIhQKEkxpc3RTZWNyZXRzUmVxdWVzdCJAChNMaXN0U2VjcmV0c1Jlc3BvbnNlEikKB3NlY3JldHMYASADKAsyGC5jb21wYXNzLnYxLlNlY3JldFN0YXR1cyKgAQoMU2VjcmV0U3RhdHVzEgwKBG5hbWUYASABKAkSDgoGaXNfc2V0GAIgASgIEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkiSwoTRGVsZXRlU2VjcmV0UmVxdWVzdBIMCgRuYW1lGAEgASgJEiYKBXNjb3BlGAIgASgOMhcuY29tcGFzcy52MS5TZWNyZXRTY29wZSIWChREZWxldGVTZWNyZXRSZXNwb25zZSIaChhMaXN0U2VydmVyU2VjcmV0c1JlcXVlc3QiUwoZTGlzdFNlcnZlclNlY3JldHNSZXNwb25zZRI2Cg5zZXJ2ZXJfc2VjcmV0cxgBIAMoCzIeLmNvbXBhc3MudjEuU2VydmVyU2VjcmV0U3RhdHVzIjIKElNlcnZlclNlY3JldFN0YXR1cxIMCgRuYW1lGAEgASgJEg4KBmlzX3NldBgCIAEoCCIWChRHZXRTZXJ2ZXJJbmZvUmVxdWVzdCKAAQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSEwoLYXBpX3ZlcnNpb24YAiABKAkSCwoDcmV2GAMgASgJEjQKEGVucm9sbGVkX3J1bm5lcnMYBCADKAsyGi5jb21wYXNzLnYxLkVucm9sbGVkUnVubmVyIjAKDkVucm9sbGVkUnVubmVyEgoKAmlkGAEgASgJEhIKCmVucm9sbG1lbnQYAiABKAQiDwoNV2hvQW1JUmVxdWVzdCIkCg5XaG9BbUlSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJIhUKE0dldFRvdXJTdGF0ZVJlcXVlc3QiUQoUR2V0VG91clN0YXRlUmVzcG9uc2USKAoHb3V0Y29tZRgBIAEoDjIXLmNvbXBhc3MudjEuVG91ck91dGNvbWUSDwoHc3RlcF9pZBgCIAEoCSIoChVDbGFpbVRvdXJTdGFydFJlcXVlc3QSDwoHc3RlcF9pZBgBIAEoCSIpChZDbGFpbVRvdXJTdGFydFJlc3BvbnNlEg8KB2NsYWltZWQYASABKAgiUAoTU2V0VG91clN0YXRlUmVxdWVzdBIoCgdvdXRjb21lGAEgASgOMhcuY29tcGFzcy52MS5Ub3VyT3V0Y29tZRIPCgdzdGVwX2lkGAIgASgJIhYKFFNldFRvdXJTdGF0ZVJlc3BvbnNlIkMKFlN1YnNjcmliZUV2ZW50c1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEIuIDChdTdWJzY3JpYmVFdmVudHNSZXNwb25zZRILCgNzZXEYASABKAQSEgoKYXRfdW5peF9tcxgCIAEoAxIWCg5pbnN0YW5jZV9lcG9jaBgDIAEoBBIUCgxzbmFwc2hvdF9zZXEYBCABKAQSMQoNc2VydmVyX3N0YXR1cxgKIAEoCzIYLmNvbXBhc3MudjEuU2VydmVyU3RhdHVzSAASNQoPcmVzeW5jX3JlcXVpcmVkGAsgASgLMhouY29tcGFzcy52MS5SZXN5bmNSZXF1aXJlZEgAEj4KFGFnZW50X3Nlc3Npb25fc3RhdHVzGAwgASgLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXNIABI8ChNhZ2VudF9tZXNzYWdlX2NodW5rGA0gASgLMh0uY29tcGFzcy52MS5BZ2VudE1lc3NhZ2VDaHVua0gAEjQKD2FnZW50X3Rvb2xfY2FsbBgOIAEoCzIZLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbEgAEisKCmFnZW50X3BsYW4YDyABKAsyFS5jb21wYXNzLnYxLkFnZW50UGxhbkgAEiIKBWlzc3VlGBAgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAQgkKB3BheWxvYWQiLgoWTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPAoXTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSJJChNTZWFyY2hJc3N1ZXNSZXF1ZXN0Eg0KBXF1ZXJ5GAEgASgJEg0KBWxpbWl0GAIgASgNEhQKDHNuYXBzaG90X3NlcRgDIAEoBCI5ChRTZWFyY2hJc3N1ZXNSZXNwb25zZRIhCgZpc3N1ZXMYASADKAsyES5jb21wYXNzLnYxLklzc3VlIjYKDFNlcnZlclN0YXR1cxImCgVzdGF0ZRgBIAEoDjIXLmNvbXBhc3MudjEuU2VydmVyU3RhdGUiEAoOUmVzeW5jUmVxdWlyZWQi0gEKEkFnZW50U2Vzc2lvblN0YXR1cxISCgpzZXNzaW9uX2lkGAEgASgJEiwKBXN0YXRlGAIgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZRIYChBhZ2VudF9hY2NvdW50X2lkGAMgASgJEi0KDHJ1bnRpbWVfdGllchgEIAEoDjIXLmNvbXBhc3MudjEuUnVudGltZVRpZXISMQoOZWdyZXNzX3Bvc3R1cmUYBSABKA4yGS5jb21wYXNzLnYxLkVncmVzc1Bvc3R1cmUiSQoRQWdlbnRNZXNzYWdlQ2h1bmsSEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgR0ZXh0GAIgASgJEhIKCmlzX3Rob3VnaHQYAyABKAgieQoNQWdlbnRUb29sQ2FsbBISCgpzZXNzaW9uX2lkGAEgASgJEhQKDHRvb2xfY2FsbF9pZBgCIAEoCRINCgV0aXRsZRgDIAEoCRIvCgZzdGF0dXMYBCABKA4yHy5jb21wYXNzLnYxLkFnZW50VG9vbENhbGxTdGF0dXMiTAoJQWdlbnRQbGFuEhIKCnNlc3Npb25faWQYASABKAkSKwoHZW50cmllcxgCIAMoCzIaLmNvbXBhc3MudjEuQWdlbnRQbGFuRW50cnkiUwoOQWdlbnRQbGFuRW50cnkSDwoHY29udGVudBgBIAEoCRIwCgZzdGF0dXMYAiABKA4yIC5jb21wYXNzLnYxLkFnZW50UGxhbkVudHJ5U3RhdHVzIpkECgxTZXNzaW9uRXZlbnQSEAoIZXZlbnRfaWQYASABKAkSEgoKYXRfdW5peF9tcxgCIAEoAxI6Cg5hc3Npc3RhbnRfdGV4dBgDIAEoCzIgLmNvbXBhc3MudjEuU2Vzc2lvbkFzc2lzdGFudFRleHRIABIvCgh0aGlua2luZxgEIAEoCzIbLmNvbXBhc3MudjEuU2Vzc2lvblRoaW5raW5nSAASMAoJdG9vbF9jYWxsGAUgASgLMhsuY29tcGFzcy52MS5TZXNzaW9uVG9vbENhbGxIABI9ChB0b29sX2NhbGxfdXBkYXRlGAYgASgLMiEuY29tcGFzcy52MS5TZXNzaW9uVG9vbENhbGxVcGRhdGVIABInCgRwbGFuGAcgASgLMhcuY29tcGFzcy52MS5TZXNzaW9uUGxhbkgAEisKBm5vdGljZRgIIAEoCzIZLmNvbXBhc3MudjEuU2Vzc2lvbk5vdGljZUgAEjkKEXNlc3Npb25faW5qZWN0aW9uGAkgASgLMhwuY29tcGFzcy52MS5TZXNzaW9uSW5qZWN0aW9uSAASMQoNc2Vzc2lvbl9lcnJvchgKIAEoCzIYLmNvbXBhc3MudjEuU2Vzc2lvbkVycm9ySAASOAoNYmF0Y2hfcGVuZGluZxgLIAEoCzIfLmNvbXBhc3MudjEuU2Vzc2lvbkJhdGNoUGVuZGluZ0gAQgcKBWV2ZW50IjgKFFNlc3Npb25Bc3Npc3RhbnRUZXh0EgwKBHRleHQYASABKAkSEgoKbWVzc2FnZV9pZBgCIAEoCSIzCg9TZXNzaW9uVGhpbmtpbmcSDAoEdGV4dBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJImcKD1Nlc3Npb25Ub29sQ2FsbBIUCgx0b29sX2NhbGxfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSLwoGc3RhdHVzGAMgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIpoBChVTZXNzaW9uVG9vbENhbGxVcGRhdGUSFAoMdG9vbF9jYWxsX2lkGAEgASgJEi8KBnN0YXR1cxgCIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cxIOCgZvdXRwdXQYAyABKAkSKgoFZGlmZnMYBCADKAsyGy5jb21wYXNzLnYxLlNlc3Npb25GaWxlRGlmZiJVCg9TZXNzaW9uRmlsZURpZmYSDAoEcGF0aBgBIAEoCRIVCghvbGRfdGV4dBgCIAEoCUgAiAEBEhAKCG5ld190ZXh0GAMgASgJQgsKCV9vbGRfdGV4dCI6CgtTZXNzaW9uUGxhbhIrCgdlbnRyaWVzGAEgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSI5Cg1TZXNzaW9uTm90aWNlEgwKBHRleHQYASABKAkSEQoEbGluaxgCIAEoCUgAiAEBQgcKBV9saW5rIoMBChBTZXNzaW9uSW5qZWN0aW9uEjEKB29wX2tpbmQYASABKA4yIC5jb21wYXNzLnYxLlNlc3Npb25JbmplY3Rpb25LaW5kEhIKCm1lc3NhZ2VfaWQYAiABKAkSEwoLZnJvbV9oYW5kbGUYAyABKAkSEwoLdHJhY2VwYXJlbnQYBCABKAkiawoMU2Vzc2lvbkVycm9yEioKBGtpbmQYASABKA4yHC5jb21wYXNzLnYxLlNlc3Npb25FcnJvcktpbmQSDwoHbWVzc2FnZRgCIAEoCRITCgZzdGF0dXMYAyABKAVIAIgBAUIJCgdfc3RhdHVzIj4KE1Nlc3Npb25CYXRjaFBlbmRpbmcSDQoFY291bnQYASABKA0SGAoQZmlyZXNfYXRfdW5peF9tcxgCIAEoAyIyChxTdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkilQEKEUFnZW50U2Vzc2lvbkZyYW1lEhIKCnNlc3Npb25faWQYASABKAkSJwoFZXZlbnQYAiABKAsyGC5jb21wYXNzLnYxLlNlc3Npb25FdmVudBIsCgVzdGF0ZRgDIAEoDjIdLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdGUSFQoNdHVybl9zZXF1ZW5jZRgEIAEoBCJwCh5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJEg8KB3BlcnNvbmEYAyABKAkSDAoEcm9sZRgEIAEoCSI5Ch9Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEhYKDmNvbnRhaW5lcl9uYW1lGAEgASgJIlAKG1JlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgCIAEoCSIeChxSZW1vdmVBZ2VudFdvcmtzcGFjZVJlc3BvbnNlImMKGFN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFyZXN1bWVfc2Vzc2lvbl9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiLwoZU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIloKEVNwYXduQWdlbnRSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiQAoSU3Bhd25BZ2VudFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSFgoOY29udGFpbmVyX25hbWUYAiABKAkiLQoXU3RvcEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIaChhTdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2UiLAoWU2tpcEJhdGNoV2luZG93UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIhkKF1NraXBCYXRjaFdpbmRvd1Jlc3BvbnNlIi8KGVJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIwChpSZWxvYWRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIisKFUdldEFnZW50U3RhdHVzUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJInwKFkdldEFnZW50U3RhdHVzUmVzcG9uc2USMAoIc3RhdHVzZXMYASADKAsyHi5jb21wYXNzLnYxLkFnZW50U2Vzc2lvblN0YXR1cxIcCg9ydW5uZXJfZW5yb2xsZWQYAiABKAhIAIgBAUISChBfcnVubmVyX2Vucm9sbGVkIisKEUlzc3VlVG9rZW5SZXF1ZXN0EhYKDmFjY291bnRfaGFuZGxlGAEgASgJIiMKEklzc3VlVG9rZW5SZXNwb25zZRINCgV0b2tlbhgBIAEoCSIoChJSZXZva2VUb2tlblJlcXVlc3QSEgoFdG9rZW4YASABKAlCA4ABASIVChNSZXZva2VUb2tlblJlc3BvbnNlIicKFVB1dEFnZW50Q29uZmlnUmVxdWVzdBIOCgZidW5kbGUYASABKAwiKQoWUHV0QWdlbnRDb25maWdSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJIhsKGUdldEFnZW50Q29uZmlnSW5mb1JlcXVlc3Qi2gEKGkdldEFnZW50Q29uZmlnSW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSDgoGc2tpbGxzGAIgAygJEhIKCmV4dGVuc2lvbnMYAyADKAkSEwoLbWNwX3NlcnZlcnMYBCADKAkSFAoMaGFzX3NldHRpbmdzGAUgASgIEhUKDWhhc19hZ2VudHNfbWQYBiABKAgSDQoFcnVsZXMYByADKAkSEQoJc3ViYWdlbnRzGAggAygJEhIKCmhhc19tb2RlbHMYCSABKAgSDwoHcHJvbXB0cxgKIAMoCSIaChhEZWxldGVBZ2VudENvbmZpZ1JlcXVlc3QiGwoZRGVsZXRlQWdlbnRDb25maWdSZXNwb25zZSI0Cg5Nb2RlbENhbmRpZGF0ZRIQCghwcm92aWRlchgBIAEoCRIQCghtb2RlbF9pZBgCIAEoCSJxCg1Nb2RlbE1ldGFkYXRhEhYKDmNvbnRleHRfd2luZG93GAEgASgDEhwKFGlucHV0X2Nvc3RfbWljcm9fdXNkGAIgASgDEh0KFW91dHB1dF9jb3N0X21pY3JvX3VzZBgDIAEoAxILCgNhcGkYBCABKAkihwEKEk1vZGVsUmVnaXN0cnlFbnRyeRIUCgxkaXNwbGF5X25hbWUYASABKAkSLgoKY2FuZGlkYXRlcxgCIAMoCzIaLmNvbXBhc3MudjEuTW9kZWxDYW5kaWRhdGUSKwoIbWV0YWRhdGEYAyABKAsyGS5jb21wYXNzLnYxLk1vZGVsTWV0YWRhdGEimAEKDU1vZGVsUmVnaXN0cnkSNwoHZW50cmllcxgBIAMoCzImLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeS5FbnRyaWVzRW50cnkaTgoMRW50cmllc0VudHJ5EgsKA2tleRgBIAEoCRItCgV2YWx1ZRgCIAEoCzIeLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeUVudHJ5OgI4ASJgChdQdXRNb2RlbFJlZ2lzdHJ5UmVxdWVzdBIrCghyZWdpc3RyeRgBIAEoCzIZLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeRIYChBleHBlY3RlZF92ZXJzaW9uGAIgASgDIisKGFB1dE1vZGVsUmVnaXN0cnlSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgDIhkKF0dldE1vZGVsUmVnaXN0cnlSZXF1ZXN0IlgKGEdldE1vZGVsUmVnaXN0cnlSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgDEisKCHJlZ2lzdHJ5GAIgASgLMhkuY29tcGFzcy52MS5Nb2RlbFJlZ2lzdHJ5IhwKGkRlbGV0ZU1vZGVsUmVnaXN0cnlSZXF1ZXN0Ih0KG0RlbGV0ZU1vZGVsUmVnaXN0cnlSZXNwb25zZSI+ChBBZ2VudEF0dHJpYnV0aW9uEhQKDGFnZW50X2hhbmRsZRgBIAEoCRIUCgxvd25lcl9oYW5kbGUYAiABKAkiRQoIRm9yZ2VSZWYSKwoIcHJvdmlkZXIYASABKA4yGS5jb21wYXNzLnYxLkZvcmdlUHJvdmlkZXISDAoEaG9zdBgCIAEoCSLUAwoFSXNzdWUSCgoCaWQYASABKAkSIwoFZm9yZ2UYAiABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAyABKAkSDgoGbnVtYmVyGAQgASgNEg0KBXRpdGxlGAUgASgJEgwKBGJvZHkYBiABKAkSEwoLZm9yZ2Vfc3RhdGUYByABKAkSCwoDdXJsGAggASgJEisKBWFnZW50GAkgASgLMhwuY29tcGFzcy52MS5BZ2VudEF0dHJpYnV0aW9uEhUKDWZvcmdlX2FjY291bnQYCiABKAkSDgoGbGFiZWxzGAsgAygJEi4KCnVwZGF0ZWRfYXQYEyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEiUKBXN0YXRlGAwgASgOMhYuY29tcGFzcy52MS5Jc3N1ZVN0YXRlEhAKCHByaW9yaXR5GA0gASgJEhAKCGFzc2lnbmVlGA4gASgJEg8KB3N1bW1hcnkYDyABKAkSDgoGYnJhbmNoGBAgASgJEiQKA3BycxgRIAMoCzIXLmNvbXBhc3MudjEuUHVsbFJlcXVlc3QSJwoHdHJhY2tlchgSIAEoCzIWLmNvbXBhc3MudjEuVHJhY2tlclJlZiL7AwoLUHVsbFJlcXVlc3QSIwoFZm9yZ2UYASABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAiABKAkSDgoGbnVtYmVyGAMgASgNEg0KBXRpdGxlGAQgASgJEhMKC2ZvcmdlX3N0YXRlGAUgASgJEgsKA3VybBgGIAEoCRIQCghoZWFkX3JlZhgHIAEoCRIQCghiYXNlX3JlZhgIIAEoCRIrCgVhZ2VudBgJIAEoCzIcLmNvbXBhc3MudjEuQWdlbnRBdHRyaWJ1dGlvbhIVCg1mb3JnZV9hY2NvdW50GAogASgJEg0KBWRyYWZ0GAsgASgIEikKB2NoYW5nZWQYDCABKAsyGC5jb21wYXNzLnYxLkNoYW5nZWRTdGF0cxIpCgZjaGVja3MYDSABKAsyGS5jb21wYXNzLnYxLkNoZWNrc1N1bW1hcnkSIwoHcmV2aWV3cxgOIAMoCzISLmNvbXBhc3MudjEuUmV2aWV3EikKB3RocmVhZHMYDyADKAsyGC5jb21wYXNzLnYxLlJldmlld1RocmVhZBIQCghoZWFkX3NoYRgQIAEoCRIXCg9tZXJnZWFibGVfc3RhdGUYESABKAkSMAoLbWVyZ2VfcXVldWUYEiABKAsyGy5jb21wYXNzLnYxLk1lcmdlUXVldWVFbnRyeSJTCg1DaGVja3NTdW1tYXJ5EhAKCGhlYWRfc2hhGAEgASgJEg0KBXN0YXRlGAIgASgJEiEKBmNoZWNrcxgDIAMoCzIRLmNvbXBhc3MudjEuQ2hlY2siQwoFQ2hlY2sSDAoEbmFtZRgBIAEoCRINCgVzdGF0ZRgCIAEoCRILCgN1cmwYAyABKAkSEAoIcmVxdWlyZWQYBCABKAgiQwoMQ2hhbmdlZFN0YXRzEg0KBWZpbGVzGAEgASgNEhEKCWFkZGl0aW9ucxgCIAEoDRIRCglkZWxldGlvbnMYAyABKA0iaQoKVHJhY2tlclJlZhIMCgRraW5kGAEgASgJEgoKAmlkGAIgASgJEg4KBnN0YXR1cxgDIAEoCRILCgN1cmwYBCABKAkSEAoIc3RhdGVfaWQYBSABKAkSEgoKc3RhdGVfdHlwZRgGIAEoCSJaCgZSZXZpZXcSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIPCgd2ZXJkaWN0GAMgASgJEgwKBGJvZHkYBCABKAkSEQoJY29tbWl0X2lkGAUgASgJIlUKDFJldmlld1RocmVhZBIMCgRwYXRoGAEgASgJEhAKCHJlc29sdmVkGAIgASgIEiUKCGNvbW1lbnRzGAMgAygLMhMuY29tcGFzcy52MS5Db21tZW50IjcKB0NvbW1lbnQSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIMCgRib2R5GAMgASgJImIKD01lcmdlUXVldWVFbnRyeRIQCghwcm92aWRlchgBIAEoCRINCgVzdGF0ZRgCIAEoCRIuCgpjaGFuZ2VkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCpkCg5TZWNyZXREZWxpdmVyeRIfChtTRUNSRVRfREVMSVZFUllfVU5TUEVDSUZJRUQQABIYChRTRUNSRVRfREVMSVZFUllfRklMRRABEhcKE1NFQ1JFVF9ERUxJVkVSWV9FTlYQAipwCgpTZWNyZXRLaW5kEhsKF1NFQ1JFVF9LSU5EX1VOU1BFQ0lGSUVEEAASFwoTU0VDUkVUX0tJTkRfR0VORVJJQxABEhgKFFNFQ1JFVF9LSU5EX1BST1ZJREVSEAISEgoOU0VDUkVUX0tJTkRfR0gQAypbCgtTZWNyZXRTY29wZRIcChhTRUNSRVRfU0NPUEVfVU5TUEVDSUZJRUQQABIVChFTRUNSRVRfU0NPUEVfVVNFUhABEhcKE1NFQ1JFVF9TQ09QRV9URU5BTlQQAip9CgtUb3VyT3V0Y29tZRIcChhUT1VSX09VVENPTUVfVU5TUEVDSUZJRUQQABIYChRUT1VSX09VVENPTUVfU1RBUlRFRBABEhoKFlRPVVJfT1VUQ09NRV9ESVNNSVNTRUQQAhIaChZUT1VSX09VVENPTUVfQ09NUExFVEVEEAMqQwoLU2VydmVyU3RhdGUSHAoYU0VSVkVSX1NUQVRFX1VOU1BFQ0lGSUVEEAASFgoSU0VSVkVSX1NUQVRFX1JFQURZEAEqlwEKC1J1bnRpbWVUaWVyEhwKGFJVTlRJTUVfVElFUl9VTlNQRUNJRklFRBAAEhcKE1JVTlRJTUVfVElFUl9QT0RNQU4QARIYChRSVU5USU1FX1RJRVJfTUlDUk9WTRACEiAKHFJVTlRJTUVfVElFUl9BUFBMRV9DT05UQUlORVIQAxIVChFSVU5USU1FX1RJRVJfSE9TVBAEKmgKDUVncmVzc1Bvc3R1cmUSHgoaRUdSRVNTX1BPU1RVUkVfVU5TUEVDSUZJRUQQABIYChRFR1JFU1NfUE9TVFVSRV9BUk1FRBABEh0KGUVHUkVTU19QT1NUVVJFX1VORU5GT1JDRUQQAiqCAgoRQWdlbnRTZXNzaW9uU3RhdGUSIwofQUdFTlRfU0VTU0lPTl9TVEFURV9VTlNQRUNJRklFRBAAEiAKHEFHRU5UX1NFU1NJT05fU1RBVEVfU1RBUlRJTkcQARIdChlBR0VOVF9TRVNTSU9OX1NUQVRFX1JFQURZEAISHwobQUdFTlRfU0VTU0lPTl9TVEFURV9XT1JLSU5HEAMSHwobQUdFTlRfU0VTU0lPTl9TVEFURV9TVE9QUEVEEAQSHwobQUdFTlRfU0VTU0lPTl9TVEFURV9FUlJPUkVEEAUSJAogQUdFTlRfU0VTU0lPTl9TVEFURV9ESVNDT05ORUNURUQQBirSAQoTQWdlbnRUb29sQ2FsbFN0YXR1cxImCiJBR0VOVF9UT09MX0NBTExfU1RBVFVTX1VOU1BFQ0lGSUVEEAASIgoeQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19QRU5ESU5HEAESJgoiQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19JTl9QUk9HUkVTUxACEiQKIEFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfQ09NUExFVEVEEAMSIQodQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19GQUlMRUQQBCq0AQoUQWdlbnRQbGFuRW50cnlTdGF0dXMSJwojQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfVU5TUEVDSUZJRUQQABIjCh9BR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19QRU5ESU5HEAESJwojQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfSU5fUFJPR1JFU1MQAhIlCiFBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19DT01QTEVURUQQAyqEAQoUU2Vzc2lvbkluamVjdGlvbktpbmQSJgoiU0VTU0lPTl9JTkpFQ1RJT05fS0lORF9VTlNQRUNJRklFRBAAEiAKHFNFU1NJT05fSU5KRUNUSU9OX0tJTkRfU1RFRVIQARIiCh5TRVNTSU9OX0lOSkVDVElPTl9LSU5EX0RFTElWRVIQAip0ChBTZXNzaW9uRXJyb3JLaW5kEiIKHlNFU1NJT05fRVJST1JfS0lORF9VTlNQRUNJRklFRBAAEhwKGFNFU1NJT05fRVJST1JfS0lORF9FUlJPUhABEh4KGlNFU1NJT05fRVJST1JfS0lORF9BQk9SVEVEEAIq8QEKCklzc3VlU3RhdGUSGwoXSVNTVUVfU1RBVEVfVU5TUEVDSUZJRUQQABIXChNJU1NVRV9TVEFURV9CQUNLTE9HEAESFAoQSVNTVUVfU1RBVEVfVE9ETxACEhYKEklTU1VFX1NUQVRFX1FVRVVFRBADEhcKE0lTU1VFX1NUQVRFX0JMT0NLRUQQBBIbChdJU1NVRV9TVEFURV9JTl9QUk9HUkVTUxAFEhkKFUlTU1VFX1NUQVRFX0lOX1JFVklFVxAGEhQKEElTU1VFX1NUQVRFX0RPTkUQBxIYChRJU1NVRV9TVEFURV9BUkNISVZFRBAIKpwBCg1Gb3JnZVByb3ZpZGVyEh4KGkZPUkdFX1BST1ZJREVSX1VOU1BFQ0lGSUVEEAASGQoVRk9SR0VfUFJPVklERVJfR0lUSFVCEAESGQoVRk9SR0VfUFJPVklERVJfR0lUTEFCEAISGgoWRk9SR0VfUFJPVklERVJfRk9SR0VKTxADEhkKFUZPUkdFX1BST1ZJREVSX0xJTkVBUhAEMoESCg5Db21wYXNzU2VydmljZRJUCg1HZXRTZXJ2ZXJJbmZvEiAuY29tcGFzcy52MS5HZXRTZXJ2ZXJJbmZvUmVxdWVzdBohLmNvbXBhc3MudjEuR2V0U2VydmVySW5mb1Jlc3BvbnNlEj8KBldob0FtSRIZLmNvbXBhc3MudjEuV2hvQW1JUmVxdWVzdBoaLmNvbXBhc3MudjEuV2hvQW1JUmVzcG9uc2USUQoMR2V0VG91clN0YXRlEh8uY29tcGFzcy52MS5HZXRUb3VyU3RhdGVSZXF1ZXN0GiAuY29tcGFzcy52MS5HZXRUb3VyU3RhdGVSZXNwb25zZRJXCg5DbGFpbVRvdXJTdGFydBIhLmNvbXBhc3MudjEuQ2xhaW1Ub3VyU3RhcnRSZXF1ZXN0GiIuY29tcGFzcy52MS5DbGFpbVRvdXJTdGFydFJlc3BvbnNlElEKDFNldFRvdXJTdGF0ZRIfLmNvbXBhc3MudjEuU2V0VG91clN0YXRlUmVxdWVzdBogLmNvbXBhc3MudjEuU2V0VG91clN0YXRlUmVzcG9uc2USXAoPU3Vic2NyaWJlRXZlbnRzEiIuY29tcGFzcy52MS5TdWJzY3JpYmVFdmVudHNSZXF1ZXN0GiMuY29tcGFzcy52MS5TdWJzY3JpYmVFdmVudHNSZXNwb25zZTABEloKD0xpc3RCb2FyZElzc3VlcxIiLmNvbXBhc3MudjEuTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBojLmNvbXBhc3MudjEuTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USUQoMU2VhcmNoSXNzdWVzEh8uY29tcGFzcy52MS5TZWFyY2hJc3N1ZXNSZXF1ZXN0GiAuY29tcGFzcy52MS5TZWFyY2hJc3N1ZXNSZXNwb25zZRJyChdQcm92aXNpb25BZ2VudFdvcmtzcGFjZRIqLmNvbXBhc3MudjEuUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0GisuY29tcGFzcy52MS5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEmAKEVN0YXJ0QWdlbnRTZXNzaW9uEiQuY29tcGFzcy52MS5TdGFydEFnZW50U2Vzc2lvblJlcXVlc3QaJS5jb21wYXNzLnYxLlN0YXJ0QWdlbnRTZXNzaW9uUmVzcG9uc2USSwoKU3Bhd25BZ2VudBIdLmNvbXBhc3MudjEuU3Bhd25BZ2VudFJlcXVlc3QaHi5jb21wYXNzLnYxLlNwYXduQWdlbnRSZXNwb25zZRJdChBTdG9wQWdlbnRTZXNzaW9uEiMuY29tcGFzcy52MS5TdG9wQWdlbnRTZXNzaW9uUmVxdWVzdBokLmNvbXBhc3MudjEuU3RvcEFnZW50U2Vzc2lvblJlc3BvbnNlEloKD1NraXBCYXRjaFdpbmRvdxIiLmNvbXBhc3MudjEuU2tpcEJhdGNoV2luZG93UmVxdWVzdBojLmNvbXBhc3MudjEuU2tpcEJhdGNoV2luZG93UmVzcG9uc2USaQoUUmVtb3ZlQWdlbnRXb3Jrc3BhY2USJy5jb21wYXNzLnYxLlJlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBooLmNvbXBhc3MudjEuUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJjChJSZWxvYWRBZ2VudFNlc3Npb24SJS5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QaJi5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlc3BvbnNlElcKDkdldEFnZW50U3RhdHVzEiEuY29tcGFzcy52MS5HZXRBZ2VudFN0YXR1c1JlcXVlc3QaIi5jb21wYXNzLnYxLkdldEFnZW50U3RhdHVzUmVzcG9uc2USYgoVU3Vic2NyaWJlQWdlbnRTZXNzaW9uEiguY29tcGFzcy52MS5TdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0Gh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25GcmFtZTABEksKCklzc3VlVG9rZW4SHS5jb21wYXNzLnYxLklzc3VlVG9rZW5SZXF1ZXN0Gh4uY29tcGFzcy52MS5Jc3N1ZVRva2VuUmVzcG9uc2USTgoLUmV2b2tlVG9rZW4SHi5jb21wYXNzLnYxLlJldm9rZVRva2VuUmVxdWVzdBofLmNvbXBhc3MudjEuUmV2b2tlVG9rZW5SZXNwb25zZRJXCg5QdXRBZ2VudENvbmZpZxIhLmNvbXBhc3MudjEuUHV0QWdlbnRDb25maWdSZXF1ZXN0GiIuY29tcGFzcy52MS5QdXRBZ2VudENvbmZpZ1Jlc3BvbnNlEmMKEkdldEFnZW50Q29uZmlnSW5mbxIlLmNvbXBhc3MudjEuR2V0QWdlbnRDb25maWdJbmZvUmVxdWVzdBomLmNvbXBhc3MudjEuR2V0QWdlbnRDb25maWdJbmZvUmVzcG9uc2USYAoRRGVsZXRlQWdlbnRDb25maWcSJC5jb21wYXNzLnYxLkRlbGV0ZUFnZW50Q29uZmlnUmVxdWVzdBolLmNvbXBhc3MudjEuRGVsZXRlQWdlbnRDb25maWdSZXNwb25zZRJdChBQdXRNb2RlbFJlZ2lzdHJ5EiMuY29tcGFzcy52MS5QdXRNb2RlbFJlZ2lzdHJ5UmVxdWVzdBokLmNvbXBhc3MudjEuUHV0TW9kZWxSZWdpc3RyeVJlc3BvbnNlEl0KEEdldE1vZGVsUmVnaXN0cnkSIy5jb21wYXNzLnYxLkdldE1vZGVsUmVnaXN0cnlSZXF1ZXN0GiQuY29tcGFzcy52MS5HZXRNb2RlbFJlZ2lzdHJ5UmVzcG9uc2USZgoTRGVsZXRlTW9kZWxSZWdpc3RyeRImLmNvbXBhc3MudjEuRGVsZXRlTW9kZWxSZWdpc3RyeVJlcXVlc3QaJy5jb21wYXNzLnYxLkRlbGV0ZU1vZGVsUmVnaXN0cnlSZXNwb25zZTLfAgoOU2VjcmV0c1NlcnZpY2USSAoJU2V0U2VjcmV0EhwuY29tcGFzcy52MS5TZXRTZWNyZXRSZXF1ZXN0Gh0uY29tcGFzcy52MS5TZXRTZWNyZXRSZXNwb25zZRJOCgtMaXN0U2VjcmV0cxIeLmNvbXBhc3MudjEuTGlzdFNlY3JldHNSZXF1ZXN0Gh8uY29tcGFzcy52MS5MaXN0U2VjcmV0c1Jlc3BvbnNlElEKDERlbGV0ZVNlY3JldBIfLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVxdWVzdBogLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVzcG9uc2USYAoRTGlzdFNlcnZlclNlY3JldHMSJC5jb21wYXNzLnYxLkxpc3RTZXJ2ZXJTZWNyZXRzUmVxdWVzdBolLmNvbXBhc3MudjEuTGlzdFNlcnZlclNlY3JldHNSZXNwb25zZWIGcHJvdG8z", [file_google_protobuf_timestamp]); + fileDesc("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEi0AEKEFNldFNlY3JldFJlcXVlc3QSDAoEbmFtZRgBIAEoCRISCgV2YWx1ZRgCIAEoCUIDgAEBEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkSJgoFc2NvcGUYByABKA4yFy5jb21wYXNzLnYxLlNlY3JldFNjb3BlIhMKEVNldFNlY3JldFJlc3BvbnNlIhQKEkxpc3RTZWNyZXRzUmVxdWVzdCJAChNMaXN0U2VjcmV0c1Jlc3BvbnNlEikKB3NlY3JldHMYASADKAsyGC5jb21wYXNzLnYxLlNlY3JldFN0YXR1cyKgAQoMU2VjcmV0U3RhdHVzEgwKBG5hbWUYASABKAkSDgoGaXNfc2V0GAIgASgIEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkiSwoTRGVsZXRlU2VjcmV0UmVxdWVzdBIMCgRuYW1lGAEgASgJEiYKBXNjb3BlGAIgASgOMhcuY29tcGFzcy52MS5TZWNyZXRTY29wZSIWChREZWxldGVTZWNyZXRSZXNwb25zZSIaChhMaXN0U2VydmVyU2VjcmV0c1JlcXVlc3QiUwoZTGlzdFNlcnZlclNlY3JldHNSZXNwb25zZRI2Cg5zZXJ2ZXJfc2VjcmV0cxgBIAMoCzIeLmNvbXBhc3MudjEuU2VydmVyU2VjcmV0U3RhdHVzIjIKElNlcnZlclNlY3JldFN0YXR1cxIMCgRuYW1lGAEgASgJEg4KBmlzX3NldBgCIAEoCCIWChRHZXRTZXJ2ZXJJbmZvUmVxdWVzdCKAAQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSEwoLYXBpX3ZlcnNpb24YAiABKAkSCwoDcmV2GAMgASgJEjQKEGVucm9sbGVkX3J1bm5lcnMYBCADKAsyGi5jb21wYXNzLnYxLkVucm9sbGVkUnVubmVyIjAKDkVucm9sbGVkUnVubmVyEgoKAmlkGAEgASgJEhIKCmVucm9sbG1lbnQYAiABKAQiDwoNV2hvQW1JUmVxdWVzdCIkCg5XaG9BbUlSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJIhUKE0dldFRvdXJTdGF0ZVJlcXVlc3QiUQoUR2V0VG91clN0YXRlUmVzcG9uc2USKAoHb3V0Y29tZRgBIAEoDjIXLmNvbXBhc3MudjEuVG91ck91dGNvbWUSDwoHc3RlcF9pZBgCIAEoCSIoChVDbGFpbVRvdXJTdGFydFJlcXVlc3QSDwoHc3RlcF9pZBgBIAEoCSIpChZDbGFpbVRvdXJTdGFydFJlc3BvbnNlEg8KB2NsYWltZWQYASABKAgiUAoTU2V0VG91clN0YXRlUmVxdWVzdBIoCgdvdXRjb21lGAEgASgOMhcuY29tcGFzcy52MS5Ub3VyT3V0Y29tZRIPCgdzdGVwX2lkGAIgASgJIhYKFFNldFRvdXJTdGF0ZVJlc3BvbnNlIkMKFlN1YnNjcmliZUV2ZW50c1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEIuIDChdTdWJzY3JpYmVFdmVudHNSZXNwb25zZRILCgNzZXEYASABKAQSEgoKYXRfdW5peF9tcxgCIAEoAxIWCg5pbnN0YW5jZV9lcG9jaBgDIAEoBBIUCgxzbmFwc2hvdF9zZXEYBCABKAQSMQoNc2VydmVyX3N0YXR1cxgKIAEoCzIYLmNvbXBhc3MudjEuU2VydmVyU3RhdHVzSAASNQoPcmVzeW5jX3JlcXVpcmVkGAsgASgLMhouY29tcGFzcy52MS5SZXN5bmNSZXF1aXJlZEgAEj4KFGFnZW50X3Nlc3Npb25fc3RhdHVzGAwgASgLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXNIABI8ChNhZ2VudF9tZXNzYWdlX2NodW5rGA0gASgLMh0uY29tcGFzcy52MS5BZ2VudE1lc3NhZ2VDaHVua0gAEjQKD2FnZW50X3Rvb2xfY2FsbBgOIAEoCzIZLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbEgAEisKCmFnZW50X3BsYW4YDyABKAsyFS5jb21wYXNzLnYxLkFnZW50UGxhbkgAEiIKBWlzc3VlGBAgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAQgkKB3BheWxvYWQiLgoWTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPAoXTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSJJChNTZWFyY2hJc3N1ZXNSZXF1ZXN0Eg0KBXF1ZXJ5GAEgASgJEg0KBWxpbWl0GAIgASgNEhQKDHNuYXBzaG90X3NlcRgDIAEoBCI5ChRTZWFyY2hJc3N1ZXNSZXNwb25zZRIhCgZpc3N1ZXMYASADKAsyES5jb21wYXNzLnYxLklzc3VlIjYKDFNlcnZlclN0YXR1cxImCgVzdGF0ZRgBIAEoDjIXLmNvbXBhc3MudjEuU2VydmVyU3RhdGUiEAoOUmVzeW5jUmVxdWlyZWQi0gEKEkFnZW50U2Vzc2lvblN0YXR1cxISCgpzZXNzaW9uX2lkGAEgASgJEiwKBXN0YXRlGAIgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZRIYChBhZ2VudF9hY2NvdW50X2lkGAMgASgJEi0KDHJ1bnRpbWVfdGllchgEIAEoDjIXLmNvbXBhc3MudjEuUnVudGltZVRpZXISMQoOZWdyZXNzX3Bvc3R1cmUYBSABKA4yGS5jb21wYXNzLnYxLkVncmVzc1Bvc3R1cmUiSQoRQWdlbnRNZXNzYWdlQ2h1bmsSEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgR0ZXh0GAIgASgJEhIKCmlzX3Rob3VnaHQYAyABKAgieQoNQWdlbnRUb29sQ2FsbBISCgpzZXNzaW9uX2lkGAEgASgJEhQKDHRvb2xfY2FsbF9pZBgCIAEoCRINCgV0aXRsZRgDIAEoCRIvCgZzdGF0dXMYBCABKA4yHy5jb21wYXNzLnYxLkFnZW50VG9vbENhbGxTdGF0dXMiTAoJQWdlbnRQbGFuEhIKCnNlc3Npb25faWQYASABKAkSKwoHZW50cmllcxgCIAMoCzIaLmNvbXBhc3MudjEuQWdlbnRQbGFuRW50cnkiUwoOQWdlbnRQbGFuRW50cnkSDwoHY29udGVudBgBIAEoCRIwCgZzdGF0dXMYAiABKA4yIC5jb21wYXNzLnYxLkFnZW50UGxhbkVudHJ5U3RhdHVzIpkECgxTZXNzaW9uRXZlbnQSEAoIZXZlbnRfaWQYASABKAkSEgoKYXRfdW5peF9tcxgCIAEoAxI6Cg5hc3Npc3RhbnRfdGV4dBgDIAEoCzIgLmNvbXBhc3MudjEuU2Vzc2lvbkFzc2lzdGFudFRleHRIABIvCgh0aGlua2luZxgEIAEoCzIbLmNvbXBhc3MudjEuU2Vzc2lvblRoaW5raW5nSAASMAoJdG9vbF9jYWxsGAUgASgLMhsuY29tcGFzcy52MS5TZXNzaW9uVG9vbENhbGxIABI9ChB0b29sX2NhbGxfdXBkYXRlGAYgASgLMiEuY29tcGFzcy52MS5TZXNzaW9uVG9vbENhbGxVcGRhdGVIABInCgRwbGFuGAcgASgLMhcuY29tcGFzcy52MS5TZXNzaW9uUGxhbkgAEisKBm5vdGljZRgIIAEoCzIZLmNvbXBhc3MudjEuU2Vzc2lvbk5vdGljZUgAEjkKEXNlc3Npb25faW5qZWN0aW9uGAkgASgLMhwuY29tcGFzcy52MS5TZXNzaW9uSW5qZWN0aW9uSAASMQoNc2Vzc2lvbl9lcnJvchgKIAEoCzIYLmNvbXBhc3MudjEuU2Vzc2lvbkVycm9ySAASOAoNYmF0Y2hfcGVuZGluZxgLIAEoCzIfLmNvbXBhc3MudjEuU2Vzc2lvbkJhdGNoUGVuZGluZ0gAQgcKBWV2ZW50IjgKFFNlc3Npb25Bc3Npc3RhbnRUZXh0EgwKBHRleHQYASABKAkSEgoKbWVzc2FnZV9pZBgCIAEoCSIzCg9TZXNzaW9uVGhpbmtpbmcSDAoEdGV4dBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJImcKD1Nlc3Npb25Ub29sQ2FsbBIUCgx0b29sX2NhbGxfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSLwoGc3RhdHVzGAMgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIpoBChVTZXNzaW9uVG9vbENhbGxVcGRhdGUSFAoMdG9vbF9jYWxsX2lkGAEgASgJEi8KBnN0YXR1cxgCIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cxIOCgZvdXRwdXQYAyABKAkSKgoFZGlmZnMYBCADKAsyGy5jb21wYXNzLnYxLlNlc3Npb25GaWxlRGlmZiJVCg9TZXNzaW9uRmlsZURpZmYSDAoEcGF0aBgBIAEoCRIVCghvbGRfdGV4dBgCIAEoCUgAiAEBEhAKCG5ld190ZXh0GAMgASgJQgsKCV9vbGRfdGV4dCI6CgtTZXNzaW9uUGxhbhIrCgdlbnRyaWVzGAEgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSI5Cg1TZXNzaW9uTm90aWNlEgwKBHRleHQYASABKAkSEQoEbGluaxgCIAEoCUgAiAEBQgcKBV9saW5rIoMBChBTZXNzaW9uSW5qZWN0aW9uEjEKB29wX2tpbmQYASABKA4yIC5jb21wYXNzLnYxLlNlc3Npb25JbmplY3Rpb25LaW5kEhIKCm1lc3NhZ2VfaWQYAiABKAkSEwoLZnJvbV9oYW5kbGUYAyABKAkSEwoLdHJhY2VwYXJlbnQYBCABKAkiawoMU2Vzc2lvbkVycm9yEioKBGtpbmQYASABKA4yHC5jb21wYXNzLnYxLlNlc3Npb25FcnJvcktpbmQSDwoHbWVzc2FnZRgCIAEoCRITCgZzdGF0dXMYAyABKAVIAIgBAUIJCgdfc3RhdHVzIj4KE1Nlc3Npb25CYXRjaFBlbmRpbmcSDQoFY291bnQYASABKA0SGAoQZmlyZXNfYXRfdW5peF9tcxgCIAEoAyIyChxTdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkilQEKEUFnZW50U2Vzc2lvbkZyYW1lEhIKCnNlc3Npb25faWQYASABKAkSJwoFZXZlbnQYAiABKAsyGC5jb21wYXNzLnYxLlNlc3Npb25FdmVudBIsCgVzdGF0ZRgDIAEoDjIdLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdGUSFQoNdHVybl9zZXF1ZW5jZRgEIAEoBCJwCh5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJEg8KB3BlcnNvbmEYAyABKAkSDAoEcm9sZRgEIAEoCSI5Ch9Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEhYKDmNvbnRhaW5lcl9uYW1lGAEgASgJIlAKG1JlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgCIAEoCSIeChxSZW1vdmVBZ2VudFdvcmtzcGFjZVJlc3BvbnNlImMKGFN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFyZXN1bWVfc2Vzc2lvbl9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiLwoZU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIloKEVNwYXduQWdlbnRSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiQAoSU3Bhd25BZ2VudFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSFgoOY29udGFpbmVyX25hbWUYAiABKAkiLQoXU3RvcEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIaChhTdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2UiLAoWU2tpcEJhdGNoV2luZG93UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIhkKF1NraXBCYXRjaFdpbmRvd1Jlc3BvbnNlIi8KGVJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIwChpSZWxvYWRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIisKFUdldEFnZW50U3RhdHVzUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJInwKFkdldEFnZW50U3RhdHVzUmVzcG9uc2USMAoIc3RhdHVzZXMYASADKAsyHi5jb21wYXNzLnYxLkFnZW50U2Vzc2lvblN0YXR1cxIcCg9ydW5uZXJfZW5yb2xsZWQYAiABKAhIAIgBAUISChBfcnVubmVyX2Vucm9sbGVkIisKEUlzc3VlVG9rZW5SZXF1ZXN0EhYKDmFjY291bnRfaGFuZGxlGAEgASgJIiMKEklzc3VlVG9rZW5SZXNwb25zZRINCgV0b2tlbhgBIAEoCSJCChRSZW5hbWVBY2NvdW50UmVxdWVzdBIWCg5hY2NvdW50X2hhbmRsZRgBIAEoCRISCgpuZXdfaGFuZGxlGAIgASgJIjsKFVJlbmFtZUFjY291bnRSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJEg4KBmhhbmRsZRgCIAEoCSIoChJSZXZva2VUb2tlblJlcXVlc3QSEgoFdG9rZW4YASABKAlCA4ABASIVChNSZXZva2VUb2tlblJlc3BvbnNlIicKFVB1dEFnZW50Q29uZmlnUmVxdWVzdBIOCgZidW5kbGUYASABKAwiKQoWUHV0QWdlbnRDb25maWdSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJIhsKGUdldEFnZW50Q29uZmlnSW5mb1JlcXVlc3Qi2gEKGkdldEFnZW50Q29uZmlnSW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSDgoGc2tpbGxzGAIgAygJEhIKCmV4dGVuc2lvbnMYAyADKAkSEwoLbWNwX3NlcnZlcnMYBCADKAkSFAoMaGFzX3NldHRpbmdzGAUgASgIEhUKDWhhc19hZ2VudHNfbWQYBiABKAgSDQoFcnVsZXMYByADKAkSEQoJc3ViYWdlbnRzGAggAygJEhIKCmhhc19tb2RlbHMYCSABKAgSDwoHcHJvbXB0cxgKIAMoCSIaChhEZWxldGVBZ2VudENvbmZpZ1JlcXVlc3QiGwoZRGVsZXRlQWdlbnRDb25maWdSZXNwb25zZSI0Cg5Nb2RlbENhbmRpZGF0ZRIQCghwcm92aWRlchgBIAEoCRIQCghtb2RlbF9pZBgCIAEoCSJxCg1Nb2RlbE1ldGFkYXRhEhYKDmNvbnRleHRfd2luZG93GAEgASgDEhwKFGlucHV0X2Nvc3RfbWljcm9fdXNkGAIgASgDEh0KFW91dHB1dF9jb3N0X21pY3JvX3VzZBgDIAEoAxILCgNhcGkYBCABKAkihwEKEk1vZGVsUmVnaXN0cnlFbnRyeRIUCgxkaXNwbGF5X25hbWUYASABKAkSLgoKY2FuZGlkYXRlcxgCIAMoCzIaLmNvbXBhc3MudjEuTW9kZWxDYW5kaWRhdGUSKwoIbWV0YWRhdGEYAyABKAsyGS5jb21wYXNzLnYxLk1vZGVsTWV0YWRhdGEimAEKDU1vZGVsUmVnaXN0cnkSNwoHZW50cmllcxgBIAMoCzImLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeS5FbnRyaWVzRW50cnkaTgoMRW50cmllc0VudHJ5EgsKA2tleRgBIAEoCRItCgV2YWx1ZRgCIAEoCzIeLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeUVudHJ5OgI4ASJgChdQdXRNb2RlbFJlZ2lzdHJ5UmVxdWVzdBIrCghyZWdpc3RyeRgBIAEoCzIZLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeRIYChBleHBlY3RlZF92ZXJzaW9uGAIgASgDIisKGFB1dE1vZGVsUmVnaXN0cnlSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgDIhkKF0dldE1vZGVsUmVnaXN0cnlSZXF1ZXN0IlgKGEdldE1vZGVsUmVnaXN0cnlSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgDEisKCHJlZ2lzdHJ5GAIgASgLMhkuY29tcGFzcy52MS5Nb2RlbFJlZ2lzdHJ5IhwKGkRlbGV0ZU1vZGVsUmVnaXN0cnlSZXF1ZXN0Ih0KG0RlbGV0ZU1vZGVsUmVnaXN0cnlSZXNwb25zZSI+ChBBZ2VudEF0dHJpYnV0aW9uEhQKDGFnZW50X2hhbmRsZRgBIAEoCRIUCgxvd25lcl9oYW5kbGUYAiABKAkiRQoIRm9yZ2VSZWYSKwoIcHJvdmlkZXIYASABKA4yGS5jb21wYXNzLnYxLkZvcmdlUHJvdmlkZXISDAoEaG9zdBgCIAEoCSLUAwoFSXNzdWUSCgoCaWQYASABKAkSIwoFZm9yZ2UYAiABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAyABKAkSDgoGbnVtYmVyGAQgASgNEg0KBXRpdGxlGAUgASgJEgwKBGJvZHkYBiABKAkSEwoLZm9yZ2Vfc3RhdGUYByABKAkSCwoDdXJsGAggASgJEisKBWFnZW50GAkgASgLMhwuY29tcGFzcy52MS5BZ2VudEF0dHJpYnV0aW9uEhUKDWZvcmdlX2FjY291bnQYCiABKAkSDgoGbGFiZWxzGAsgAygJEi4KCnVwZGF0ZWRfYXQYEyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEiUKBXN0YXRlGAwgASgOMhYuY29tcGFzcy52MS5Jc3N1ZVN0YXRlEhAKCHByaW9yaXR5GA0gASgJEhAKCGFzc2lnbmVlGA4gASgJEg8KB3N1bW1hcnkYDyABKAkSDgoGYnJhbmNoGBAgASgJEiQKA3BycxgRIAMoCzIXLmNvbXBhc3MudjEuUHVsbFJlcXVlc3QSJwoHdHJhY2tlchgSIAEoCzIWLmNvbXBhc3MudjEuVHJhY2tlclJlZiL7AwoLUHVsbFJlcXVlc3QSIwoFZm9yZ2UYASABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAiABKAkSDgoGbnVtYmVyGAMgASgNEg0KBXRpdGxlGAQgASgJEhMKC2ZvcmdlX3N0YXRlGAUgASgJEgsKA3VybBgGIAEoCRIQCghoZWFkX3JlZhgHIAEoCRIQCghiYXNlX3JlZhgIIAEoCRIrCgVhZ2VudBgJIAEoCzIcLmNvbXBhc3MudjEuQWdlbnRBdHRyaWJ1dGlvbhIVCg1mb3JnZV9hY2NvdW50GAogASgJEg0KBWRyYWZ0GAsgASgIEikKB2NoYW5nZWQYDCABKAsyGC5jb21wYXNzLnYxLkNoYW5nZWRTdGF0cxIpCgZjaGVja3MYDSABKAsyGS5jb21wYXNzLnYxLkNoZWNrc1N1bW1hcnkSIwoHcmV2aWV3cxgOIAMoCzISLmNvbXBhc3MudjEuUmV2aWV3EikKB3RocmVhZHMYDyADKAsyGC5jb21wYXNzLnYxLlJldmlld1RocmVhZBIQCghoZWFkX3NoYRgQIAEoCRIXCg9tZXJnZWFibGVfc3RhdGUYESABKAkSMAoLbWVyZ2VfcXVldWUYEiABKAsyGy5jb21wYXNzLnYxLk1lcmdlUXVldWVFbnRyeSJTCg1DaGVja3NTdW1tYXJ5EhAKCGhlYWRfc2hhGAEgASgJEg0KBXN0YXRlGAIgASgJEiEKBmNoZWNrcxgDIAMoCzIRLmNvbXBhc3MudjEuQ2hlY2siQwoFQ2hlY2sSDAoEbmFtZRgBIAEoCRINCgVzdGF0ZRgCIAEoCRILCgN1cmwYAyABKAkSEAoIcmVxdWlyZWQYBCABKAgiQwoMQ2hhbmdlZFN0YXRzEg0KBWZpbGVzGAEgASgNEhEKCWFkZGl0aW9ucxgCIAEoDRIRCglkZWxldGlvbnMYAyABKA0iaQoKVHJhY2tlclJlZhIMCgRraW5kGAEgASgJEgoKAmlkGAIgASgJEg4KBnN0YXR1cxgDIAEoCRILCgN1cmwYBCABKAkSEAoIc3RhdGVfaWQYBSABKAkSEgoKc3RhdGVfdHlwZRgGIAEoCSJaCgZSZXZpZXcSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIPCgd2ZXJkaWN0GAMgASgJEgwKBGJvZHkYBCABKAkSEQoJY29tbWl0X2lkGAUgASgJIlUKDFJldmlld1RocmVhZBIMCgRwYXRoGAEgASgJEhAKCHJlc29sdmVkGAIgASgIEiUKCGNvbW1lbnRzGAMgAygLMhMuY29tcGFzcy52MS5Db21tZW50IjcKB0NvbW1lbnQSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIMCgRib2R5GAMgASgJImIKD01lcmdlUXVldWVFbnRyeRIQCghwcm92aWRlchgBIAEoCRINCgVzdGF0ZRgCIAEoCRIuCgpjaGFuZ2VkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCpkCg5TZWNyZXREZWxpdmVyeRIfChtTRUNSRVRfREVMSVZFUllfVU5TUEVDSUZJRUQQABIYChRTRUNSRVRfREVMSVZFUllfRklMRRABEhcKE1NFQ1JFVF9ERUxJVkVSWV9FTlYQAipwCgpTZWNyZXRLaW5kEhsKF1NFQ1JFVF9LSU5EX1VOU1BFQ0lGSUVEEAASFwoTU0VDUkVUX0tJTkRfR0VORVJJQxABEhgKFFNFQ1JFVF9LSU5EX1BST1ZJREVSEAISEgoOU0VDUkVUX0tJTkRfR0gQAypbCgtTZWNyZXRTY29wZRIcChhTRUNSRVRfU0NPUEVfVU5TUEVDSUZJRUQQABIVChFTRUNSRVRfU0NPUEVfVVNFUhABEhcKE1NFQ1JFVF9TQ09QRV9URU5BTlQQAip9CgtUb3VyT3V0Y29tZRIcChhUT1VSX09VVENPTUVfVU5TUEVDSUZJRUQQABIYChRUT1VSX09VVENPTUVfU1RBUlRFRBABEhoKFlRPVVJfT1VUQ09NRV9ESVNNSVNTRUQQAhIaChZUT1VSX09VVENPTUVfQ09NUExFVEVEEAMqQwoLU2VydmVyU3RhdGUSHAoYU0VSVkVSX1NUQVRFX1VOU1BFQ0lGSUVEEAASFgoSU0VSVkVSX1NUQVRFX1JFQURZEAEqlwEKC1J1bnRpbWVUaWVyEhwKGFJVTlRJTUVfVElFUl9VTlNQRUNJRklFRBAAEhcKE1JVTlRJTUVfVElFUl9QT0RNQU4QARIYChRSVU5USU1FX1RJRVJfTUlDUk9WTRACEiAKHFJVTlRJTUVfVElFUl9BUFBMRV9DT05UQUlORVIQAxIVChFSVU5USU1FX1RJRVJfSE9TVBAEKmgKDUVncmVzc1Bvc3R1cmUSHgoaRUdSRVNTX1BPU1RVUkVfVU5TUEVDSUZJRUQQABIYChRFR1JFU1NfUE9TVFVSRV9BUk1FRBABEh0KGUVHUkVTU19QT1NUVVJFX1VORU5GT1JDRUQQAiqCAgoRQWdlbnRTZXNzaW9uU3RhdGUSIwofQUdFTlRfU0VTU0lPTl9TVEFURV9VTlNQRUNJRklFRBAAEiAKHEFHRU5UX1NFU1NJT05fU1RBVEVfU1RBUlRJTkcQARIdChlBR0VOVF9TRVNTSU9OX1NUQVRFX1JFQURZEAISHwobQUdFTlRfU0VTU0lPTl9TVEFURV9XT1JLSU5HEAMSHwobQUdFTlRfU0VTU0lPTl9TVEFURV9TVE9QUEVEEAQSHwobQUdFTlRfU0VTU0lPTl9TVEFURV9FUlJPUkVEEAUSJAogQUdFTlRfU0VTU0lPTl9TVEFURV9ESVNDT05ORUNURUQQBirSAQoTQWdlbnRUb29sQ2FsbFN0YXR1cxImCiJBR0VOVF9UT09MX0NBTExfU1RBVFVTX1VOU1BFQ0lGSUVEEAASIgoeQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19QRU5ESU5HEAESJgoiQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19JTl9QUk9HUkVTUxACEiQKIEFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfQ09NUExFVEVEEAMSIQodQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19GQUlMRUQQBCq0AQoUQWdlbnRQbGFuRW50cnlTdGF0dXMSJwojQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfVU5TUEVDSUZJRUQQABIjCh9BR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19QRU5ESU5HEAESJwojQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfSU5fUFJPR1JFU1MQAhIlCiFBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19DT01QTEVURUQQAyqEAQoUU2Vzc2lvbkluamVjdGlvbktpbmQSJgoiU0VTU0lPTl9JTkpFQ1RJT05fS0lORF9VTlNQRUNJRklFRBAAEiAKHFNFU1NJT05fSU5KRUNUSU9OX0tJTkRfU1RFRVIQARIiCh5TRVNTSU9OX0lOSkVDVElPTl9LSU5EX0RFTElWRVIQAip0ChBTZXNzaW9uRXJyb3JLaW5kEiIKHlNFU1NJT05fRVJST1JfS0lORF9VTlNQRUNJRklFRBAAEhwKGFNFU1NJT05fRVJST1JfS0lORF9FUlJPUhABEh4KGlNFU1NJT05fRVJST1JfS0lORF9BQk9SVEVEEAIq8QEKCklzc3VlU3RhdGUSGwoXSVNTVUVfU1RBVEVfVU5TUEVDSUZJRUQQABIXChNJU1NVRV9TVEFURV9CQUNLTE9HEAESFAoQSVNTVUVfU1RBVEVfVE9ETxACEhYKEklTU1VFX1NUQVRFX1FVRVVFRBADEhcKE0lTU1VFX1NUQVRFX0JMT0NLRUQQBBIbChdJU1NVRV9TVEFURV9JTl9QUk9HUkVTUxAFEhkKFUlTU1VFX1NUQVRFX0lOX1JFVklFVxAGEhQKEElTU1VFX1NUQVRFX0RPTkUQBxIYChRJU1NVRV9TVEFURV9BUkNISVZFRBAIKpwBCg1Gb3JnZVByb3ZpZGVyEh4KGkZPUkdFX1BST1ZJREVSX1VOU1BFQ0lGSUVEEAASGQoVRk9SR0VfUFJPVklERVJfR0lUSFVCEAESGQoVRk9SR0VfUFJPVklERVJfR0lUTEFCEAISGgoWRk9SR0VfUFJPVklERVJfRk9SR0VKTxADEhkKFUZPUkdFX1BST1ZJREVSX0xJTkVBUhAEMtcSCg5Db21wYXNzU2VydmljZRJUCg1HZXRTZXJ2ZXJJbmZvEiAuY29tcGFzcy52MS5HZXRTZXJ2ZXJJbmZvUmVxdWVzdBohLmNvbXBhc3MudjEuR2V0U2VydmVySW5mb1Jlc3BvbnNlEj8KBldob0FtSRIZLmNvbXBhc3MudjEuV2hvQW1JUmVxdWVzdBoaLmNvbXBhc3MudjEuV2hvQW1JUmVzcG9uc2USUQoMR2V0VG91clN0YXRlEh8uY29tcGFzcy52MS5HZXRUb3VyU3RhdGVSZXF1ZXN0GiAuY29tcGFzcy52MS5HZXRUb3VyU3RhdGVSZXNwb25zZRJXCg5DbGFpbVRvdXJTdGFydBIhLmNvbXBhc3MudjEuQ2xhaW1Ub3VyU3RhcnRSZXF1ZXN0GiIuY29tcGFzcy52MS5DbGFpbVRvdXJTdGFydFJlc3BvbnNlElEKDFNldFRvdXJTdGF0ZRIfLmNvbXBhc3MudjEuU2V0VG91clN0YXRlUmVxdWVzdBogLmNvbXBhc3MudjEuU2V0VG91clN0YXRlUmVzcG9uc2USXAoPU3Vic2NyaWJlRXZlbnRzEiIuY29tcGFzcy52MS5TdWJzY3JpYmVFdmVudHNSZXF1ZXN0GiMuY29tcGFzcy52MS5TdWJzY3JpYmVFdmVudHNSZXNwb25zZTABEloKD0xpc3RCb2FyZElzc3VlcxIiLmNvbXBhc3MudjEuTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBojLmNvbXBhc3MudjEuTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USUQoMU2VhcmNoSXNzdWVzEh8uY29tcGFzcy52MS5TZWFyY2hJc3N1ZXNSZXF1ZXN0GiAuY29tcGFzcy52MS5TZWFyY2hJc3N1ZXNSZXNwb25zZRJyChdQcm92aXNpb25BZ2VudFdvcmtzcGFjZRIqLmNvbXBhc3MudjEuUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0GisuY29tcGFzcy52MS5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEmAKEVN0YXJ0QWdlbnRTZXNzaW9uEiQuY29tcGFzcy52MS5TdGFydEFnZW50U2Vzc2lvblJlcXVlc3QaJS5jb21wYXNzLnYxLlN0YXJ0QWdlbnRTZXNzaW9uUmVzcG9uc2USSwoKU3Bhd25BZ2VudBIdLmNvbXBhc3MudjEuU3Bhd25BZ2VudFJlcXVlc3QaHi5jb21wYXNzLnYxLlNwYXduQWdlbnRSZXNwb25zZRJdChBTdG9wQWdlbnRTZXNzaW9uEiMuY29tcGFzcy52MS5TdG9wQWdlbnRTZXNzaW9uUmVxdWVzdBokLmNvbXBhc3MudjEuU3RvcEFnZW50U2Vzc2lvblJlc3BvbnNlEloKD1NraXBCYXRjaFdpbmRvdxIiLmNvbXBhc3MudjEuU2tpcEJhdGNoV2luZG93UmVxdWVzdBojLmNvbXBhc3MudjEuU2tpcEJhdGNoV2luZG93UmVzcG9uc2USaQoUUmVtb3ZlQWdlbnRXb3Jrc3BhY2USJy5jb21wYXNzLnYxLlJlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBooLmNvbXBhc3MudjEuUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJjChJSZWxvYWRBZ2VudFNlc3Npb24SJS5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QaJi5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlc3BvbnNlElcKDkdldEFnZW50U3RhdHVzEiEuY29tcGFzcy52MS5HZXRBZ2VudFN0YXR1c1JlcXVlc3QaIi5jb21wYXNzLnYxLkdldEFnZW50U3RhdHVzUmVzcG9uc2USYgoVU3Vic2NyaWJlQWdlbnRTZXNzaW9uEiguY29tcGFzcy52MS5TdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0Gh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25GcmFtZTABEksKCklzc3VlVG9rZW4SHS5jb21wYXNzLnYxLklzc3VlVG9rZW5SZXF1ZXN0Gh4uY29tcGFzcy52MS5Jc3N1ZVRva2VuUmVzcG9uc2USVAoNUmVuYW1lQWNjb3VudBIgLmNvbXBhc3MudjEuUmVuYW1lQWNjb3VudFJlcXVlc3QaIS5jb21wYXNzLnYxLlJlbmFtZUFjY291bnRSZXNwb25zZRJOCgtSZXZva2VUb2tlbhIeLmNvbXBhc3MudjEuUmV2b2tlVG9rZW5SZXF1ZXN0Gh8uY29tcGFzcy52MS5SZXZva2VUb2tlblJlc3BvbnNlElcKDlB1dEFnZW50Q29uZmlnEiEuY29tcGFzcy52MS5QdXRBZ2VudENvbmZpZ1JlcXVlc3QaIi5jb21wYXNzLnYxLlB1dEFnZW50Q29uZmlnUmVzcG9uc2USYwoSR2V0QWdlbnRDb25maWdJbmZvEiUuY29tcGFzcy52MS5HZXRBZ2VudENvbmZpZ0luZm9SZXF1ZXN0GiYuY29tcGFzcy52MS5HZXRBZ2VudENvbmZpZ0luZm9SZXNwb25zZRJgChFEZWxldGVBZ2VudENvbmZpZxIkLmNvbXBhc3MudjEuRGVsZXRlQWdlbnRDb25maWdSZXF1ZXN0GiUuY29tcGFzcy52MS5EZWxldGVBZ2VudENvbmZpZ1Jlc3BvbnNlEl0KEFB1dE1vZGVsUmVnaXN0cnkSIy5jb21wYXNzLnYxLlB1dE1vZGVsUmVnaXN0cnlSZXF1ZXN0GiQuY29tcGFzcy52MS5QdXRNb2RlbFJlZ2lzdHJ5UmVzcG9uc2USXQoQR2V0TW9kZWxSZWdpc3RyeRIjLmNvbXBhc3MudjEuR2V0TW9kZWxSZWdpc3RyeVJlcXVlc3QaJC5jb21wYXNzLnYxLkdldE1vZGVsUmVnaXN0cnlSZXNwb25zZRJmChNEZWxldGVNb2RlbFJlZ2lzdHJ5EiYuY29tcGFzcy52MS5EZWxldGVNb2RlbFJlZ2lzdHJ5UmVxdWVzdBonLmNvbXBhc3MudjEuRGVsZXRlTW9kZWxSZWdpc3RyeVJlc3BvbnNlMt8CCg5TZWNyZXRzU2VydmljZRJICglTZXRTZWNyZXQSHC5jb21wYXNzLnYxLlNldFNlY3JldFJlcXVlc3QaHS5jb21wYXNzLnYxLlNldFNlY3JldFJlc3BvbnNlEk4KC0xpc3RTZWNyZXRzEh4uY29tcGFzcy52MS5MaXN0U2VjcmV0c1JlcXVlc3QaHy5jb21wYXNzLnYxLkxpc3RTZWNyZXRzUmVzcG9uc2USUQoMRGVsZXRlU2VjcmV0Eh8uY29tcGFzcy52MS5EZWxldGVTZWNyZXRSZXF1ZXN0GiAuY29tcGFzcy52MS5EZWxldGVTZWNyZXRSZXNwb25zZRJgChFMaXN0U2VydmVyU2VjcmV0cxIkLmNvbXBhc3MudjEuTGlzdFNlcnZlclNlY3JldHNSZXF1ZXN0GiUuY29tcGFzcy52MS5MaXN0U2VydmVyU2VjcmV0c1Jlc3BvbnNlYgZwcm90bzM", [file_google_protobuf_timestamp]); /** * @generated from message compass.v1.SetSecretRequest @@ -1875,6 +1875,61 @@ export type IssueTokenResponse = Message<"compass.v1.IssueTokenResponse"> & { export const IssueTokenResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_compass_v1_compass, 64); +/** + * RenameAccount: the admin-only path to rename a user or agent account. + * + * @generated from message compass.v1.RenameAccountRequest + */ +export type RenameAccountRequest = Message<"compass.v1.RenameAccountRequest"> & { + /** + * Current bare user handle or `owner/agent` handle. Empty is INVALID_ARGUMENT; + * bad qualified grammar is INVALID_ARGUMENT, unknown handles are NOT_FOUND, and + * the system account is PERMISSION_DENIED. + * + * @generated from field: string account_handle = 1; + */ + accountHandle: string; + + /** + * New bare handle; an agent keeps its current owner. Empty, bad grammar, or a + * qualified handle is INVALID_ARGUMENT; a taken handle is ALREADY_EXISTS. + * + * @generated from field: string new_handle = 2; + */ + newHandle: string; +}; + +/** + * Describes the message compass.v1.RenameAccountRequest. + * Use `create(RenameAccountRequestSchema)` to create a new message. + */ +export const RenameAccountRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 65); + +/** + * @generated from message compass.v1.RenameAccountResponse + */ +export type RenameAccountResponse = Message<"compass.v1.RenameAccountResponse"> & { + /** + * @generated from field: string account_id = 1; + */ + accountId: string; + + /** + * The new address: bare for users, `owner/agent` for agents. + * + * @generated from field: string handle = 2; + */ + handle: string; +}; + +/** + * Describes the message compass.v1.RenameAccountResponse. + * Use `create(RenameAccountResponseSchema)` to create a new message. + */ +export const RenameAccountResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 66); + /** * RevokeToken: the admin-only path to withdraw a bearer token by its value. * @@ -1897,7 +1952,7 @@ export type RevokeTokenRequest = Message<"compass.v1.RevokeTokenRequest"> & { * Use `create(RevokeTokenRequestSchema)` to create a new message. */ export const RevokeTokenRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 65); + messageDesc(file_compass_v1_compass, 67); /** * @generated from message compass.v1.RevokeTokenResponse @@ -1910,7 +1965,7 @@ export type RevokeTokenResponse = Message<"compass.v1.RevokeTokenResponse"> & { * Use `create(RevokeTokenResponseSchema)` to create a new message. */ export const RevokeTokenResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 66); + messageDesc(file_compass_v1_compass, 68); /** * PutAgentConfig: declare the fleet config bundle. The caller's identity is the @@ -1933,7 +1988,7 @@ export type PutAgentConfigRequest = Message<"compass.v1.PutAgentConfigRequest"> * Use `create(PutAgentConfigRequestSchema)` to create a new message. */ export const PutAgentConfigRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 67); + messageDesc(file_compass_v1_compass, 69); /** * @generated from message compass.v1.PutAgentConfigResponse @@ -1954,7 +2009,7 @@ export type PutAgentConfigResponse = Message<"compass.v1.PutAgentConfigResponse" * Use `create(PutAgentConfigResponseSchema)` to create a new message. */ export const PutAgentConfigResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 68); + messageDesc(file_compass_v1_compass, 70); /** * @generated from message compass.v1.GetAgentConfigInfoRequest @@ -1967,7 +2022,7 @@ export type GetAgentConfigInfoRequest = Message<"compass.v1.GetAgentConfigInfoRe * Use `create(GetAgentConfigInfoRequestSchema)` to create a new message. */ export const GetAgentConfigInfoRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 69); + messageDesc(file_compass_v1_compass, 71); /** * GetAgentConfigInfo: the current bundle's version and member names by top dir — @@ -2054,7 +2109,7 @@ export type GetAgentConfigInfoResponse = Message<"compass.v1.GetAgentConfigInfoR * Use `create(GetAgentConfigInfoResponseSchema)` to create a new message. */ export const GetAgentConfigInfoResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 70); + messageDesc(file_compass_v1_compass, 72); /** * @generated from message compass.v1.DeleteAgentConfigRequest @@ -2067,7 +2122,7 @@ export type DeleteAgentConfigRequest = Message<"compass.v1.DeleteAgentConfigRequ * Use `create(DeleteAgentConfigRequestSchema)` to create a new message. */ export const DeleteAgentConfigRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 71); + messageDesc(file_compass_v1_compass, 73); /** * @generated from message compass.v1.DeleteAgentConfigResponse @@ -2080,7 +2135,7 @@ export type DeleteAgentConfigResponse = Message<"compass.v1.DeleteAgentConfigRes * Use `create(DeleteAgentConfigResponseSchema)` to create a new message. */ export const DeleteAgentConfigResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 72); + messageDesc(file_compass_v1_compass, 74); /** * One candidate in a stable name's ordered chain: an upstream (provider, @@ -2108,7 +2163,7 @@ export type ModelCandidate = Message<"compass.v1.ModelCandidate"> & { * Use `create(ModelCandidateSchema)` to create a new message. */ export const ModelCandidateSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 73); + messageDesc(file_compass_v1_compass, 75); /** * The listing metadata a stable name carries, taken from its primary candidate @@ -2152,7 +2207,7 @@ export type ModelMetadata = Message<"compass.v1.ModelMetadata"> & { * Use `create(ModelMetadataSchema)` to create a new message. */ export const ModelMetadataSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 74); + messageDesc(file_compass_v1_compass, 76); /** * One stable name's registry entry: a human display name, the ordered candidate @@ -2182,7 +2237,7 @@ export type ModelRegistryEntry = Message<"compass.v1.ModelRegistryEntry"> & { * Use `create(ModelRegistryEntrySchema)` to create a new message. */ export const ModelRegistryEntrySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 75); + messageDesc(file_compass_v1_compass, 77); /** * The fleet model registry payload: the stable-name → entry map. The map key is @@ -2202,7 +2257,7 @@ export type ModelRegistry = Message<"compass.v1.ModelRegistry"> & { * Use `create(ModelRegistrySchema)` to create a new message. */ export const ModelRegistrySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 76); + messageDesc(file_compass_v1_compass, 78); /** * PutModelRegistry: declare the fleet model registry. The caller's identity is @@ -2233,7 +2288,7 @@ export type PutModelRegistryRequest = Message<"compass.v1.PutModelRegistryReques * Use `create(PutModelRegistryRequestSchema)` to create a new message. */ export const PutModelRegistryRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 77); + messageDesc(file_compass_v1_compass, 79); /** * @generated from message compass.v1.PutModelRegistryResponse @@ -2252,7 +2307,7 @@ export type PutModelRegistryResponse = Message<"compass.v1.PutModelRegistryRespo * Use `create(PutModelRegistryResponseSchema)` to create a new message. */ export const PutModelRegistryResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 78); + messageDesc(file_compass_v1_compass, 80); /** * @generated from message compass.v1.GetModelRegistryRequest @@ -2265,7 +2320,7 @@ export type GetModelRegistryRequest = Message<"compass.v1.GetModelRegistryReques * Use `create(GetModelRegistryRequestSchema)` to create a new message. */ export const GetModelRegistryRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 79); + messageDesc(file_compass_v1_compass, 81); /** * GetModelRegistry: the current registry version and payload. An unconfigured @@ -2290,7 +2345,7 @@ export type GetModelRegistryResponse = Message<"compass.v1.GetModelRegistryRespo * Use `create(GetModelRegistryResponseSchema)` to create a new message. */ export const GetModelRegistryResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 80); + messageDesc(file_compass_v1_compass, 82); /** * @generated from message compass.v1.DeleteModelRegistryRequest @@ -2303,7 +2358,7 @@ export type DeleteModelRegistryRequest = Message<"compass.v1.DeleteModelRegistry * Use `create(DeleteModelRegistryRequestSchema)` to create a new message. */ export const DeleteModelRegistryRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 81); + messageDesc(file_compass_v1_compass, 83); /** * @generated from message compass.v1.DeleteModelRegistryResponse @@ -2316,7 +2371,7 @@ export type DeleteModelRegistryResponse = Message<"compass.v1.DeleteModelRegistr * Use `create(DeleteModelRegistryResponseSchema)` to create a new message. */ export const DeleteModelRegistryResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 82); + messageDesc(file_compass_v1_compass, 84); /** * The Compass agent attribution parsed from the owner header at ingestion — a @@ -2349,7 +2404,7 @@ export type AgentAttribution = Message<"compass.v1.AgentAttribution"> & { * Use `create(AgentAttributionSchema)` to create a new message. */ export const AgentAttributionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 83); + messageDesc(file_compass_v1_compass, 85); /** * @generated from message compass.v1.ForgeRef @@ -2373,7 +2428,7 @@ export type ForgeRef = Message<"compass.v1.ForgeRef"> & { * Use `create(ForgeRefSchema)` to create a new message. */ export const ForgeRefSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_compass_v1_compass, 84); + messageDesc(file_compass_v1_compass, 86); /** * The board unit: a Compass Issue — the forge issue's fields PLUS the Compass @@ -2531,7 +2586,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, 85); + messageDesc(file_compass_v1_compass, 87); /** * A Compass pull request: the forge PR's fields plus the Compass agent @@ -2659,7 +2714,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, 86); + messageDesc(file_compass_v1_compass, 88); /** * The rolled-up CI + status-check state on a PR head — Compass-owned, populated @@ -2691,7 +2746,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, 87); + messageDesc(file_compass_v1_compass, 89); /** * @generated from message compass.v1.Check @@ -2725,7 +2780,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, 88); + messageDesc(file_compass_v1_compass, 90); /** * A PR diffstat (files/additions/deletions), carried on PullRequest — a @@ -2755,7 +2810,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, 89); + messageDesc(file_compass_v1_compass, 91); /** * The linked tracker issue — the projection target (DL-032); the @@ -2810,7 +2865,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, 90); + messageDesc(file_compass_v1_compass, 92); /** * The full review state the right-sidebar PR pane shows — every submitted review @@ -2864,7 +2919,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, 91); + messageDesc(file_compass_v1_compass, 93); /** * @generated from message compass.v1.ReviewThread @@ -2893,7 +2948,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, 92); + messageDesc(file_compass_v1_compass, 94); /** * @generated from message compass.v1.Comment @@ -2920,7 +2975,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, 93); + messageDesc(file_compass_v1_compass, 95); /** * The observed state of a pull request in its merge queue. @@ -2955,7 +3010,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, 94); + messageDesc(file_compass_v1_compass, 96); /** * How a resolved secret is delivered into the agent container. Mirrors @@ -3743,6 +3798,18 @@ export const CompassService: GenService<{ input: typeof IssueTokenRequestSchema; output: typeof IssueTokenResponseSchema; }, + /** + * Admin-gated: only an admin may rename an account; non-admin callers get + * permission_denied. Freed handles may be reclaimed; the owner of an agent + * does not change. + * + * @generated from rpc compass.v1.CompassService.RenameAccount + */ + renameAccount: { + methodKind: "unary"; + input: typeof RenameAccountRequestSchema; + output: typeof RenameAccountResponseSchema; + }, /** * Admin-gated: only an admin may revoke a token; a non-admin caller gets * permission_denied. Revocation is BY TOKEN VALUE (per-hash), not by diff --git a/proto/compass/v1/compass.proto b/proto/compass/v1/compass.proto index f9a261313..5f85ffc57 100644 --- a/proto/compass/v1/compass.proto +++ b/proto/compass/v1/compass.proto @@ -132,6 +132,11 @@ service CompassService { // out-of-band at first start). rpc IssueToken(IssueTokenRequest) returns (IssueTokenResponse); + // Admin-gated: only an admin may rename an account; non-admin callers get + // permission_denied. Freed handles may be reclaimed; the owner of an agent + // does not change. + rpc RenameAccount(RenameAccountRequest) returns (RenameAccountResponse); + // Admin-gated: only an admin may revoke a token; a non-admin caller gets // permission_denied. Revocation is BY TOKEN VALUE (per-hash), not by // account: the caller presents the token plaintext and the server hashes it, @@ -917,6 +922,23 @@ message IssueTokenResponse { string token = 1; } +// RenameAccount: the admin-only path to rename a user or agent account. +message RenameAccountRequest { + // Current bare user handle or `owner/agent` handle. Empty is INVALID_ARGUMENT; + // bad qualified grammar is INVALID_ARGUMENT, unknown handles are NOT_FOUND, and + // the system account is PERMISSION_DENIED. + string account_handle = 1; + // New bare handle; an agent keeps its current owner. Empty, bad grammar, or a + // qualified handle is INVALID_ARGUMENT; a taken handle is ALREADY_EXISTS. + string new_handle = 2; +} + +message RenameAccountResponse { + string account_id = 1; + // The new address: bare for users, `owner/agent` for agents. + string handle = 2; +} + // RevokeToken: the admin-only path to withdraw a bearer token by its value. message RevokeTokenRequest { // The bearer token plaintext to revoke. Carried on the wire only so the From 87d61e155c62544c74ee547727679dc07688cc11 Mon Sep 17 00:00:00 2001 From: mintaka Date: Sun, 11 Oct 2026 20:34:44 -0400 Subject: [PATCH 2/3] fix(server): re-key same-owner DMs, publish, and protect built-ins on rename (RIG-5151) Review follow-up. Same-owner DM names are built from handles, so after a rename a reclaimed handle resumed the old DM and its history. RenameHandle now re-keys every same-owner DM of the renamed account (agent-agent and owner-agent) in the rename transaction, under the owner's DM lock, and OpenDM reads both handles inside that lock. A rename publishes AccountChanged and a ChannelChanged per re-keyed DM. The bootstrap admin and its root supervisor are refused with FailedPrecondition, because startup finds them by handle. Ledger-impact: none Co-authored-by: Matt Wilkinson --- docs/specs/product/compass.md | 35 +++-- go/gen/compass/v1/compass.pb.go | 5 +- .../v1/compassv1connect/compass.connect.go | 10 +- go/internal/comms/comms.go | 24 +++- go/internal/comms/dm.go | 31 ++--- go/internal/comms/dm_open_pgtest_test.go | 126 +++++++++++++++++- .../store/account_handles_pgtest_test.go | 77 +++++++++-- go/internal/store/accounts.go | 103 +++++++++++--- go/internal/store/db/dm.sql.go | 64 +++++++++ go/internal/store/db/querier.go | 6 + go/internal/store/dm.go | 15 +++ go/internal/{comms => store}/dm_test.go | 17 +-- go/internal/store/queries/dm.sql | 24 ++++ go/server/network_door_test.go | 126 ++++++++++++++++-- go/server/serve.go | 2 +- go/server/service.go | 23 +++- .../src/gen/compass/v1/compass_pb.ts | 10 +- .../src/gen/compass/v1/compass_pb.ts | 10 +- proto/compass/v1/compass.proto | 10 +- 19 files changed, 608 insertions(+), 110 deletions(-) rename go/internal/{comms => store}/dm_test.go (59%) diff --git a/docs/specs/product/compass.md b/docs/specs/product/compass.md index 9f286fb16..26495d32b 100644 --- a/docs/specs/product/compass.md +++ b/docs/specs/product/compass.md @@ -107,20 +107,23 @@ the only path by which the server pushes state to a UI. `RenameAccount` SHALL resolve a bare user handle or an owner-qualified agent handle without viewer-scoped filtering. It SHALL rename the account's resolution -handle and display copy in one store transaction. The new handle MUST be -valid and bare; an agent's owner does not change. Unknown accounts return -`NotFound`, invalid input returns `InvalidArgument`, and a collision in the -account's handle tier returns `AlreadyExists`. The system account cannot be -renamed. The response returns the account id and its current address, qualified -by the owner's current handle for agents. A freed handle is immediately -available for reuse. +handle and display copy in one store transaction. Same-owner DMs SHALL be +re-keyed in that transaction while their messages remain with the original +accounts. The new handle MUST be valid and bare; an agent's owner does not +change. Unknown accounts return `NotFound`, invalid input returns +`InvalidArgument`, and a collision in the account's handle tier returns +`AlreadyExists`. The system account cannot be renamed. The bootstrap admin and +its root supervisor return `FailedPrecondition` and remain unchanged. The +response returns the account id and its current address, qualified by the owner's +current handle for agents. A freed handle is immediately available for reuse. #### Scenario: Rename an account at either handle tier - **Given** an existing user or an agent addressed as `owner/agent` - **When** an admin renames it to a valid bare handle -- **Then** both handle records change together, the old handle becomes - available, and the response returns the new address +- **Then** both handle records change together, same-owner DM names change + without moving messages, the old handle becomes available, and the response + returns the new address #### Scenario: Reject a rename that cannot be applied @@ -130,12 +133,26 @@ available for reuse. - **Then** it returns `NotFound`, `InvalidArgument`, or `AlreadyExists` as appropriate and leaves both stored handles unchanged +#### Scenario: Protect the bootstrap admin and root supervisor + +- **Given** the bootstrap admin or its root supervisor +- **When** an admin requests a rename +- **Then** the server returns `FailedPrecondition` and retains the current handle + #### Scenario: The admin cannot rename the system account - **Given** the system account - **When** an admin requests a rename - **Then** the server returns `PermissionDenied` and retains its handle +#### Scenario: Account rename publishes updated account and DM state + +- **Given** an account with a same-owner DM and a subscriber entitled to both +- **When** an admin renames the account +- **Then** the subscriber receives `AccountChanged` with the new handle and + `ChannelChanged` with the re-keyed DM name, while the channel ID and message + history remain unchanged + ### Requirement: A generated client is the only door to the server Client code SHALL reach the server only through the generated `compass.v1` diff --git a/go/gen/compass/v1/compass.pb.go b/go/gen/compass/v1/compass.pb.go index 2ed25bb63..a0099fbe7 100644 --- a/go/gen/compass/v1/compass.pb.go +++ b/go/gen/compass/v1/compass.pb.go @@ -4651,8 +4651,9 @@ func (x *IssueTokenResponse) GetToken() string { type RenameAccountRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Current bare user handle or `owner/agent` handle. Empty is INVALID_ARGUMENT; - // bad qualified grammar is INVALID_ARGUMENT, unknown handles are NOT_FOUND, and - // the system account is PERMISSION_DENIED. + // bad qualified grammar is INVALID_ARGUMENT, unknown handles are NOT_FOUND, + // and the system account is PERMISSION_DENIED. The bootstrap admin and its + // root supervisor are protected with FAILED_PRECONDITION. AccountHandle string `protobuf:"bytes,1,opt,name=account_handle,json=accountHandle,proto3" json:"account_handle,omitempty"` // New bare handle; an agent keeps its current owner. Empty, bad grammar, or a // qualified handle is INVALID_ARGUMENT; a taken handle is ALREADY_EXISTS. diff --git a/go/gen/compass/v1/compassv1connect/compass.connect.go b/go/gen/compass/v1/compassv1connect/compass.connect.go index d681251d4..bce5c996d 100644 --- a/go/gen/compass/v1/compassv1connect/compass.connect.go +++ b/go/gen/compass/v1/compassv1connect/compass.connect.go @@ -240,8 +240,9 @@ type CompassServiceClient interface { // out-of-band at first start). IssueToken(context.Context, *connect.Request[v1.IssueTokenRequest]) (*connect.Response[v1.IssueTokenResponse], error) // Admin-gated: only an admin may rename an account; non-admin callers get - // permission_denied. Freed handles may be reclaimed; the owner of an agent - // does not change. + // permission_denied. Same-owner DMs are re-keyed; freed handles may be + // reclaimed, and an agent's owner does not change. The bootstrap admin and + // its root supervisor are protected from rename with failed_precondition. RenameAccount(context.Context, *connect.Request[v1.RenameAccountRequest]) (*connect.Response[v1.RenameAccountResponse], error) // Admin-gated: only an admin may revoke a token; a non-admin caller gets // permission_denied. Revocation is BY TOKEN VALUE (per-hash), not by @@ -730,8 +731,9 @@ type CompassServiceHandler interface { // out-of-band at first start). IssueToken(context.Context, *connect.Request[v1.IssueTokenRequest]) (*connect.Response[v1.IssueTokenResponse], error) // Admin-gated: only an admin may rename an account; non-admin callers get - // permission_denied. Freed handles may be reclaimed; the owner of an agent - // does not change. + // permission_denied. Same-owner DMs are re-keyed; freed handles may be + // reclaimed, and an agent's owner does not change. The bootstrap admin and + // its root supervisor are protected from rename with failed_precondition. RenameAccount(context.Context, *connect.Request[v1.RenameAccountRequest]) (*connect.Response[v1.RenameAccountResponse], error) // Admin-gated: only an admin may revoke a token; a non-admin caller gets // permission_denied. Revocation is BY TOKEN VALUE (per-hash), not by diff --git a/go/internal/comms/comms.go b/go/internal/comms/comms.go index daf0e8e25..a50de8b45 100644 --- a/go/internal/comms/comms.go +++ b/go/internal/comms/comms.go @@ -84,6 +84,22 @@ func NewComms(st *store.Store, bus commsBus, fab fabric.EventFabric, adminID sto return &Comms{store: st, bus: bus, fabric: fab, fabricPublishFailures: failures, adminID: adminID} } +// PublishAccountRename emits account and same-owner DM changes after the +// rename transaction commits. Channels are fetched in the committed state so +// every event carries their new deterministic names. +func (c *Comms) PublishAccountRename(ctx context.Context, account store.Account, channelIDs []store.ChannelID) { + ctx = context.WithoutCancel(ctx) + c.publishAccountChanged(account) + for _, channelID := range channelIDs { + channel, err := c.store.GetChannel(ctx, channelID) + if err != nil { + slog.ErrorContext(ctx, "comms: reading renamed dm channel for event", "channel_id", channelID, "err", err) + continue + } + c.publishChannelChanged(channel, nil) + } +} + // SetPresenceSource wires the in-memory presence enum source GetRoster joins // against, AFTER both Comms and the hub exist — a post-construction setter that // breaks the comms<->hub construction cycle (comms is built before the hub @@ -690,13 +706,7 @@ func (c *Comms) OpenDM( peerOwner := peer.Agent.OwnerUserID host := callerOwner var name string - if peerOwner == callerOwner { - callerAcc, err := c.store.GetAccount(ctx, caller) - if err != nil { - return nil, edgeError(err) - } - name = dmChannelName(callerAcc.Handle, peer.Handle) - } else { + if peerOwner != callerOwner { if peerOwner < host { host = peerOwner } diff --git a/go/internal/comms/dm.go b/go/internal/comms/dm.go index 90faf7321..c394685a6 100644 --- a/go/internal/comms/dm.go +++ b/go/internal/comms/dm.go @@ -8,26 +8,6 @@ import ( "github.com/RigelBuild/compass/go/internal/store" ) -// dmChannelName derives the deterministic sorted-handle pair name for a peer DM. -// Handles are sorted lexicographically so open(a,b) and open(b,a) resolve the -// same channel (store dm_pgtest_test.go proves reversed member order → same name). -// -// The separator is `:`, a byte the handle grammar (store handle.go handleRE -// `^[a-z0-9][a-z0-9._-]*$`) excludes — so no handle can contain it and the split -// is unambiguous. A hyphen delimiter would NOT be injective: handles may contain -// `--` (the grammar permits consecutive hyphens), so `dm--a--b--c` is ambiguous -// between the pairs {a, b--c} and {a--b, c}, which would resolve two distinct -// logical DMs onto ONE channel and cross-add members (a same-owner private-DM -// confidentiality break). `:` cannot appear in a handle, so `dm::` maps -// each unordered pair to exactly one name. -func dmChannelName(h1, h2 string) string { - lo, hi := h1, h2 - if lo > hi { - lo, hi = hi, lo - } - return "dm:" + lo + ":" + hi -} - // crossOwnerDMName keys a peer DM on both party ids, not reclaimable handles. func crossOwnerDMName(a, b store.AccountID) string { lo, hi := string(a), string(b) @@ -48,6 +28,17 @@ func (c *Comms) openDMTx(ctx context.Context, owner, callerOwner, peerOwner stor if err := store.LockOwnerDMTx(ctx, tx, owner); err != nil { return err } + if callerOwner == peerOwner { + callerHandle, err := c.store.AccountHandleTx(ctx, tx, members[0]) + if err != nil { + return err + } + peerCurrentHandle, err := c.store.AccountHandleTx(ctx, tx, members[1]) + if err != nil { + return err + } + name = store.DMChannelName(callerHandle, peerCurrentHandle) + } if callerOwner != peerOwner { peered, err := c.store.OwnersPeeredTx(ctx, tx, callerOwner, peerOwner) if err != nil { diff --git a/go/internal/comms/dm_open_pgtest_test.go b/go/internal/comms/dm_open_pgtest_test.go index 3629173dc..07f000319 100644 --- a/go/internal/comms/dm_open_pgtest_test.go +++ b/go/internal/comms/dm_open_pgtest_test.go @@ -893,7 +893,7 @@ func TestOpenDMAsAccountResolvesOrCreates(t *testing.T) { // the first's channel, cross-adding members (a same-owner private-DM // confidentiality break). The `:` separator is excluded from the handle grammar, // so the two pairs map to distinct names (dm:a:b--c vs dm:a--b:c) and must -// resolve to DISTINCT channels. RED if dmChannelName reverts to a `-` delimiter. +// resolve to DISTINCT channels. RED if DMChannelName reverts to a `-` delimiter. func TestOpenDMDoubleHyphenHandlesResolveDistinctChannels(t *testing.T) { svc, st := newHandler(t) ctx := context.Background() @@ -918,7 +918,7 @@ func TestOpenDMDoubleHyphenHandlesResolveDistinctChannels(t *testing.T) { t.Fatalf("second pair created = false, want true (a distinct pair must mint its own DM, not resume)") } if second.Msg.GetChannel().GetId() == first.Msg.GetChannel().GetId() { - t.Fatalf("the two distinct pairs collided onto one channel %q — dmChannelName is not injective", first.Msg.GetChannel().GetId()) + t.Fatalf("the two distinct pairs collided onto one channel %q — DMChannelName is not injective", first.Msg.GetChannel().GetId()) } // And the first DM's membership is uncorrupted: exactly a + b--c (+ their // owner), never the second pair's ab or c. @@ -974,6 +974,75 @@ func TestOpenDMResumeEmitsNoChannelChanged(t *testing.T) { } } +// TestRenameReclaimsSameOwnerDMWithoutSharingHistory proves that a new account +// cannot resume a renamed account's private DM history. +func TestRenameReclaimsSameOwnerDMWithoutSharingHistory(t *testing.T) { + svc, st := newHandler(t) + ctx := t.Context() + owner := mustUser(t, st, "owner") + original := mustAgent(t, st, owner.ID, "alice") + bob := mustAgent(t, st, owner.ID, "bob") + + first, err := svc.OpenDM(WithActor(ctx, original.ID), connect.NewRequest(&compassv1.OpenDMRequest{PeerHandle: "bob"})) + if err != nil { + t.Fatalf("OpenDM(original -> bob): %v", err) + } + oldID := store.ChannelID(first.Msg.GetChannel().GetId()) + if _, err := svc.PostMessage(WithActor(ctx, original.ID), connect.NewRequest(&compassv1.PostMessageRequest{ + Container: &compassv1.PostMessageRequest_ChannelId{ChannelId: string(oldID)}, + Topic: &compassv1.PostMessageRequest_TopicName{TopicName: "general"}, + CreateTopic: true, + Blocks: textBlocks("private history"), + })); err != nil { + t.Fatalf("PostMessage(original DM): %v", err) + } + + if _, _, err := st.RenameHandle(ctx, original.ID, "alice-renamed"); err != nil { + t.Fatalf("RenameHandle(original): %v", err) + } + messages, err := st.ListMessages(ctx, store.ListMessagesQuery{Actor: original.ID, ChannelID: oldID}) + if err != nil { + t.Fatalf("ListMessages(old DM after rename): %v", err) + } + if len(messages) != 1 || messages[0].Blocks[0].Text == nil || *messages[0].Blocks[0].Text != "private history" { + t.Fatalf("old DM history after rename = %+v, want the original private message", messages) + } + reclaimer := mustAgent(t, st, owner.ID, "alice") + + newDM, err := svc.OpenDM(WithActor(ctx, reclaimer.ID), connect.NewRequest(&compassv1.OpenDMRequest{PeerHandle: "bob"})) + if err != nil { + t.Fatalf("OpenDM(reclaimer -> bob): %v", err) + } + if !newDM.Msg.GetCreated() || newDM.Msg.GetChannel().GetId() == string(oldID) { + t.Fatalf("reclaimer DM = (id %q, created %v), want a new channel distinct from %q", newDM.Msg.GetChannel().GetId(), newDM.Msg.GetCreated(), oldID) + } + oldChannel, err := st.GetChannel(ctx, oldID) + if err != nil { + t.Fatalf("GetChannel(old DM): %v", err) + } + if containsString(accountIDsToStrings(oldChannel.MemberAccountIDs), string(reclaimer.ID)) { + t.Fatalf("old DM members %v include reclaimed account %q", oldChannel.MemberAccountIDs, reclaimer.ID) + } + reclaimedMessages, err := st.ListMessages(ctx, store.ListMessagesQuery{Actor: reclaimer.ID, ChannelID: oldID}) + if err != nil { + t.Fatalf("ListMessages(old DM as reclaimer): %v", err) + } + if len(reclaimedMessages) != 0 { + t.Fatalf("reclaimer can read old DM history: %+v", reclaimedMessages) + } + + resumed, err := svc.OpenDM(WithActor(ctx, original.ID), connect.NewRequest(&compassv1.OpenDMRequest{PeerHandle: "bob"})) + if err != nil { + t.Fatalf("OpenDM(renamed original -> bob): %v", err) + } + if resumed.Msg.GetCreated() || resumed.Msg.GetChannel().GetId() != string(oldID) { + t.Fatalf("renamed original DM = (id %q, created %v), want resume of %q", resumed.Msg.GetChannel().GetId(), resumed.Msg.GetCreated(), oldID) + } + if bob.ID == reclaimer.ID { + t.Fatal("test setup returned duplicate account ids") + } +} + // Each party posts under its own actor, and each read must return BOTH posts — // red if DM membership fails to grant a party read on the peer's turn. func TestOpenDMPostAndReadBothParties(t *testing.T) { @@ -1193,3 +1262,56 @@ func TestConvertChannelNameIsNoOpOnNonDM(t *testing.T) { t.Fatalf("members = %v, want the added t15newcomer %s", ch.GetMemberAccountIds(), newcomer.ID) } } + +// TestRenameReclaimsOwnerAgentDMWithoutResume covers the owner-agent DM, also +// handle-keyed: a reclaimed agent handle must not resume the owner's old DM. +func TestRenameReclaimsOwnerAgentDMWithoutResume(t *testing.T) { + svc, st := newHandler(t) + ctx := t.Context() + owner := mustUser(t, st, "probe-owner") + alice := mustAgent(t, st, owner.ID, "alice") + first, err := svc.OpenDM(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.OpenDMRequest{PeerHandle: "alice"})) + if err != nil { + t.Fatalf("OpenDM(owner -> alice): %v", err) + } + if _, _, err := st.RenameHandle(ctx, alice.ID, "alice-old"); err != nil { + t.Fatalf("RenameHandle(alice): %v", err) + } + reclaimer := mustAgent(t, st, owner.ID, "alice") + second, err := svc.OpenDM(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.OpenDMRequest{PeerHandle: "alice"})) + if err != nil { + t.Fatalf("OpenDM(owner -> reclaimed alice): %v", err) + } + if second.Msg.GetChannel().GetId() == first.Msg.GetChannel().GetId() { + t.Fatalf("owner's DM with the reclaimed handle resumed the renamed agent's channel %q", first.Msg.GetChannel().GetId()) + } + if containsString(second.Msg.GetChannel().GetMemberAccountIds(), string(alice.ID)) { + t.Fatalf("new DM members %v include the renamed agent", second.Msg.GetChannel().GetMemberAccountIds()) + } + if !containsString(second.Msg.GetChannel().GetMemberAccountIds(), string(reclaimer.ID)) { + t.Fatalf("new DM members %v omit the reclaimer", second.Msg.GetChannel().GetMemberAccountIds()) + } +} + +// TestRenameOwnerResumesOwnerAgentDM: renaming the owner re-keys its DMs with its +// agents, so the renamed owner resumes the same channel instead of forking one. +func TestRenameOwnerResumesOwnerAgentDM(t *testing.T) { + svc, st := newHandler(t) + ctx := t.Context() + owner := mustUser(t, st, "rename-owner") + mustAgent(t, st, owner.ID, "helper") + first, err := svc.OpenDM(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.OpenDMRequest{PeerHandle: "helper"})) + if err != nil { + t.Fatalf("OpenDM(owner -> helper): %v", err) + } + if _, _, err := st.RenameHandle(ctx, owner.ID, "rename-owner-2"); err != nil { + t.Fatalf("RenameHandle(owner): %v", err) + } + again, err := svc.OpenDM(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.OpenDMRequest{PeerHandle: "helper"})) + if err != nil { + t.Fatalf("OpenDM(renamed owner -> helper): %v", err) + } + if again.Msg.GetCreated() || again.Msg.GetChannel().GetId() != first.Msg.GetChannel().GetId() { + t.Fatalf("renamed owner DM = (id %q, created %v), want resume of %q", again.Msg.GetChannel().GetId(), again.Msg.GetCreated(), first.Msg.GetChannel().GetId()) + } +} diff --git a/go/internal/store/account_handles_pgtest_test.go b/go/internal/store/account_handles_pgtest_test.go index bdde0daf3..d823b3e26 100644 --- a/go/internal/store/account_handles_pgtest_test.go +++ b/go/internal/store/account_handles_pgtest_test.go @@ -22,9 +22,12 @@ import ( "context" "errors" "fmt" + "slices" "strings" "testing" + "github.com/jackc/pgx/v5" + "github.com/RigelBuild/compass/go/internal/pgtest" ) @@ -112,6 +115,9 @@ func TestAgentHandleMayOverlapUserHandle(t *testing.T) { // TestHandleRenameInPlaceBothTiers: rename updates both the resolution key and // the accounts.handle display copy, while freeing the previous handle. +// +// Mutation: skipping RenameAccountDisplayHandle leaves accounts.handle stale; +// the Account.Handle assertions below then fail. func TestHandleRenameInPlaceBothTiers(t *testing.T) { ctx := t.Context() s := newTestStore(t) @@ -120,7 +126,7 @@ func TestHandleRenameInPlaceBothTiers(t *testing.T) { agent := mustAgent(t, s, owner.ID, "old-agent") // User rename (global tier). - got, err := s.RenameHandle(ctx, user.ID, "newname") + got, _, err := s.RenameHandle(ctx, user.ID, "newname") if err != nil || got.Handle != "newname" { t.Fatalf("RenameHandle(user, newname) = (%+v, %v), want handle newname", got, err) } @@ -134,7 +140,7 @@ func TestHandleRenameInPlaceBothTiers(t *testing.T) { assertAccountDisplayHandle(t, ctx, s, user.ID, "newname") // Agent rename (per-owner tier). - gotA, err := s.RenameHandle(ctx, agent.ID, "new-agent") + gotA, _, err := s.RenameHandle(ctx, agent.ID, "new-agent") if err != nil || gotA.Handle != "new-agent" { t.Fatalf("RenameHandle(agent, new-agent) = (%+v, %v), want handle new-agent", gotA, err) } @@ -148,13 +154,59 @@ func TestHandleRenameInPlaceBothTiers(t *testing.T) { assertAccountDisplayHandle(t, ctx, s, agent.ID, "new-agent") } +// TestRenameHandleRekeysSameOwnerDMWithoutChangingMembership proves an agent +// rename keeps the channel ID and parties while deriving its name from the new +// handle. +func TestRenameHandleRekeysSameOwnerDMWithoutChangingMembership(t *testing.T) { + ctx := t.Context() + s := newTestStore(t) + owner := mustUser(t, s, "dm-owner") + alice := mustAgent(t, s, owner.ID, "alice") + bob := mustAgent(t, s, owner.ID, "bob") + var channelID ChannelID + if err := s.WithTx(ctx, func(tx pgx.Tx) error { + if err := LockOwnerDMTx(ctx, tx, owner.ID); err != nil { + return err + } + group, err := s.EnsureOwnerDMGroupTx(ctx, tx, owner.ID) + if err != nil { + return err + } + channelID, _, err = s.UpsertDMChannelTx(ctx, tx, DMChannelSpec{ + GroupID: group, + Name: DMChannelName(alice.Handle, bob.Handle), + Members: []AccountID{alice.ID, bob.ID}, + }) + return err + }); err != nil { + t.Fatalf("open DM: %v", err) + } + renamed, rekeyed, err := s.RenameHandle(ctx, alice.ID, "alice-new") + if err != nil { + t.Fatalf("RenameHandle: %v", err) + } + if renamed.Handle != "alice-new" || len(rekeyed) != 1 || rekeyed[0] != channelID { + t.Fatalf("RenameHandle = (%q, %v), want handle alice-new and the same DM ID %q", renamed.Handle, rekeyed, channelID) + } + channel, err := s.GetChannel(ctx, channelID) + if err != nil { + t.Fatalf("GetChannel: %v", err) + } + if channel.Name != DMChannelName("alice-new", bob.Handle) { + t.Fatalf("renamed DM name = %q, want %q", channel.Name, DMChannelName("alice-new", bob.Handle)) + } + if !slices.Contains(channel.MemberAccountIDs, alice.ID) || !slices.Contains(channel.MemberAccountIDs, bob.ID) || !slices.Contains(channel.MemberAccountIDs, owner.ID) { + t.Fatalf("renamed DM members = %v, want both agents and their owner", channel.MemberAccountIDs) + } +} + // TestHandleReclaimBothTiers: a freed handle may be re-registered at either tier. func TestHandleReclaimBothTiers(t *testing.T) { ctx := t.Context() s := newTestStore(t) user := mustUser(t, s, "matt") // Free the user handle by renaming it away, then a new user reclaims "matt". - if _, err := s.RenameHandle(ctx, user.ID, "matt-old"); err != nil { + if _, _, err := s.RenameHandle(ctx, user.ID, "matt-old"); err != nil { t.Fatalf("rename user away: %v", err) } reclaimer, err := s.CreateUser(ctx, NewUser{Handle: "matt", DisplayName: "New Matt"}) @@ -170,7 +222,7 @@ func TestHandleReclaimBothTiers(t *testing.T) { // Agent tier: free `owner/ux`, a new agent reclaims it under the same owner. owner := mustUser(t, s, "owner") agent := mustAgent(t, s, owner.ID, "ux") - if _, err := s.RenameHandle(ctx, agent.ID, "ux-old"); err != nil { + if _, _, err := s.RenameHandle(ctx, agent.ID, "ux-old"); err != nil { t.Fatalf("rename agent away: %v", err) } reAgent, err := s.CreateAgent(ctx, owner.ID, NewAgent{Handle: "ux", DisplayName: "New UX"}) @@ -186,6 +238,9 @@ func TestHandleReclaimBothTiers(t *testing.T) { // TestRenameHandleCollisionsRespectHandleTiers proves conflicts remain local to // each namespace and a failed update leaves both accounts unchanged. +// +// Mutation: skipping mapAccountHandleUniqueViolation changes ErrConflict into a +// raw database error; the first assertion then fails. func TestRenameHandleCollisionsRespectHandleTiers(t *testing.T) { ctx := t.Context() s := newTestStore(t) @@ -216,7 +271,7 @@ func TestRenameHandleCollisionsRespectHandleTiers(t *testing.T) { t.Run("user collision rolls back", func(t *testing.T) { user := createUser("user-before") taken := createUser("user-taken") - if _, err := s.RenameHandle(ctx, user.ID, taken.Handle); !errors.Is(err, ErrConflict) { + if _, _, err := s.RenameHandle(ctx, user.ID, taken.Handle); !errors.Is(err, ErrConflict) { t.Fatalf("RenameHandle(user, taken) = %v, want ErrConflict", err) } assertBothHandles(user, "user-before") @@ -227,7 +282,7 @@ func TestRenameHandleCollisionsRespectHandleTiers(t *testing.T) { owner := createUser("same-owner") agent := createAgent(owner.ID, "agent-before") sibling := createAgent(owner.ID, "agent-taken") - if _, err := s.RenameHandle(ctx, agent.ID, sibling.Handle); !errors.Is(err, ErrConflict) { + if _, _, err := s.RenameHandle(ctx, agent.ID, sibling.Handle); !errors.Is(err, ErrConflict) { t.Fatalf("RenameHandle(agent, sibling handle) = %v, want ErrConflict", err) } assertBothHandles(agent, "agent-before") @@ -239,7 +294,7 @@ func TestRenameHandleCollisionsRespectHandleTiers(t *testing.T) { ownerB := createUser("owner-b") agent := createAgent(ownerA.ID, "agent-a") other := createAgent(ownerB.ID, "agent-b") - got, err := s.RenameHandle(ctx, agent.ID, other.Handle) + got, _, err := s.RenameHandle(ctx, agent.ID, other.Handle) if err != nil || got.Handle != other.Handle { t.Fatalf("RenameHandle(agent, different owner handle) = (%+v, %v)", got, err) } @@ -251,7 +306,7 @@ func TestRenameHandleCollisionsRespectHandleTiers(t *testing.T) { user := createUser("shared-name") owner := createUser("agent-owner") agent := createAgent(owner.ID, "agent-name") - got, err := s.RenameHandle(ctx, agent.ID, user.Handle) + got, _, err := s.RenameHandle(ctx, agent.ID, user.Handle) if err != nil || got.Handle != user.Handle { t.Fatalf("RenameHandle(agent, user handle) = (%+v, %v)", got, err) } @@ -266,10 +321,10 @@ func TestRenameHandleRejectsInvalidHandleAndUnknownID(t *testing.T) { ctx := t.Context() s := newTestStore(t) user := mustUser(t, s, "rename-target") - if _, err := s.RenameHandle(ctx, user.ID, "owner/agent"); !errors.Is(err, ErrInvalidArgument) { + if _, _, err := s.RenameHandle(ctx, user.ID, "owner/agent"); !errors.Is(err, ErrInvalidArgument) { t.Fatalf("RenameHandle(qualified handle) = %v, want ErrInvalidArgument", err) } - if _, err := s.RenameHandle(ctx, "missing-account", "valid-name"); !errors.Is(err, ErrNotFound) { + if _, _, err := s.RenameHandle(ctx, "missing-account", "valid-name"); !errors.Is(err, ErrNotFound) { t.Fatalf("RenameHandle(unknown id) = %v, want ErrNotFound", err) } } @@ -278,7 +333,7 @@ func TestRenameHandleCurrentHandleIsNoop(t *testing.T) { ctx := t.Context() s := newTestStore(t) user := mustUser(t, s, "same-name") - got, err := s.RenameHandle(ctx, user.ID, user.Handle) + got, _, err := s.RenameHandle(ctx, user.ID, user.Handle) if err != nil || got.ID != user.ID || got.Handle != user.Handle { t.Fatalf("RenameHandle(current handle) = (%+v, %v), want unchanged account", got, err) } diff --git a/go/internal/store/accounts.go b/go/internal/store/accounts.go index 1f63233a6..64d6499bc 100644 --- a/go/internal/store/accounts.go +++ b/go/internal/store/accounts.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" "github.com/RigelBuild/compass/go/internal/store/db" @@ -517,51 +518,113 @@ func (s *Store) AccountHandle(ctx context.Context, id AccountID) (string, error) return handle, nil } +// AccountHandleTx resolves an account's current handle through the caller's +// transaction. It is used after the per-owner DM lock so an open that waited +// behind a rename cannot create a channel under its stale pre-lock name. +func (s *Store) AccountHandleTx(ctx context.Context, tx pgx.Tx, id AccountID) (string, error) { + handle, err := db.New(tx).GetAccountHandle(ctx, string(id)) + if err != nil { + if noRows(err) { + return "", fmt.Errorf("%w: account handle %q", ErrNotFound, id) + } + return "", fmt.Errorf("store: get account handle in transaction: %w", err) + } + return handle, nil +} + // RenameHandle changes an account's resolution and display handles atomically. +// Same-owner DM names are re-keyed in the same transaction so a freed handle +// cannot make a new account inherit the renamed account's channel history. // A handle freed by the rename is immediately available for reuse. -func (s *Store) RenameHandle(ctx context.Context, id AccountID, newHandle string) (Account, error) { +func (s *Store) RenameHandle(ctx context.Context, id AccountID, newHandle string) (Account, []ChannelID, error) { if err := validateHandle(newHandle); err != nil { - return Account{}, err + return Account{}, nil, err } tx, err := s.beginTenantTx(ctx) if err != nil { - return Account{}, fmt.Errorf("store: begin rename handle: %w", err) + return Account{}, nil, fmt.Errorf("store: begin rename handle: %w", err) } defer func() { _ = tx.Rollback(ctx) }() qtx := s.q.WithTx(tx) + // A user owns its DM group, so its own id is the lock key; an agent locks its owner's. + ownerUserID, err := qtx.ResolveOwner(ctx, string(id)) + if err != nil { + return Account{}, nil, fmt.Errorf("store: resolve owner before rename: %w", err) + } + if err := LockOwnerDMTx(ctx, tx, AccountID(ownerUserID)); err != nil { + return Account{}, nil, err + } + currentHandle, err := qtx.GetAccountHandle(ctx, string(id)) if err != nil { if noRows(err) { - return Account{}, fmt.Errorf("%w: account %q", ErrNotFound, id) + return Account{}, nil, fmt.Errorf("%w: account %q", ErrNotFound, id) } - return Account{}, fmt.Errorf("store: get handle before rename: %w", err) + return Account{}, nil, fmt.Errorf("store: get handle before rename: %w", err) } + dmChannels, err := qtx.AccountDMChannelsForRename(ctx, string(id)) + if err != nil { + return Account{}, nil, fmt.Errorf("store: list account dms before rename: %w", err) + } + var channelIDs []ChannelID if currentHandle != newHandle { - rows, err := qtx.RenameAccountHandle(ctx, db.RenameAccountHandleParams{AccountID: string(id), Handle: newHandle}) + channelIDs, err = renameHandleTx(ctx, qtx, id, newHandle, dmChannels) if err != nil { - if mapped := mapAccountHandleUniqueViolation(err, newHandle); mapped != nil { - return Account{}, mapped - } - return Account{}, fmt.Errorf("store: update account handle: %w", err) + return Account{}, nil, err } - if rows == 0 { - return Account{}, fmt.Errorf("%w: account %q", ErrNotFound, id) + } + + if err := tx.Commit(ctx); err != nil { + return Account{}, nil, fmt.Errorf("store: commit rename handle: %w", err) + } + account, err := s.GetAccount(ctx, id) + if err != nil { + return Account{}, nil, err + } + return account, channelIDs, nil +} + +func renameHandleTx( + ctx context.Context, + qtx *db.Queries, + id AccountID, + newHandle string, + dmChannels []db.AccountDMChannelsForRenameRow, +) ([]ChannelID, error) { + rows, err := qtx.RenameAccountHandle(ctx, db.RenameAccountHandleParams{AccountID: string(id), Handle: newHandle}) + if err != nil { + if mapped := mapAccountHandleUniqueViolation(err, newHandle); mapped != nil { + return nil, mapped } - rows, err = qtx.RenameAccountDisplayHandle(ctx, db.RenameAccountDisplayHandleParams{ID: string(id), Handle: newHandle}) + return nil, fmt.Errorf("store: update account handle: %w", err) + } + if rows == 0 { + return nil, fmt.Errorf("%w: account %q", ErrNotFound, id) + } + rows, err = qtx.RenameAccountDisplayHandle(ctx, db.RenameAccountDisplayHandleParams{ID: string(id), Handle: newHandle}) + if err != nil { + return nil, fmt.Errorf("store: update account display handle: %w", err) + } + if rows == 0 { + return nil, fmt.Errorf("%w: account %q", ErrNotFound, id) + } + channelIDs := make([]ChannelID, 0, len(dmChannels)) + for _, channel := range dmChannels { + rows, err := qtx.RenameDMChannel(ctx, db.RenameDMChannelParams{ + ID: channel.ID, + Name: DMChannelName(newHandle, channel.Handle), + }) if err != nil { - return Account{}, fmt.Errorf("store: update account display handle: %w", err) + return nil, fmt.Errorf("store: re-key dm channel %q: %w", channel.ID, err) } if rows == 0 { - return Account{}, fmt.Errorf("%w: account %q", ErrNotFound, id) + return nil, fmt.Errorf("%w: dm channel %q", ErrNotFound, channel.ID) } + channelIDs = append(channelIDs, ChannelID(channel.ID)) } - - if err := tx.Commit(ctx); err != nil { - return Account{}, fmt.Errorf("store: commit rename handle: %w", err) - } - return s.GetAccount(ctx, id) + return channelIDs, nil } // ResolveOwner resolves a caller to the user account it acts under. It mirrors diff --git a/go/internal/store/db/dm.sql.go b/go/internal/store/db/dm.sql.go index b2f7f99d7..cde261339 100644 --- a/go/internal/store/db/dm.sql.go +++ b/go/internal/store/db/dm.sql.go @@ -11,6 +11,53 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const accountDMChannelsForRename = `-- name: AccountDMChannelsForRename :many +SELECT c.id, other.account_id, other_handle.handle +FROM channels c +JOIN channel_groups cg ON cg.id = c.group_id AND cg.name = '__dm__' + AND cg.parent_group_id IS NULL AND cg.visibility = 0 +JOIN channel_members subject ON subject.channel_id = c.id AND subject.account_id = $1 +JOIN channel_members other ON other.channel_id = c.id AND other.account_id <> $1 +JOIN account_handles other_handle ON other_handle.account_id = other.account_id +JOIN account_handles subject_handle ON subject_handle.account_id = subject.account_id +WHERE c.kind = 1 + AND cg.owner_user_id = COALESCE((SELECT owner_user_id FROM agent_accounts WHERE account_id = $1), $1) + AND c.name IN ( + 'dm:' || subject_handle.handle || ':' || other_handle.handle, + 'dm:' || other_handle.handle || ':' || subject_handle.handle + ) +ORDER BY c.id, other.account_id +` + +type AccountDMChannelsForRenameRow struct { + ID string + AccountID string + Handle string +} + +// AccountDMChannelsForRename reads the same-owner DMs (agent-agent and +// owner-agent) keyed by the renamed account's current handle. Cross-owner DMs use +// account IDs and do not change. +func (q *Queries) AccountDMChannelsForRename(ctx context.Context, accountID string) ([]AccountDMChannelsForRenameRow, error) { + rows, err := q.db.Query(ctx, accountDMChannelsForRename, accountID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []AccountDMChannelsForRenameRow + for rows.Next() { + var i AccountDMChannelsForRenameRow + if err := rows.Scan(&i.ID, &i.AccountID, &i.Handle); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getDMChannelByName = `-- name: GetDMChannelByName :one SELECT id, kind FROM channels WHERE group_id = $1 AND name = $2 ` @@ -153,3 +200,20 @@ func (q *Queries) ReassertDMMandatory(ctx context.Context, id string) error { _, err := q.db.Exec(ctx, reassertDMMandatory, id) return err } + +const renameDMChannel = `-- name: RenameDMChannel :execrows +UPDATE channels SET name = $2 WHERE id = $1 +` + +type RenameDMChannelParams struct { + ID string + Name string +} + +func (q *Queries) RenameDMChannel(ctx context.Context, arg RenameDMChannelParams) (int64, error) { + result, err := q.db.Exec(ctx, renameDMChannel, arg.ID, arg.Name) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} diff --git a/go/internal/store/db/querier.go b/go/internal/store/db/querier.go index 0ef48f24f..8287c7b20 100644 --- a/go/internal/store/db/querier.go +++ b/go/internal/store/db/querier.go @@ -11,6 +11,11 @@ import ( ) type Querier interface { + // AccountDMChannelsForRename reads the same-owner DMs (agent-agent and + // owner-agent) keyed by the renamed account's current handle. Cross-owner DMs use + // account IDs and do not change. + // + AccountDMChannelsForRename(ctx context.Context, accountID string) ([]AccountDMChannelsForRenameRow, error) // The tenant an account belongs to. A system-role caller (the delivery loop) reads // it to act for that account under its own tenant. AccountTenant(ctx context.Context, id string) (string, error) @@ -584,6 +589,7 @@ type Querier interface { RemarkSafetyValveSuperseded(ctx context.Context, arg RemarkSafetyValveSupersededParams) error RenameAccountDisplayHandle(ctx context.Context, arg RenameAccountDisplayHandleParams) (int64, error) RenameAccountHandle(ctx context.Context, arg RenameAccountHandleParams) (int64, error) + RenameDMChannel(ctx context.Context, arg RenameDMChannelParams) (int64, error) RenameTopic(ctx context.Context, arg RenameTopicParams) error RequireAgentSessionOwner(ctx context.Context, arg RequireAgentSessionOwnerParams) (bool, error) RequireAgentSessionSubscriber(ctx context.Context, arg RequireAgentSessionSubscriberParams) (bool, error) diff --git a/go/internal/store/dm.go b/go/internal/store/dm.go index bc0fd1efb..b80c29e35 100644 --- a/go/internal/store/dm.go +++ b/go/internal/store/dm.go @@ -42,6 +42,21 @@ type DMChannelSpec struct { Members []AccountID } +// DMChannelName derives the deterministic sorted-handle pair name for a peer DM. +// Handles are sorted lexicographically so open(a,b) and open(b,a) resolve the +// same channel. +// +// The separator is `:`, a byte the handle grammar excludes, so the split is +// unambiguous. A hyphen delimiter is not injective because handles may contain +// consecutive hyphens. +func DMChannelName(h1, h2 string) string { + lo, hi := h1, h2 + if lo > hi { + lo, hi = hi, lo + } + return "dm:" + lo + ":" + hi +} + // EnsureOwnerDMGroupTx get-or-creates the single per-owner namespace group that // hosts every one of ownerUserID's peer-DM channels, on tx, and returns its id. // It mirrors EnsureOwnerCoordinationGroupTx exactly — deterministic + idempotent diff --git a/go/internal/comms/dm_test.go b/go/internal/store/dm_test.go similarity index 59% rename from go/internal/comms/dm_test.go rename to go/internal/store/dm_test.go index 2c1e30103..c6922831d 100644 --- a/go/internal/comms/dm_test.go +++ b/go/internal/store/dm_test.go @@ -1,31 +1,28 @@ -package comms +package store import "testing" -// dmChannelName must be order-independent and injective. The `:` separator is +// DMChannelName must be order-independent and injective. The `:` separator is // load-bearing: a hyphen would derive one name for the distinct pairs // {a, b--c} and {a--b, c}, cross-adding members into a same-owner private DM. -func TestDmChannelName(t *testing.T) { +func TestDMChannelName(t *testing.T) { cases := []struct { name string h1, h2 string want string }{ - // lo > hi: exercises the swap branch (bob sorts after alice). {name: "unsorted input swaps to sorted name", h1: "bob", h2: "alice", want: "dm:alice:bob"}, - // lo <= hi: exercises the no-swap branch. {name: "already-sorted input keeps order", h1: "alice", h2: "bob", want: "dm:alice:bob"}, {name: "double-hyphen handle, first pair", h1: "a", h2: "b--c", want: "dm:a:b--c"}, {name: "double-hyphen handle, second pair", h1: "a--b", h2: "c", want: "dm:a--b:c"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - if got := dmChannelName(tc.h1, tc.h2); got != tc.want { - t.Fatalf("dmChannelName(%q, %q) = %q, want %q", tc.h1, tc.h2, got, tc.want) + if got := DMChannelName(tc.h1, tc.h2); got != tc.want { + t.Fatalf("DMChannelName(%q, %q) = %q, want %q", tc.h1, tc.h2, got, tc.want) } - // Order independence: the reversed pair must map to the same name. - if rev := dmChannelName(tc.h2, tc.h1); rev != tc.want { - t.Fatalf("dmChannelName(%q, %q) = %q, want %q (order-independent)", tc.h2, tc.h1, rev, tc.want) + if reversed := DMChannelName(tc.h2, tc.h1); reversed != tc.want { + t.Fatalf("DMChannelName(%q, %q) = %q, want %q (order-independent)", tc.h2, tc.h1, reversed, tc.want) } }) } diff --git a/go/internal/store/queries/dm.sql b/go/internal/store/queries/dm.sql index 07de4346d..4b7523865 100644 --- a/go/internal/store/queries/dm.sql +++ b/go/internal/store/queries/dm.sql @@ -30,6 +30,30 @@ VALUES ($1, $2, $3, $4, $5, NULL, $6) ON CONFLICT (group_id, name) WHERE group_id IS NOT NULL DO NOTHING RETURNING id; +-- AccountDMChannelsForRename reads the same-owner DMs (agent-agent and +-- owner-agent) keyed by the renamed account's current handle. Cross-owner DMs use +-- account IDs and do not change. +-- +-- name: AccountDMChannelsForRename :many +SELECT c.id, other.account_id, other_handle.handle +FROM channels c +JOIN channel_groups cg ON cg.id = c.group_id AND cg.name = '__dm__' + AND cg.parent_group_id IS NULL AND cg.visibility = 0 +JOIN channel_members subject ON subject.channel_id = c.id AND subject.account_id = $1 +JOIN channel_members other ON other.channel_id = c.id AND other.account_id <> $1 +JOIN account_handles other_handle ON other_handle.account_id = other.account_id +JOIN account_handles subject_handle ON subject_handle.account_id = subject.account_id +WHERE c.kind = 1 + AND cg.owner_user_id = COALESCE((SELECT owner_user_id FROM agent_accounts WHERE account_id = $1), $1) + AND c.name IN ( + 'dm:' || subject_handle.handle || ':' || other_handle.handle, + 'dm:' || other_handle.handle || ':' || subject_handle.handle + ) +ORDER BY c.id, other.account_id; + +-- name: RenameDMChannel :execrows +UPDATE channels SET name = $2 WHERE id = $1; + -- name: LockOwnerDM :exec SELECT pg_advisory_xact_lock(hashtext('dm:' || $1)); diff --git a/go/server/network_door_test.go b/go/server/network_door_test.go index bef793aab..4d4975991 100644 --- a/go/server/network_door_test.go +++ b/go/server/network_door_test.go @@ -37,6 +37,7 @@ import ( compassv1 "github.com/RigelBuild/compass/go/gen/compass/v1" "github.com/RigelBuild/compass/go/gen/compass/v1/compassv1connect" "github.com/RigelBuild/compass/go/internal/auth" + "github.com/RigelBuild/compass/go/internal/comms" "github.com/RigelBuild/compass/go/internal/pgtest" "github.com/RigelBuild/compass/go/internal/store" ) @@ -842,9 +843,9 @@ func TestIssueTokenHandlerInputContract(t *testing.T) { }) } -func TestRenameAccountHandlerInputContract(t *testing.T) { +func TestRenameAccountPublishesAccountAndDMChanges(t *testing.T) { ctx := t.Context() - st, _, member := newNetworkStore(t) + st, adminID, member := newNetworkStore(t) owner, err := st.GetAccount(ctx, member) if err != nil { t.Fatalf("GetAccount(member): %v", err) @@ -859,8 +860,11 @@ func TestRenameAccountHandlerInputContract(t *testing.T) { } bus := events.NewBus[busPayload]() t.Cleanup(bus.Close) - client := newH2CClient(t, newH2CTestServer(t, newService("test", bus, st, nil, nil, nil, nil))) - + commsBus := events.NewBus[*compassv1.SubscribeCommsResponse]() + t.Cleanup(commsBus.Close) + commsSvc := comms.NewComms(st, commsBus, nil, adminID) + svc := newService("test", bus, st, nil, nil, nil, nil).withAccountRenamer(commsSvc, adminID) + client := newH2CClient(t, newH2CTestServer(t, svc)) resp, err := client.RenameAccount(ctx, connect.NewRequest(&compassv1.RenameAccountRequest{ AccountHandle: owner.Handle, NewHandle: "renamed-member", @@ -868,6 +872,18 @@ func TestRenameAccountHandlerInputContract(t *testing.T) { if err != nil || resp.Msg.GetAccountId() != string(member) || resp.Msg.GetHandle() != "renamed-member" { t.Fatalf("RenameAccount(user) = (%+v, %v), want id %q and handle renamed-member", resp, err, member) } + accountEvents, err := commsBus.Subscribe(0, 0) + if err != nil { + t.Fatalf("comms bus Subscribe after user rename: %v", err) + } + assertOnlyAccountRenameEvent(t, accountEvents.Replay, "renamed-member") + accountEvents.Cancel() + agentCursor, err := commsBus.Subscribe(0, 0) + if err != nil { + t.Fatalf("comms bus Subscribe before agent rename: %v", err) + } + lastAgentSeq, agentEpoch := agentCursor.HeadSeq, agentCursor.Epoch + agentCursor.Cancel() resp, err = client.RenameAccount(ctx, connect.NewRequest(&compassv1.RenameAccountRequest{ AccountHandle: "renamed-member/rename-agent", NewHandle: "renamed-agent", @@ -875,7 +891,77 @@ func TestRenameAccountHandlerInputContract(t *testing.T) { if err != nil || resp.Msg.GetAccountId() != string(agent.ID) || resp.Msg.GetHandle() != "renamed-member/renamed-agent" { t.Fatalf("RenameAccount(agent) = (%+v, %v), want qualified renamed handle", resp, err) } + agentEvents, err := commsBus.Subscribe(lastAgentSeq, agentEpoch) + if err != nil { + t.Fatalf("comms bus Subscribe after agent rename: %v", err) + } + assertOnlyAccountRenameEvent(t, agentEvents.Replay, "renamed-agent") + agentEvents.Cancel() + + bob, err := st.CreateAgent(ctx, member, store.NewAgent{Handle: "rename-peer", DisplayName: "Rename Peer"}) + if err != nil { + t.Fatalf("CreateAgent(rename-peer): %v", err) + } + dm, err := commsSvc.OpenDM(comms.WithActor(ctx, agent.ID), connect.NewRequest(&compassv1.OpenDMRequest{PeerHandle: bob.Handle})) + if err != nil { + t.Fatalf("OpenDM(rename-agent -> peer): %v", err) + } + dmID := dm.Msg.GetChannel().GetId() + dmCursor, err := commsBus.Subscribe(0, 0) + if err != nil { + t.Fatalf("comms bus Subscribe before DM rename: %v", err) + } + lastSeq, epoch := dmCursor.HeadSeq, dmCursor.Epoch + dmCursor.Cancel() + if _, err := client.RenameAccount(ctx, connect.NewRequest(&compassv1.RenameAccountRequest{ + AccountHandle: "renamed-member/renamed-agent", NewHandle: "renamed-agent-two", + })); err != nil { + t.Fatalf("RenameAccount(agent with DM): %v", err) + } + dmRenameEvents, err := commsBus.Subscribe(lastSeq, epoch) + if err != nil { + t.Fatalf("comms bus Subscribe after DM rename: %v", err) + } + defer dmRenameEvents.Cancel() + if len(dmRenameEvents.Replay) != 2 { + t.Fatalf("events after DM rename = %d, want AccountChanged followed by ChannelChanged", len(dmRenameEvents.Replay)) + } + accountChange := dmRenameEvents.Replay[0].Payload.GetAccountChanged() + if accountChange == nil || accountChange.GetAccount().GetId() != string(agent.ID) || accountChange.GetAccount().GetHandle() != "renamed-agent-two" { + t.Fatalf("first DM rename event = %v, want AccountChanged for renamed agent", dmRenameEvents.Replay[0].Payload.GetPayload()) + } + channelChange := dmRenameEvents.Replay[1].Payload.GetChannelChanged() + if channelChange == nil || channelChange.GetChannel().GetId() != dmID || channelChange.GetChannel().GetName() != store.DMChannelName("rename-peer", "renamed-agent-two") { + t.Fatalf("second DM rename event = %v, want same channel ID with re-keyed DM name", dmRenameEvents.Replay[1].Payload.GetPayload()) + } + +} + +func assertOnlyAccountRenameEvent(t *testing.T, replay []events.Stamped[*compassv1.SubscribeCommsResponse], wantHandle string) { + t.Helper() + if len(replay) != 1 { + t.Fatalf("comms bus events after account rename = %d, want only AccountChanged", len(replay)) + } + changed := replay[0].Payload.GetAccountChanged() + if changed == nil || changed.GetAccount().GetHandle() != wantHandle { + t.Fatalf("rename account event = %v, want AccountChanged with handle %q", replay[0].Payload.GetPayload(), wantHandle) + } +} +func TestRenameAccountErrorCodesAndProtectedAccounts(t *testing.T) { + ctx := t.Context() + st, adminID, member := newNetworkStore(t) + owner, err := st.GetAccount(ctx, member) + if err != nil { + t.Fatalf("GetAccount(member): %v", err) + } + if _, err := st.CreateUser(ctx, store.NewUser{Handle: "rename-taken", DisplayName: "Taken"}); err != nil { + t.Fatalf("CreateUser(rename-taken): %v", err) + } + bus := events.NewBus[busPayload]() + t.Cleanup(bus.Close) + svc := newService("test", bus, st, nil, nil, nil, nil).withAccountRenamer(nil, adminID) + client := newH2CClient(t, newH2CTestServer(t, svc)) tests := []struct { name string req *compassv1.RenameAccountRequest @@ -883,7 +969,7 @@ func TestRenameAccountHandlerInputContract(t *testing.T) { }{ { name: "conflict maps to AlreadyExists", - req: &compassv1.RenameAccountRequest{AccountHandle: "renamed-member", NewHandle: "rename-taken"}, + req: &compassv1.RenameAccountRequest{AccountHandle: owner.Handle, NewHandle: "rename-taken"}, code: connect.CodeAlreadyExists, }, { @@ -893,12 +979,12 @@ func TestRenameAccountHandlerInputContract(t *testing.T) { }, { name: "invalid new handle maps to InvalidArgument", - req: &compassv1.RenameAccountRequest{AccountHandle: "renamed-member", NewHandle: "Bad Handle"}, + req: &compassv1.RenameAccountRequest{AccountHandle: owner.Handle, NewHandle: "Bad Handle"}, code: connect.CodeInvalidArgument, }, { name: "qualified new handle maps to InvalidArgument", - req: &compassv1.RenameAccountRequest{AccountHandle: "renamed-member", NewHandle: "other/agent"}, + req: &compassv1.RenameAccountRequest{AccountHandle: owner.Handle, NewHandle: "other/agent"}, code: connect.CodeInvalidArgument, }, { @@ -915,18 +1001,38 @@ func TestRenameAccountHandlerInputContract(t *testing.T) { } }) } - system, err := st.EnsureSystemAccount(ctx) if err != nil { t.Fatalf("EnsureSystemAccount: %v", err) } _, err = client.RenameAccount(ctx, connect.NewRequest(&compassv1.RenameAccountRequest{ - AccountHandle: system.Handle, - NewHandle: "renamed-system", + AccountHandle: system.Handle, NewHandle: "renamed-system", })) if code := connect.CodeOf(err); code != connect.CodePermissionDenied { t.Fatalf("RenameAccount(system account) = %v, want PermissionDenied", code) } + admin, err := st.GetAccount(ctx, adminID) + if err != nil { + t.Fatalf("GetAccount(admin): %v", err) + } + adminResp, err := client.RenameAccount(ctx, connect.NewRequest(&compassv1.RenameAccountRequest{ + AccountHandle: admin.Handle, NewHandle: "renamed-admin", + })) + if code := connect.CodeOf(err); code != connect.CodeFailedPrecondition || adminResp != nil { + t.Fatalf("RenameAccount(admin) = (%+v, %v), want FailedPrecondition and no response", adminResp, err) + } + supervisor, err := st.CreateAgent(ctx, adminID, store.NewAgent{ + Handle: rootSupervisorHandle, DisplayName: rootSupervisorDisplayName, Role: rootSupervisorRole, + }) + if err != nil { + t.Fatalf("CreateAgent(root supervisor): %v", err) + } + supervisorResp, err := client.RenameAccount(ctx, connect.NewRequest(&compassv1.RenameAccountRequest{ + AccountHandle: admin.Handle + "/" + supervisor.Handle, NewHandle: "renamed-supervisor", + })) + if code := connect.CodeOf(err); code != connect.CodeFailedPrecondition || supervisorResp != nil { + t.Fatalf("RenameAccount(root supervisor) = (%+v, %v), want FailedPrecondition and no response", supervisorResp, err) + } } // TestServeWithListenWritesAdminToken0600: on a --listen start, Serve mints the diff --git a/go/server/serve.go b/go/server/serve.go index 64add91cd..8116ac578 100644 --- a/go/server/serve.go +++ b/go/server/serve.go @@ -848,7 +848,7 @@ func Serve(ctx context.Context, cfg ServeConfig) error { // summary the drain logs, so both land on the same sink. hubLog := slog.Default() hub := newRunnerHub(st, brd, tail, commsSvc, hubLog) - svc := newService(cfg.Version, bus, st, hub, brd, issueBrd, tail).withRev(cfg.Rev) + svc := newService(cfg.Version, bus, st, hub, brd, issueBrd, tail).withRev(cfg.Rev).withAccountRenamer(commsSvc, admin.ID) // Break the hub<->lifecycle (RIG-1618 T5), hub<->board (agent primary // lifecycle T3-a, RelayBoardCall), and comms<->hub ask-answer wake (RIG-1577) // construction cycles; see wireHubServiceCycles in sinks.go. diff --git a/go/server/service.go b/go/server/service.go index c12bac65b..d27a66088 100644 --- a/go/server/service.go +++ b/go/server/service.go @@ -63,6 +63,10 @@ var errNoSessionID = errors.New("runner returned no session id to stop") // onto a copy at the stream edge. type busPayload = *compassv1.SubscribeEventsResponse +type accountRenamePublisher interface { + PublishAccountRename(ctx context.Context, account store.Account, channelIDs []store.ChannelID) +} + // service is the server's compass.v1 service. The event bus and the store of // record are shared by pointer and the version is a small owned string. The // store backs IssueToken: it verifies the target account against the store, then @@ -91,7 +95,9 @@ type service struct { // so a frame the hub relays reaches this service's stream subscribers. nil on // a server built with no Runner door (SubscribeAgentSession then Unavailable, // like the other Runner-backed RPCs). - tail *sessionTail + tail *sessionTail + accountRenamer accountRenamePublisher + adminID store.AccountID // signaler emits a ConfigVersion signal to every live session after a // successful config write (PutAgentConfig / DeleteAgentConfig), so live // Runners re-fetch the bundle. *runnerhub.Hub satisfies it; the service @@ -464,7 +470,11 @@ func (s *service) RenameAccount( fmt.Errorf("account %q is the system account and cannot be renamed", raw)) } - renamed, err := s.store.RenameHandle(ctx, acc.ID, newHandle) + if acc.ID == s.adminID || isAdminRootSupervisor(acc, s.adminID) { + return nil, connect.NewError(connect.CodeFailedPrecondition, + fmt.Errorf("account %q is a protected bootstrap account", raw)) + } + renamed, rekeyedChannels, err := s.store.RenameHandle(ctx, acc.ID, newHandle) if err != nil { switch { case errors.Is(err, store.ErrInvalidArgument): @@ -477,6 +487,9 @@ func (s *service) RenameAccount( return nil, connect.NewError(connect.CodeInternal, err) } } + if s.accountRenamer != nil { + s.accountRenamer.PublishAccountRename(ctx, renamed, rekeyedChannels) + } handle := renamed.Handle if renamed.Agent != nil { owner, err := s.store.GetAccount(ctx, renamed.Agent.OwnerUserID) @@ -622,6 +635,12 @@ func (s *service) SubscribeAgentSession( } } +func (s *service) withAccountRenamer(p accountRenamePublisher, adminID store.AccountID) *service { + s.accountRenamer = p + s.adminID = adminID + return s +} + // withRev sets the build git rev GetServerInfo reports; only Serve stamps it. func (s *service) withRev(rev string) *service { s.rev = rev 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 7a6297d40..5e6358959 100644 --- a/packages/compass-agent/src/gen/compass/v1/compass_pb.ts +++ b/packages/compass-agent/src/gen/compass/v1/compass_pb.ts @@ -1883,8 +1883,9 @@ export const IssueTokenResponseSchema: GenMessage = /*@__PUR export type RenameAccountRequest = Message<"compass.v1.RenameAccountRequest"> & { /** * Current bare user handle or `owner/agent` handle. Empty is INVALID_ARGUMENT; - * bad qualified grammar is INVALID_ARGUMENT, unknown handles are NOT_FOUND, and - * the system account is PERMISSION_DENIED. + * bad qualified grammar is INVALID_ARGUMENT, unknown handles are NOT_FOUND, + * and the system account is PERMISSION_DENIED. The bootstrap admin and its + * root supervisor are protected with FAILED_PRECONDITION. * * @generated from field: string account_handle = 1; */ @@ -3800,8 +3801,9 @@ export const CompassService: GenService<{ }, /** * Admin-gated: only an admin may rename an account; non-admin callers get - * permission_denied. Freed handles may be reclaimed; the owner of an agent - * does not change. + * permission_denied. Same-owner DMs are re-keyed; freed handles may be + * reclaimed, and an agent's owner does not change. The bootstrap admin and + * its root supervisor are protected from rename with failed_precondition. * * @generated from rpc compass.v1.CompassService.RenameAccount */ 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 7a6297d40..5e6358959 100644 --- a/packages/compass-client/src/gen/compass/v1/compass_pb.ts +++ b/packages/compass-client/src/gen/compass/v1/compass_pb.ts @@ -1883,8 +1883,9 @@ export const IssueTokenResponseSchema: GenMessage = /*@__PUR export type RenameAccountRequest = Message<"compass.v1.RenameAccountRequest"> & { /** * Current bare user handle or `owner/agent` handle. Empty is INVALID_ARGUMENT; - * bad qualified grammar is INVALID_ARGUMENT, unknown handles are NOT_FOUND, and - * the system account is PERMISSION_DENIED. + * bad qualified grammar is INVALID_ARGUMENT, unknown handles are NOT_FOUND, + * and the system account is PERMISSION_DENIED. The bootstrap admin and its + * root supervisor are protected with FAILED_PRECONDITION. * * @generated from field: string account_handle = 1; */ @@ -3800,8 +3801,9 @@ export const CompassService: GenService<{ }, /** * Admin-gated: only an admin may rename an account; non-admin callers get - * permission_denied. Freed handles may be reclaimed; the owner of an agent - * does not change. + * permission_denied. Same-owner DMs are re-keyed; freed handles may be + * reclaimed, and an agent's owner does not change. The bootstrap admin and + * its root supervisor are protected from rename with failed_precondition. * * @generated from rpc compass.v1.CompassService.RenameAccount */ diff --git a/proto/compass/v1/compass.proto b/proto/compass/v1/compass.proto index 5f85ffc57..928581bae 100644 --- a/proto/compass/v1/compass.proto +++ b/proto/compass/v1/compass.proto @@ -133,8 +133,9 @@ service CompassService { rpc IssueToken(IssueTokenRequest) returns (IssueTokenResponse); // Admin-gated: only an admin may rename an account; non-admin callers get - // permission_denied. Freed handles may be reclaimed; the owner of an agent - // does not change. + // permission_denied. Same-owner DMs are re-keyed; freed handles may be + // reclaimed, and an agent's owner does not change. The bootstrap admin and + // its root supervisor are protected from rename with failed_precondition. rpc RenameAccount(RenameAccountRequest) returns (RenameAccountResponse); // Admin-gated: only an admin may revoke a token; a non-admin caller gets @@ -925,8 +926,9 @@ message IssueTokenResponse { // RenameAccount: the admin-only path to rename a user or agent account. message RenameAccountRequest { // Current bare user handle or `owner/agent` handle. Empty is INVALID_ARGUMENT; - // bad qualified grammar is INVALID_ARGUMENT, unknown handles are NOT_FOUND, and - // the system account is PERMISSION_DENIED. + // bad qualified grammar is INVALID_ARGUMENT, unknown handles are NOT_FOUND, + // and the system account is PERMISSION_DENIED. The bootstrap admin and its + // root supervisor are protected with FAILED_PRECONDITION. string account_handle = 1; // New bare handle; an agent keeps its current owner. Empty, bad grammar, or a // qualified handle is INVALID_ARGUMENT; a taken handle is ALREADY_EXISTS. From cfad46815d1746adfa463f847a28e41be3e8082d Mon Sep 17 00:00:00 2001 From: mintaka Date: Sun, 11 Oct 2026 22:37:20 -0400 Subject: [PATCH 3/3] fix(store): rename home and coordination channels with the handle (RIG-5151) Review follow-up. An agent's home channel and a manager's coordination channel are named from the handle, so a rename left them stale. A later reconcile would then create a second coordination channel. RenameHandle now renames both in the same transaction, keeping ids, with the existing collision suffix for coordination names. The coordination reconcile re-reads the manager handle after taking the owner lock, so a queued reconcile cannot bring back the old name. DM re-keying now matches only real DM parties, not owners pulled into agent-agent DMs. It skips a DM converted to a channel after the scan. Ledger-impact: none Co-authored-by: Matt Wilkinson --- docs/specs/product/compass.md | 25 +-- .../v1/compassv1connect/compass.connect.go | 6 +- go/internal/comms/comms.go | 8 +- go/internal/comms/coordination.go | 31 ++-- go/internal/comms/coordination_pgtest_test.go | 102 ++++++++++++ go/internal/comms/dm_open_pgtest_test.go | 77 +++++++++ .../store/account_handles_pgtest_test.go | 156 +++++++++++++++++- go/internal/store/accounts.go | 117 ++++++++++++- go/internal/store/coordination.go | 13 +- go/internal/store/coordination_pgtest_test.go | 59 +++++++ go/internal/store/db/accounts.sql.go | 39 +++++ go/internal/store/db/coordination.sql.go | 64 +++++++ go/internal/store/db/dm.sql.go | 57 ++++++- go/internal/store/db/querier.go | 11 +- go/internal/store/queries/accounts.sql | 11 ++ go/internal/store/queries/coordination.sql | 11 ++ go/internal/store/queries/dm.sql | 50 +++++- go/server/network_door_test.go | 79 +++++++-- .../src/gen/compass/v1/compass_pb.ts | 3 +- .../src/gen/compass/v1/compass_pb.ts | 3 +- proto/compass/v1/compass.proto | 3 +- 21 files changed, 849 insertions(+), 76 deletions(-) diff --git a/docs/specs/product/compass.md b/docs/specs/product/compass.md index 26495d32b..6fd631a82 100644 --- a/docs/specs/product/compass.md +++ b/docs/specs/product/compass.md @@ -107,15 +107,15 @@ the only path by which the server pushes state to a UI. `RenameAccount` SHALL resolve a bare user handle or an owner-qualified agent handle without viewer-scoped filtering. It SHALL rename the account's resolution -handle and display copy in one store transaction. Same-owner DMs SHALL be -re-keyed in that transaction while their messages remain with the original -accounts. The new handle MUST be valid and bare; an agent's owner does not -change. Unknown accounts return `NotFound`, invalid input returns -`InvalidArgument`, and a collision in the account's handle tier returns -`AlreadyExists`. The system account cannot be renamed. The bootstrap admin and -its root supervisor return `FailedPrecondition` and remain unchanged. The -response returns the account id and its current address, qualified by the owner's -current handle for agents. A freed handle is immediately available for reuse. +handle and display copy in one store transaction. Same-owner DMs, agent home +channels, and coordination channels SHALL be renamed in that transaction while +their channel ids and message history remain unchanged. The new handle MUST be +valid and bare; an agent's owner does not change. Unknown accounts return +`NotFound`, invalid input returns `InvalidArgument`, and a collision in the account's handle tier returns `AlreadyExists`. +The system account cannot be renamed. The bootstrap admin and its root +supervisor return `FailedPrecondition` and remain unchanged. The response +returns the account id and its current address, qualified by the owner's current +handle for agents. A freed handle is immediately available for reuse. #### Scenario: Rename an account at either handle tier @@ -147,11 +147,12 @@ current handle for agents. A freed handle is immediately available for reuse. #### Scenario: Account rename publishes updated account and DM state -- **Given** an account with a same-owner DM and a subscriber entitled to both +- **Given** an account with a same-owner DM and a subscriber entitled to its + account and channel changes - **When** an admin renames the account - **Then** the subscriber receives `AccountChanged` with the new handle and - `ChannelChanged` with the re-keyed DM name, while the channel ID and message - history remain unchanged + `ChannelChanged` for each renamed same-owner DM, home, or coordination channel; + each channel retains its id and message history ### Requirement: A generated client is the only door to the server diff --git a/go/gen/compass/v1/compassv1connect/compass.connect.go b/go/gen/compass/v1/compassv1connect/compass.connect.go index bce5c996d..ee400c84d 100644 --- a/go/gen/compass/v1/compassv1connect/compass.connect.go +++ b/go/gen/compass/v1/compassv1connect/compass.connect.go @@ -240,7 +240,8 @@ type CompassServiceClient interface { // out-of-band at first start). IssueToken(context.Context, *connect.Request[v1.IssueTokenRequest]) (*connect.Response[v1.IssueTokenResponse], error) // Admin-gated: only an admin may rename an account; non-admin callers get - // permission_denied. Same-owner DMs are re-keyed; freed handles may be + // permission_denied. Same-owner DM, agent home, and coordination channel names + // follow the new handle; freed handles may be // reclaimed, and an agent's owner does not change. The bootstrap admin and // its root supervisor are protected from rename with failed_precondition. RenameAccount(context.Context, *connect.Request[v1.RenameAccountRequest]) (*connect.Response[v1.RenameAccountResponse], error) @@ -731,7 +732,8 @@ type CompassServiceHandler interface { // out-of-band at first start). IssueToken(context.Context, *connect.Request[v1.IssueTokenRequest]) (*connect.Response[v1.IssueTokenResponse], error) // Admin-gated: only an admin may rename an account; non-admin callers get - // permission_denied. Same-owner DMs are re-keyed; freed handles may be + // permission_denied. Same-owner DM, agent home, and coordination channel names + // follow the new handle; freed handles may be // reclaimed, and an agent's owner does not change. The bootstrap admin and // its root supervisor are protected from rename with failed_precondition. RenameAccount(context.Context, *connect.Request[v1.RenameAccountRequest]) (*connect.Response[v1.RenameAccountResponse], error) diff --git a/go/internal/comms/comms.go b/go/internal/comms/comms.go index a50de8b45..f84beb564 100644 --- a/go/internal/comms/comms.go +++ b/go/internal/comms/comms.go @@ -84,16 +84,16 @@ func NewComms(st *store.Store, bus commsBus, fab fabric.EventFabric, adminID sto return &Comms{store: st, bus: bus, fabric: fab, fabricPublishFailures: failures, adminID: adminID} } -// PublishAccountRename emits account and same-owner DM changes after the -// rename transaction commits. Channels are fetched in the committed state so -// every event carries their new deterministic names. +// PublishAccountRename emits account and renamed-channel changes after the store +// transaction commits. Renamed channels include same-owner DMs, agent home +// channels, and manager coordination channels. Each event carries committed state. func (c *Comms) PublishAccountRename(ctx context.Context, account store.Account, channelIDs []store.ChannelID) { ctx = context.WithoutCancel(ctx) c.publishAccountChanged(account) for _, channelID := range channelIDs { channel, err := c.store.GetChannel(ctx, channelID) if err != nil { - slog.ErrorContext(ctx, "comms: reading renamed dm channel for event", "channel_id", channelID, "err", err) + slog.ErrorContext(ctx, "comms: reading renamed channel for event", "channel_id", channelID, "err", err) continue } c.publishChannelChanged(channel, nil) diff --git a/go/internal/comms/coordination.go b/go/internal/comms/coordination.go index c9bc16075..19c56cea0 100644 --- a/go/internal/comms/coordination.go +++ b/go/internal/comms/coordination.go @@ -72,28 +72,31 @@ func (c *Comms) RegisterCoordinationHook(st *store.Store) { // reconcileCoordinationTx is the in-tx coordination reconcile registered as the // store's CoordinationHook: on the passed tx (mid-parent-edge-write) it (a) -// resolves the manager's handle + owning user; (b) get-or-creates the per-owner -// coordination namespace group and UPSERTs the `-coordination` channel in -// it (owner=manager, OWNER_ONLY, mandatory_subscription), suffixing around a -// user-owned name collision; (c) resyncs membership to manager + current reports, -// seeding each agent member's D2 cursor in this tx; (d) records the change for the -// post-commit emit. Idempotent per manager: a re-run with the tree unchanged -// upserts nothing new and resyncs to the same set. ALL reads/writes use tx, never -// the pool — the tx is mid-flight. +// resolves the manager's owning user; (b) locks that owner's coordination +// namespace and re-reads the manager handle; (c) get-or-creates the namespace +// group and UPSERTs the `-coordination` channel in it (owner=manager, +// OWNER_ONLY, mandatory_subscription), suffixing around a user-owned name +// collision; (d) resyncs membership to manager + current reports, seeding each +// agent member's D2 cursor in this tx; (e) records the change for the post-commit +// emit. Idempotent per manager: a re-run with the tree unchanged upserts nothing +// new and resyncs to the same set. ALL reads/writes use tx, never the pool — the +// tx is mid-flight. func (c *Comms) reconcileCoordinationTx(ctx context.Context, tx pgx.Tx, managerAgentID store.AccountID) error { - handle, ownerUserID, err := c.store.ResolveCoordinationManagerTx(ctx, tx, managerAgentID) + _, ownerUserID, err := c.store.ResolveCoordinationManagerTx(ctx, tx, managerAgentID) if err != nil { return err } - // Serialize every coordination reconcile under this owner so the group - // get-or-create and the channel provision cannot race a concurrent - // first-report for a different manager of the same owner into two groups or - // two channels (design.md fork-2 resolution). Auto-released at tx end. + // Serialize every coordination reconcile under this owner. Resolve the handle + // again after locking so a queued reconcile cannot restore an old name after a + // concurrent account rename commits. if err := store.LockOwnerCoordinationTx(ctx, tx, ownerUserID); err != nil { return err } - + handle, _, err := c.store.ResolveCoordinationManagerTx(ctx, tx, managerAgentID) + if err != nil { + return err + } groupID, err := c.store.EnsureOwnerCoordinationGroupTx(ctx, tx, ownerUserID) if err != nil { return err diff --git a/go/internal/comms/coordination_pgtest_test.go b/go/internal/comms/coordination_pgtest_test.go index 16c79d254..20acec26d 100644 --- a/go/internal/comms/coordination_pgtest_test.go +++ b/go/internal/comms/coordination_pgtest_test.go @@ -11,9 +11,11 @@ import ( "context" "slices" "testing" + "time" "connectrpc.com/connect" "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" compassv1 "github.com/RigelBuild/compass/go/gen/compass/v1" "github.com/RigelBuild/compass/go/internal/store" @@ -76,6 +78,106 @@ func TestCreateAgentWithParentProvisionsCoordinationChannel(t *testing.T) { } } +// TestReconcileReadsRenamedManagerHandleAfterCoordinationLock proves a reconcile +// queued behind RenameHandle uses the new manager handle rather than creating a +// second channel under the old name. +func TestReconcileReadsRenamedManagerHandleAfterCoordinationLock(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second) + defer cancel() + st, dsn := newTestStoreDSN(t) + owner := mustUser(t, st, "rename-race-owner") + manager := mustAgent(t, st, owner.ID, "manager-before") + if _, err := st.CreateAgent(ctx, owner.ID, store.NewAgent{Handle: "report", DisplayName: "Report", ParentAgentID: manager.ID}); err != nil { + t.Fatalf("create manager report: %v", err) + } + svc := NewComms(st, newBus(t), nil, owner.ID) + originalChannelID, err := svc.EnsureCoordinationChannel(ctx, manager.ID) + if err != nil { + t.Fatalf("EnsureCoordinationChannel: %v", err) + } + observer, err := pgxpool.New(ctx, dsn) + if err != nil { + t.Fatalf("open lock observer pool: %v", err) + } + t.Cleanup(observer.Close) + blocker, err := observer.Begin(ctx) + if err != nil { + t.Fatalf("begin coordination lock blocker: %v", err) + } + defer func() { _ = blocker.Rollback(ctx) }() + var blockerPID int32 + if err := blocker.QueryRow(ctx, "SELECT pg_backend_pid()").Scan(&blockerPID); err != nil { + t.Fatalf("read coordination blocker pid: %v", err) + } + if err := store.LockOwnerCoordinationTx(ctx, blocker, owner.ID); err != nil { + t.Fatalf("lock coordination namespace: %v", err) + } + + renameDone := make(chan error, 1) + go func() { + _, _, err := st.RenameHandle(ctx, manager.ID, "manager-after") + renameDone <- err + }() + waitForBlockedCoordinationTransactions(t, ctx, observer, blockerPID, 1) + + reconcileDone := make(chan error, 1) + go func() { + err := st.WithTx(ctx, func(tx pgx.Tx) error { + return svc.reconcileCoordinationTx(ctx, tx, manager.ID) + }) + reconcileDone <- err + }() + waitForBlockedCoordinationTransactions(t, ctx, observer, blockerPID, 2) + + if err := blocker.Commit(ctx); err != nil { + t.Fatalf("release coordination lock: %v", err) + } + for name, done := range map[string]<-chan error{"rename": renameDone, "reconcile": reconcileDone} { + select { + case err := <-done: + if err != nil { + t.Fatalf("%s: %v", name, err) + } + case <-ctx.Done(): + t.Fatalf("%s did not finish after coordination lock release: %v", name, ctx.Err()) + } + } + + channels := coordChannelsFor(t, st, manager.ID) + if len(channels) != 1 || channels[0].ID != originalChannelID || channels[0].Name != "manager-after-coordination" { + t.Fatalf("coordination channels after queued rename/reconcile = %+v, want only original channel %q named manager-after-coordination", channels, originalChannelID) + } +} + +func waitForBlockedCoordinationTransactions(t *testing.T, ctx context.Context, pool *pgxpool.Pool, blockerPID int32, want int64) { + t.Helper() + ticker := time.NewTicker(10 * time.Millisecond) + defer ticker.Stop() + timer := time.NewTimer(5 * time.Second) + defer timer.Stop() + for { + var blocked int64 + if err := pool.QueryRow(ctx, ` + SELECT count(*) + FROM pg_stat_activity + WHERE $1 = ANY(pg_blocking_pids(pid)) + AND query LIKE '%coordination:%' + `, blockerPID).Scan(&blocked); err != nil { + t.Fatalf("check coordination lock waiters: %v", err) + } + if blocked >= want { + return + } + select { + case <-timer.C: + t.Fatalf("coordination lock waiters = %d, want at least %d", blocked, want) + case <-ctx.Done(): + t.Fatalf("waiting for coordination lock waiters: %v", ctx.Err()) + case <-ticker.C: + } + } +} + // TestCreateAgentByAgentCallerProvisionsCoordinationChannel proves owner // resolution (RIG-1644) and coordination provisioning compose: an AGENT caller // spawning a report under a same-owner manager resolves to its owner, passes the diff --git a/go/internal/comms/dm_open_pgtest_test.go b/go/internal/comms/dm_open_pgtest_test.go index 07f000319..6271b871a 100644 --- a/go/internal/comms/dm_open_pgtest_test.go +++ b/go/internal/comms/dm_open_pgtest_test.go @@ -63,6 +63,83 @@ func TestOpenDMSameOwnerCreatesDMChannel(t *testing.T) { } } +// TestOpenDMSurvivesOwnerHandleRename ensures an owner is not treated as a party +// when its handle overlaps an agent handle in an agent-agent DM. +func TestOpenDMSurvivesOwnerHandleRename(t *testing.T) { + svc, st := newHandler(t) + ctx := t.Context() + owner := mustUser(t, st, "alice") + alice := mustAgent(t, st, owner.ID, "alice") + bob := mustAgent(t, st, owner.ID, "bob") + + first, err := svc.OpenDM(WithActor(ctx, alice.ID), connect.NewRequest(&compassv1.OpenDMRequest{ + PeerHandle: bob.Handle, + })) + if err != nil { + t.Fatalf("OpenDM before owner rename: %v", err) + } + original := first.Msg.GetChannel() + if original.GetName() != store.DMChannelName(alice.Handle, bob.Handle) { + t.Fatalf("initial DM name = %q, want %q", original.GetName(), store.DMChannelName(alice.Handle, bob.Handle)) + } + + if _, _, err := st.RenameHandle(ctx, owner.ID, "alice-owner-new"); err != nil { + t.Fatalf("RenameHandle(owner): %v", err) + } + + second, err := svc.OpenDM(WithActor(ctx, alice.ID), connect.NewRequest(&compassv1.OpenDMRequest{ + PeerHandle: bob.Handle, + })) + if err != nil { + t.Fatalf("OpenDM after owner rename: %v", err) + } + if second.Msg.GetCreated() || second.Msg.GetChannel().GetId() != original.GetId() { + t.Fatalf("OpenDM after owner rename = (id %q, created %t), want original channel %q resumed", + second.Msg.GetChannel().GetId(), second.Msg.GetCreated(), original.GetId()) + } + if second.Msg.GetChannel().GetName() != original.GetName() { + t.Fatalf("agent DM name after owner rename = %q, want unchanged %q", second.Msg.GetChannel().GetName(), original.GetName()) + } +} + +// TestOpenOwnerAgentDMSurvivesOwnerHandleRename preserves owner-agent DMs when +// the user is an actual party, not a pulled-in owner member. +func TestOpenOwnerAgentDMSurvivesOwnerHandleRename(t *testing.T) { + svc, st := newHandler(t) + ctx := t.Context() + owner := mustUser(t, st, "owner-before") + agent := mustAgent(t, st, owner.ID, "agent") + + first, err := svc.OpenDM(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.OpenDMRequest{ + PeerHandle: agent.Handle, + })) + if err != nil { + t.Fatalf("OpenDM before owner rename: %v", err) + } + original := first.Msg.GetChannel() + if !first.Msg.GetCreated() { + t.Fatal("first owner-agent OpenDM reported created=false") + } + + if _, _, err := st.RenameHandle(ctx, owner.ID, "owner-after"); err != nil { + t.Fatalf("RenameHandle(owner): %v", err) + } + second, err := svc.OpenDM(WithActor(ctx, owner.ID), connect.NewRequest(&compassv1.OpenDMRequest{ + PeerHandle: agent.Handle, + })) + if err != nil { + t.Fatalf("OpenDM after owner rename: %v", err) + } + if second.Msg.GetCreated() || second.Msg.GetChannel().GetId() != original.GetId() { + t.Fatalf("OpenDM after owner rename = (id %q, created %t), want original channel %q resumed", + second.Msg.GetChannel().GetId(), second.Msg.GetCreated(), original.GetId()) + } + if second.Msg.GetChannel().GetName() != store.DMChannelName("owner-after", agent.Handle) { + t.Fatalf("owner-agent DM name after owner rename = %q, want %q", + second.Msg.GetChannel().GetName(), store.DMChannelName("owner-after", agent.Handle)) + } +} + // TestOpenDMRevokeWaitsForLockedPeering keeps a stale authorization from creating a DM. func TestOpenDMRevokeWaitsForLockedPeering(t *testing.T) { svc, st := newHandler(t) diff --git a/go/internal/store/account_handles_pgtest_test.go b/go/internal/store/account_handles_pgtest_test.go index d823b3e26..e48ae320f 100644 --- a/go/internal/store/account_handles_pgtest_test.go +++ b/go/internal/store/account_handles_pgtest_test.go @@ -29,6 +29,7 @@ import ( "github.com/jackc/pgx/v5" "github.com/RigelBuild/compass/go/internal/pgtest" + "github.com/RigelBuild/compass/go/internal/store/db" ) // ---- T0: uniqueness invariants ---- @@ -185,8 +186,8 @@ func TestRenameHandleRekeysSameOwnerDMWithoutChangingMembership(t *testing.T) { if err != nil { t.Fatalf("RenameHandle: %v", err) } - if renamed.Handle != "alice-new" || len(rekeyed) != 1 || rekeyed[0] != channelID { - t.Fatalf("RenameHandle = (%q, %v), want handle alice-new and the same DM ID %q", renamed.Handle, rekeyed, channelID) + if renamed.Handle != "alice-new" || !slices.Contains(rekeyed, channelID) { + t.Fatalf("RenameHandle = (%q, %v), want handle alice-new and DM ID %q", renamed.Handle, rekeyed, channelID) } channel, err := s.GetChannel(ctx, channelID) if err != nil { @@ -200,6 +201,157 @@ func TestRenameHandleRekeysSameOwnerDMWithoutChangingMembership(t *testing.T) { } } +// TestRenameHandleDoesNotRekeyAgentDMForOwnerHandle verifies that an owner +// pulled into an agent DM is not treated as a party when its handle changes. +func TestRenameHandleDoesNotRekeyAgentDMForOwnerHandle(t *testing.T) { + ctx := t.Context() + s := newTestStore(t) + owner := mustUser(t, s, "alice") + alice := mustAgent(t, s, owner.ID, "alice") + bob := mustAgent(t, s, owner.ID, "bob") + var channelID ChannelID + if err := s.WithTx(ctx, func(tx pgx.Tx) error { + if err := LockOwnerDMTx(ctx, tx, owner.ID); err != nil { + return err + } + group, err := s.EnsureOwnerDMGroupTx(ctx, tx, owner.ID) + if err != nil { + return err + } + channelID, _, err = s.UpsertDMChannelTx(ctx, tx, DMChannelSpec{ + GroupID: group, + Name: DMChannelName(alice.Handle, bob.Handle), + Members: []AccountID{alice.ID, bob.ID}, + }) + return err + }); err != nil { + t.Fatalf("open agent DM: %v", err) + } + + _, renamedIDs, err := s.RenameHandle(ctx, owner.ID, "alice-owner-new") + if err != nil { + t.Fatalf("RenameHandle(owner): %v", err) + } + channel, err := s.GetChannel(ctx, channelID) + if err != nil { + t.Fatalf("GetChannel: %v", err) + } + if channel.Name != DMChannelName(alice.Handle, bob.Handle) { + t.Fatalf("agent DM name after owner rename = %q, want %q", channel.Name, DMChannelName(alice.Handle, bob.Handle)) + } + if slices.Contains(renamedIDs, channelID) { + t.Fatalf("renamed channel ids %v include agent DM %q for an owner handle change", renamedIDs, channelID) + } + var reopenedID ChannelID + var created bool + if err := s.WithTx(ctx, func(tx pgx.Tx) error { + if err := LockOwnerDMTx(ctx, tx, owner.ID); err != nil { + return err + } + group, err := s.EnsureOwnerDMGroupTx(ctx, tx, owner.ID) + if err != nil { + return err + } + reopenedID, created, err = s.UpsertDMChannelTx(ctx, tx, DMChannelSpec{ + GroupID: group, + Name: DMChannelName(alice.Handle, bob.Handle), + Members: []AccountID{alice.ID, bob.ID}, + }) + return err + }); err != nil { + t.Fatalf("reopen agent DM after owner rename: %v", err) + } + if reopenedID != channelID || created { + t.Fatalf("reopen agent DM after owner rename = (%q, %t), want original %q resumed", reopenedID, created, channelID) + } +} + +// TestRenameHandleRenamesAgentHomeChannel proves a rename updates the channel +// name through the stable home_channel_id. +func TestRenameHandleRenamesAgentHomeChannel(t *testing.T) { + ctx := t.Context() + s := newTestStore(t) + owner := mustUser(t, s, "home-owner") + agent := mustAgent(t, s, owner.ID, "home-old") + homeID := agent.Agent.HomeChannelID + + _, renamedIDs, err := s.RenameHandle(ctx, agent.ID, "home-new") + if err != nil { + t.Fatalf("RenameHandle: %v", err) + } + if !slices.Contains(renamedIDs, homeID) { + t.Fatalf("RenameHandle channel ids = %v, want home channel %q", renamedIDs, homeID) + } + home, err := s.GetChannel(ctx, homeID) + if err != nil { + t.Fatalf("GetChannel(home): %v", err) + } + if home.ID != homeID || home.Name != "home-new" { + t.Fatalf("renamed home channel = (%q, %q), want same id %q and name home-new", home.ID, home.Name, homeID) + } +} + +// TestRenameSkipsDMConvertedAfterScan protects a user-selected name from a +// concurrent conversion that wins after the rename scan. +func TestRenameSkipsDMConvertedAfterScan(t *testing.T) { + ctx := t.Context() + s := newTestStore(t) + owner := mustUser(t, s, "convert-owner") + alice := mustAgent(t, s, owner.ID, "convert-alice") + bob := mustAgent(t, s, owner.ID, "convert-bob") + var ( + channelID ChannelID + renamedIDs []ChannelID + ) + if err := s.WithTx(ctx, func(tx pgx.Tx) error { + if err := LockOwnerDMTx(ctx, tx, owner.ID); err != nil { + return err + } + groupID, err := s.EnsureOwnerDMGroupTx(ctx, tx, owner.ID) + if err != nil { + return err + } + channelID, _, err = s.UpsertDMChannelTx(ctx, tx, DMChannelSpec{ + GroupID: groupID, + Name: DMChannelName(alice.Handle, bob.Handle), + Members: []AccountID{alice.ID, bob.ID}, + }) + if err != nil { + return err + } + rows, err := db.New(tx).AccountDMChannelsForRename(ctx, string(alice.ID)) + if err != nil { + return err + } + if len(rows) != 1 || rows[0].ID != string(channelID) { + t.Fatalf("DM rename scan = %+v, want channel %q", rows, channelID) + } + if err := db.New(tx).ConvertDMChannel(ctx, db.ConvertDMChannelParams{ + Kind: int16(ChannelKindChannel), Name: "Chosen after scan", ID: string(channelID), + }); err != nil { + return err + } + renamedIDs, err = renameHandleTx(ctx, tx, db.New(tx), alice.ID, alice.Handle, "convert-alice-new", rows, nil, nil) + if err != nil { + return err + } + var name string + var kind int16 + if err := tx.QueryRow(ctx, "SELECT name, kind FROM channels WHERE id = $1", string(channelID)).Scan(&name, &kind); err != nil { + return err + } + if name != "Chosen after scan" || ChannelKind(kind) != ChannelKindChannel { + t.Fatalf("converted channel = (%q, %d), want its chosen name and CHANNEL kind", name, kind) + } + return nil + }); err != nil { + t.Fatalf("convert and rename in transaction: %v", err) + } + if slices.Contains(renamedIDs, channelID) { + t.Fatalf("renamed channel ids %v include converted channel %q", renamedIDs, channelID) + } +} + // TestHandleReclaimBothTiers: a freed handle may be re-registered at either tier. func TestHandleReclaimBothTiers(t *testing.T) { ctx := t.Context() diff --git a/go/internal/store/accounts.go b/go/internal/store/accounts.go index 64d6499bc..f43207503 100644 --- a/go/internal/store/accounts.go +++ b/go/internal/store/accounts.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strconv" "strings" "github.com/jackc/pgx/v5" @@ -568,9 +569,42 @@ func (s *Store) RenameHandle(ctx context.Context, id AccountID, newHandle string if err != nil { return Account{}, nil, fmt.Errorf("store: list account dms before rename: %w", err) } + var homeChannel *db.AgentHomeChannelForRenameRow + if _, err := qtx.GetAgentOwner(ctx, string(id)); err == nil { + row, err := qtx.AgentHomeChannelForRename(ctx, string(id)) + if err != nil { + return Account{}, nil, fmt.Errorf("store: resolve agent home channel before rename: %w", err) + } + homeChannel = &row + } else if !noRows(err) { + return Account{}, nil, fmt.Errorf("store: resolve agent before rename: %w", err) + } + var coordChannels []db.CoordinationChannelsForRenameRow + if homeChannel != nil { + if err := LockOwnerCoordinationTx(ctx, tx, AccountID(ownerUserID)); err != nil { + return Account{}, nil, err + } + coordGroupID, err := qtx.GetCoordinationGroup(ctx, db.GetCoordinationGroupParams{ + OwnerUserID: ownerUserID, Name: coordinationGroupName, Visibility: int16(VisibilityOwner), + }) + if err != nil && !noRows(err) { + return Account{}, nil, fmt.Errorf("store: resolve coordination group before rename: %w", err) + } + if err == nil { + coordChannels, err = qtx.CoordinationChannelsForRename(ctx, db.CoordinationChannelsForRenameParams{ + GroupID: pgtype.Text{String: coordGroupID, Valid: true}, + OwnerAccountID: pgtype.Text{String: string(id), Valid: true}, + Name: currentHandle + "-coordination", + }) + if err != nil { + return Account{}, nil, fmt.Errorf("store: list coordination channels before rename: %w", err) + } + coordChannels = filterCoordinationChannelsForRename(coordChannels, currentHandle) + } + } var channelIDs []ChannelID if currentHandle != newHandle { - channelIDs, err = renameHandleTx(ctx, qtx, id, newHandle, dmChannels) + channelIDs, err = renameHandleTx(ctx, tx, qtx, id, currentHandle, newHandle, dmChannels, homeChannel, coordChannels) if err != nil { return Account{}, nil, err } @@ -588,10 +622,13 @@ func (s *Store) RenameHandle(ctx context.Context, id AccountID, newHandle string func renameHandleTx( ctx context.Context, + tx pgx.Tx, qtx *db.Queries, id AccountID, - newHandle string, + oldHandle, newHandle string, dmChannels []db.AccountDMChannelsForRenameRow, + homeChannel *db.AgentHomeChannelForRenameRow, + coordChannels []db.CoordinationChannelsForRenameRow, ) ([]ChannelID, error) { rows, err := qtx.RenameAccountHandle(ctx, db.RenameAccountHandleParams{AccountID: string(id), Handle: newHandle}) if err != nil { @@ -610,23 +647,87 @@ func renameHandleTx( if rows == 0 { return nil, fmt.Errorf("%w: account %q", ErrNotFound, id) } - channelIDs := make([]ChannelID, 0, len(dmChannels)) + channelIDs := make([]ChannelID, 0, len(dmChannels)+len(coordChannels)+1) for _, channel := range dmChannels { rows, err := qtx.RenameDMChannel(ctx, db.RenameDMChannelParams{ - ID: channel.ID, - Name: DMChannelName(newHandle, channel.Handle), + ID: channel.ID, + Name: DMChannelName(newHandle, channel.Handle), + Name_2: DMChannelName(oldHandle, channel.Handle), }) if err != nil { return nil, fmt.Errorf("store: re-key dm channel %q: %w", channel.ID, err) } - if rows == 0 { - return nil, fmt.Errorf("%w: dm channel %q", ErrNotFound, channel.ID) + if rows == 1 { + channelIDs = append(channelIDs, ChannelID(channel.ID)) + } + } + if homeChannel != nil { + rows, err := qtx.RenameAgentHomeChannel(ctx, db.RenameAgentHomeChannelParams{ + AccountID: string(id), Name: newHandle, Name_2: oldHandle, + }) + if err != nil { + return nil, fmt.Errorf("store: rename agent home channel: %w", err) + } + if rows != 1 { + return nil, fmt.Errorf("%w: agent home channel %q changed during rename", ErrNotFound, homeChannel.ID) + } + channelIDs = append(channelIDs, ChannelID(homeChannel.ID)) + } + for _, channel := range coordChannels { + for suffix := 1; ; suffix++ { + name := coordinationChannelName(newHandle+"-coordination", suffix) + savepoint, err := tx.Begin(ctx) + if err != nil { + return nil, fmt.Errorf("store: begin coordination rename savepoint: %w", err) + } + rows, updateErr := db.New(savepoint).RenameCoordinationChannel(ctx, db.RenameCoordinationChannelParams{ + ID: channel.ID, Name: name, OwnerAccountID: pgtype.Text{String: string(id), Valid: true}, Name_2: channel.Name, + }) + if updateErr != nil && pgErrIs(updateErr, pgUniqueViolation) && pgConstraintName(updateErr) == "channels_group_name_key" { + if rollbackErr := savepoint.Rollback(ctx); rollbackErr != nil { + return nil, fmt.Errorf("store: roll back coordination rename savepoint: %w", rollbackErr) + } + continue + } + if updateErr != nil { + _ = savepoint.Rollback(ctx) + return nil, fmt.Errorf("store: rename coordination channel %q: %w", channel.ID, updateErr) + } + if rows != 1 { + _ = savepoint.Rollback(ctx) + return nil, fmt.Errorf("%w: coordination channel %q changed during rename", ErrNotFound, channel.ID) + } + if err := savepoint.Commit(ctx); err != nil { + return nil, fmt.Errorf("store: commit coordination rename savepoint: %w", err) + } + channelIDs = append(channelIDs, ChannelID(channel.ID)) + break } - channelIDs = append(channelIDs, ChannelID(channel.ID)) } return channelIDs, nil } +// filterCoordinationChannelsForRename keeps the base name and its numeric-suffix +// forms, dropping user channels that merely share the prefix. +func filterCoordinationChannelsForRename(channels []db.CoordinationChannelsForRenameRow, oldHandle string) []db.CoordinationChannelsForRenameRow { + base := oldHandle + "-coordination" + filtered := channels[:0] + for _, channel := range channels { + if channel.Name == base { + filtered = append(filtered, channel) + continue + } + suffix := strings.TrimPrefix(channel.Name, base+"-") + if suffix == "" { + continue + } + if _, err := strconv.Atoi(suffix); err == nil { + filtered = append(filtered, channel) + } + } + return filtered +} + // ResolveOwner resolves a caller to the user account it acts under. It mirrors // the caller-owner resolution ReparentAgent applies inline (clause 0): an agent // caller resolves to its agent_accounts.owner_user_id, while a user caller has diff --git a/go/internal/store/coordination.go b/go/internal/store/coordination.go index 84989268e..d426d51ab 100644 --- a/go/internal/store/coordination.go +++ b/go/internal/store/coordination.go @@ -172,10 +172,7 @@ func (s *Store) UpsertCoordinationChannelTx(ctx context.Context, tx pgx.Tx, spec qtx := db.New(tx) for suffix := 1; ; suffix++ { - name := spec.BaseName - if suffix > 1 { - name = fmt.Sprintf("%s-%d", spec.BaseName, suffix) - } + name := coordinationChannelName(spec.BaseName, suffix) existing, err := qtx.GetCoordinationChannelByName(ctx, db.GetCoordinationChannelByNameParams{ GroupID: pgtype.Text{String: string(spec.GroupID), Valid: true}, @@ -224,6 +221,14 @@ func (s *Store) UpsertCoordinationChannelTx(ctx context.Context, tx pgx.Tx, spec } } +// coordinationChannelName is the collision-suffixed name: base, then base-2, base-3, ... +func coordinationChannelName(baseName string, suffix int) string { + if suffix <= 1 { + return baseName + } + return fmt.Sprintf("%s-%d", baseName, suffix) +} + // SetCoordinationMembersTx resyncs channelID's membership to exactly want on tx: // it adds every wanted member missing a row (seeding each agent member's D2 // delivery cursor to channel head IN THIS TX, so a mandatory channel never mints diff --git a/go/internal/store/coordination_pgtest_test.go b/go/internal/store/coordination_pgtest_test.go index f8e11d451..e97503f01 100644 --- a/go/internal/store/coordination_pgtest_test.go +++ b/go/internal/store/coordination_pgtest_test.go @@ -14,6 +14,7 @@ package store import ( "context" "fmt" + "slices" "testing" "time" @@ -208,6 +209,64 @@ func TestFirstReportProvisionsExactlyOnce(t *testing.T) { } } +func TestRenameManagerKeepsCoordinationChannel(t *testing.T) { + ctx := t.Context() + s := newTestStore(t) + owner := mustUser(t, s, "rename-coord-owner") + h := &recordingHook{s: s} + s.SetCoordinationHook(h.reconcile) + manager := mustAgent(t, s, owner.ID, "manager-before") + if _, err := s.CreateAgent(ctx, owner.ID, NewAgent{Handle: "report-before", ParentAgentID: manager.ID}); err != nil { + t.Fatalf("CreateAgent(first report): %v", err) + } + channels := coordChannels(t, s, owner.ID) + if len(channels) != 1 { + t.Fatalf("coordination channels before rename = %d, want 1", len(channels)) + } + coordID := channels[0].ID + var userChannelID ChannelID + if err := s.WithTx(ctx, func(tx pgx.Tx) error { + groupID, err := s.EnsureOwnerCoordinationGroupTx(ctx, tx, owner.ID) + if err != nil { + return err + } + userChannelID, err = s.UpsertCoordinationChannelTx(ctx, tx, CoordinationChannelSpec{ + GroupID: groupID, BaseName: "manager-after-coordination", OwnerAccountID: owner.ID, + Policy: ChannelPolicy{OwnerAccountID: owner.ID}, + }) + return err + }); err != nil { + t.Fatalf("create destination-name collision: %v", err) + } + + _, renamedIDs, err := s.RenameHandle(ctx, manager.ID, "manager-after") + if err != nil { + t.Fatalf("RenameHandle(manager): %v", err) + } + if _, err := s.CreateAgent(ctx, owner.ID, NewAgent{Handle: "report-after", ParentAgentID: manager.ID}); err != nil { + t.Fatalf("CreateAgent(second report): %v", err) + } + + channels = coordChannels(t, s, owner.ID) + if len(channels) != 2 { + t.Fatalf("coordination channels after rename and report = %+v, want user collision and manager channel", channels) + } + byID := make(map[ChannelID]Channel, len(channels)) + for _, channel := range channels { + byID[channel.ID] = channel + } + if byID[coordID].Name != "manager-after-coordination-2" || byID[userChannelID].Name != "manager-after-coordination" { + t.Fatalf("coordination channels after rename = %+v, want manager suffixed without changing user channel", channels) + } + if !containsChannelID(renamedIDs, coordID) || !containsChannelID(renamedIDs, manager.Agent.HomeChannelID) || containsChannelID(renamedIDs, userChannelID) { + t.Fatalf("RenameHandle channel ids = %v, want manager coordination and home only (coord %q, home %q)", renamedIDs, coordID, manager.Agent.HomeChannelID) + } +} + +func containsChannelID(ids []ChannelID, want ChannelID) bool { + return slices.Contains(ids, want) +} + // TestReparentMovesCoordinationMembership pins reparent-in adds and reparent-out // removes: moving a report from manager A to manager B adds it to B's channel and // removes it from A's, and SetCoordinationMembersTx reports the removal so the diff --git a/go/internal/store/db/accounts.sql.go b/go/internal/store/db/accounts.sql.go index fe765480f..5f55a0670 100644 --- a/go/internal/store/db/accounts.sql.go +++ b/go/internal/store/db/accounts.sql.go @@ -67,6 +67,25 @@ func (q *Queries) AcquireOwnerTreeLock(ctx context.Context, hashtext string) err return err } +const agentHomeChannelForRename = `-- name: AgentHomeChannelForRename :one +SELECT ch.id, ch.name +FROM agent_accounts aa +JOIN channels ch ON ch.id = aa.home_channel_id AND ch.kind = 0 +WHERE aa.account_id = $1 +` + +type AgentHomeChannelForRenameRow struct { + ID string + Name string +} + +func (q *Queries) AgentHomeChannelForRename(ctx context.Context, accountID string) (AgentHomeChannelForRenameRow, error) { + row := q.db.QueryRow(ctx, agentHomeChannelForRename, accountID) + var i AgentHomeChannelForRenameRow + err := row.Scan(&i.ID, &i.Name) + return i, err +} + const countRootAgents = `-- name: CountRootAgents :one SELECT COUNT(*) FROM agent_accounts @@ -497,6 +516,26 @@ func (q *Queries) RenameAccountHandle(ctx context.Context, arg RenameAccountHand return result.RowsAffected(), nil } +const renameAgentHomeChannel = `-- name: RenameAgentHomeChannel :execrows +UPDATE channels ch SET name = $2 +FROM agent_accounts aa +WHERE aa.account_id = $1 AND ch.id = aa.home_channel_id AND ch.kind = 0 AND ch.name = $3 +` + +type RenameAgentHomeChannelParams struct { + AccountID string + Name string + Name_2 string +} + +func (q *Queries) RenameAgentHomeChannel(ctx context.Context, arg RenameAgentHomeChannelParams) (int64, error) { + result, err := q.db.Exec(ctx, renameAgentHomeChannel, arg.AccountID, arg.Name, arg.Name_2) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} + const resolveGlobalHandles = `-- name: ResolveGlobalHandles :many SELECT ah.handle, ah.account_id FROM account_handles ah diff --git a/go/internal/store/db/coordination.sql.go b/go/internal/store/db/coordination.sql.go index 2e3508221..79b0070ef 100644 --- a/go/internal/store/db/coordination.sql.go +++ b/go/internal/store/db/coordination.sql.go @@ -35,6 +35,45 @@ func (q *Queries) ChannelMemberIDs(ctx context.Context, channelID string) ([]str return items, nil } +const coordinationChannelsForRename = `-- name: CoordinationChannelsForRename :many +SELECT id, name +FROM channels +WHERE group_id = $1 AND kind = 0 AND owner_account_id = $2 + AND (name = $3 OR left(name, length($3) + 1) = $3 || '-') +ORDER BY id +` + +type CoordinationChannelsForRenameParams struct { + GroupID pgtype.Text + OwnerAccountID pgtype.Text + Name string +} + +type CoordinationChannelsForRenameRow struct { + ID string + Name string +} + +func (q *Queries) CoordinationChannelsForRename(ctx context.Context, arg CoordinationChannelsForRenameParams) ([]CoordinationChannelsForRenameRow, error) { + rows, err := q.db.Query(ctx, coordinationChannelsForRename, arg.GroupID, arg.OwnerAccountID, arg.Name) + if err != nil { + return nil, err + } + defer rows.Close() + var items []CoordinationChannelsForRenameRow + for rows.Next() { + var i CoordinationChannelsForRenameRow + if err := rows.Scan(&i.ID, &i.Name); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const coordinationReports = `-- name: CoordinationReports :many SELECT account_id FROM agent_accounts WHERE parent_agent_id = $1 ORDER BY account_id ` @@ -170,6 +209,31 @@ func (q *Queries) LockOwnerCoordination(ctx context.Context, dollar_1 pgtype.Tex return err } +const renameCoordinationChannel = `-- name: RenameCoordinationChannel :execrows +UPDATE channels SET name = $2 +WHERE id = $1 AND kind = 0 AND owner_account_id = $3 AND name = $4 +` + +type RenameCoordinationChannelParams struct { + ID string + Name string + OwnerAccountID pgtype.Text + Name_2 string +} + +func (q *Queries) RenameCoordinationChannel(ctx context.Context, arg RenameCoordinationChannelParams) (int64, error) { + result, err := q.db.Exec(ctx, renameCoordinationChannel, + arg.ID, + arg.Name, + arg.OwnerAccountID, + arg.Name_2, + ) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} + const resolveCoordinationManager = `-- name: ResolveCoordinationManager :one SELECT a.handle, ag.owner_user_id FROM accounts a diff --git a/go/internal/store/db/dm.sql.go b/go/internal/store/db/dm.sql.go index cde261339..bcf6d1f2e 100644 --- a/go/internal/store/db/dm.sql.go +++ b/go/internal/store/db/dm.sql.go @@ -17,11 +17,51 @@ FROM channels c JOIN channel_groups cg ON cg.id = c.group_id AND cg.name = '__dm__' AND cg.parent_group_id IS NULL AND cg.visibility = 0 JOIN channel_members subject ON subject.channel_id = c.id AND subject.account_id = $1 +LEFT JOIN agent_accounts subject_party ON subject_party.account_id = subject.account_id JOIN channel_members other ON other.channel_id = c.id AND other.account_id <> $1 +LEFT JOIN agent_accounts other_party ON other_party.account_id = other.account_id JOIN account_handles other_handle ON other_handle.account_id = other.account_id JOIN account_handles subject_handle ON subject_handle.account_id = subject.account_id WHERE c.kind = 1 AND cg.owner_user_id = COALESCE((SELECT owner_user_id FROM agent_accounts WHERE account_id = $1), $1) + AND ( + ( + subject_party.account_id IS NULL + AND other_party.account_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM channel_members party_member + JOIN agent_accounts party ON party.account_id = party_member.account_id + WHERE party_member.channel_id = c.id + AND party_member.account_id <> other.account_id + ) + ) + OR ( + subject_party.account_id IS NOT NULL + AND ( + ( + other_party.account_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM channel_members party_member + JOIN agent_accounts party ON party.account_id = party_member.account_id + WHERE party_member.channel_id = c.id + AND party_member.account_id NOT IN (subject.account_id, other.account_id) + ) + ) + OR ( + other.account_id = subject_party.owner_user_id + AND NOT EXISTS ( + SELECT 1 + FROM channel_members party_member + JOIN agent_accounts party ON party.account_id = party_member.account_id + WHERE party_member.channel_id = c.id + AND party_member.account_id <> subject.account_id + ) + ) + ) + ) + ) AND c.name IN ( 'dm:' || subject_handle.handle || ':' || other_handle.handle, 'dm:' || other_handle.handle || ':' || subject_handle.handle @@ -35,9 +75,10 @@ type AccountDMChannelsForRenameRow struct { Handle string } -// AccountDMChannelsForRename reads the same-owner DMs (agent-agent and -// owner-agent) keyed by the renamed account's current handle. Cross-owner DMs use -// account IDs and do not change. +// AccountDMChannelsForRename reads same-owner DMs by actual agent-party handles. +// An agent subject is a party directly. A user subject is a party only in an +// owner-agent DM with exactly one agent party; user members of agent-agent DMs +// are pulled-in owners and do not affect the deterministic pair name. func (q *Queries) AccountDMChannelsForRename(ctx context.Context, accountID string) ([]AccountDMChannelsForRenameRow, error) { rows, err := q.db.Query(ctx, accountDMChannelsForRename, accountID) if err != nil { @@ -202,16 +243,18 @@ func (q *Queries) ReassertDMMandatory(ctx context.Context, id string) error { } const renameDMChannel = `-- name: RenameDMChannel :execrows -UPDATE channels SET name = $2 WHERE id = $1 +UPDATE channels SET name = $2 +WHERE id = $1 AND kind = 1 AND name = $3 ` type RenameDMChannelParams struct { - ID string - Name string + ID string + Name string + Name_2 string } func (q *Queries) RenameDMChannel(ctx context.Context, arg RenameDMChannelParams) (int64, error) { - result, err := q.db.Exec(ctx, renameDMChannel, arg.ID, arg.Name) + result, err := q.db.Exec(ctx, renameDMChannel, arg.ID, arg.Name, arg.Name_2) if err != nil { return 0, err } diff --git a/go/internal/store/db/querier.go b/go/internal/store/db/querier.go index 8287c7b20..2f65afe9d 100644 --- a/go/internal/store/db/querier.go +++ b/go/internal/store/db/querier.go @@ -11,9 +11,10 @@ import ( ) type Querier interface { - // AccountDMChannelsForRename reads the same-owner DMs (agent-agent and - // owner-agent) keyed by the renamed account's current handle. Cross-owner DMs use - // account IDs and do not change. + // AccountDMChannelsForRename reads same-owner DMs by actual agent-party handles. + // An agent subject is a party directly. A user subject is a party only in an + // owner-agent DM with exactly one agent party; user members of agent-agent DMs + // are pulled-in owners and do not affect the deterministic pair name. // AccountDMChannelsForRename(ctx context.Context, accountID string) ([]AccountDMChannelsForRenameRow, error) // The tenant an account belongs to. A system-role caller (the delivery loop) reads @@ -48,6 +49,7 @@ type Querier interface { // file passed its SQL as a const identifier). The hand-written Store methods keep // their signatures and error mapping. AgentHasOpenAsk(ctx context.Context, authorAccountID string) (bool, error) + AgentHomeChannelForRename(ctx context.Context, accountID string) (AgentHomeChannelForRenameRow, error) // Agent-tree queries (sqlc adoption T2, RIG-3034). These replace the // const-hoisted `agentTreeProjection` + per-caller WHERE composition that lived // in internal/store/agent_tree.go and was run through the `queryAgents` helper — @@ -116,6 +118,7 @@ type Querier interface { // which is the correct answer for every human/external transition. ConsumeStateTransition(ctx context.Context, arg ConsumeStateTransitionParams) (string, error) ConvertDMChannel(ctx context.Context, arg ConvertDMChannelParams) error + CoordinationChannelsForRename(ctx context.Context, arg CoordinationChannelsForRenameParams) ([]CoordinationChannelsForRenameRow, error) CoordinationReports(ctx context.Context, parentAgentID pgtype.Text) ([]string, error) CountAgentForgeSubscriptionsForArtifact(ctx context.Context, arg CountAgentForgeSubscriptionsForArtifactParams) (int64, error) CountAgentMembers(ctx context.Context, channelID string) (int64, error) @@ -589,6 +592,8 @@ type Querier interface { RemarkSafetyValveSuperseded(ctx context.Context, arg RemarkSafetyValveSupersededParams) error RenameAccountDisplayHandle(ctx context.Context, arg RenameAccountDisplayHandleParams) (int64, error) RenameAccountHandle(ctx context.Context, arg RenameAccountHandleParams) (int64, error) + RenameAgentHomeChannel(ctx context.Context, arg RenameAgentHomeChannelParams) (int64, error) + RenameCoordinationChannel(ctx context.Context, arg RenameCoordinationChannelParams) (int64, error) RenameDMChannel(ctx context.Context, arg RenameDMChannelParams) (int64, error) RenameTopic(ctx context.Context, arg RenameTopicParams) error RequireAgentSessionOwner(ctx context.Context, arg RequireAgentSessionOwnerParams) (bool, error) diff --git a/go/internal/store/queries/accounts.sql b/go/internal/store/queries/accounts.sql index 98e29351e..a2409baf5 100644 --- a/go/internal/store/queries/accounts.sql +++ b/go/internal/store/queries/accounts.sql @@ -48,6 +48,17 @@ WHERE id = $1; INSERT INTO channels (id, name, group_id, kind) VALUES ($1, $2, NULL, $3); +-- name: AgentHomeChannelForRename :one +SELECT ch.id, ch.name +FROM agent_accounts aa +JOIN channels ch ON ch.id = aa.home_channel_id AND ch.kind = 0 +WHERE aa.account_id = $1; + +-- name: RenameAgentHomeChannel :execrows +UPDATE channels ch SET name = $2 +FROM agent_accounts aa +WHERE aa.account_id = $1 AND ch.id = aa.home_channel_id AND ch.kind = 0 AND ch.name = $3; + -- name: SeedHomeChannelMembers :exec INSERT INTO channel_members (channel_id, account_id, subscribed) VALUES ($1, $2, FALSE), ($1, $3, TRUE); diff --git a/go/internal/store/queries/coordination.sql b/go/internal/store/queries/coordination.sql index 3d1851de2..2c30e69f5 100644 --- a/go/internal/store/queries/coordination.sql +++ b/go/internal/store/queries/coordination.sql @@ -24,6 +24,17 @@ VALUES ($1, $2, $3, $4, $5, NULLIF($6, ''), $7) ON CONFLICT (group_id, name) WHERE group_id IS NOT NULL DO NOTHING RETURNING id; +-- name: CoordinationChannelsForRename :many +SELECT id, name +FROM channels +WHERE group_id = $1 AND kind = 0 AND owner_account_id = $2 + AND (name = $3 OR left(name, length($3) + 1) = $3 || '-') +ORDER BY id; + +-- name: RenameCoordinationChannel :execrows +UPDATE channels SET name = $2 +WHERE id = $1 AND kind = 0 AND owner_account_id = $3 AND name = $4; + -- name: ChannelMemberIDs :many SELECT account_id FROM channel_members WHERE channel_id = $1; diff --git a/go/internal/store/queries/dm.sql b/go/internal/store/queries/dm.sql index 4b7523865..26e6631e8 100644 --- a/go/internal/store/queries/dm.sql +++ b/go/internal/store/queries/dm.sql @@ -30,9 +30,10 @@ VALUES ($1, $2, $3, $4, $5, NULL, $6) ON CONFLICT (group_id, name) WHERE group_id IS NOT NULL DO NOTHING RETURNING id; --- AccountDMChannelsForRename reads the same-owner DMs (agent-agent and --- owner-agent) keyed by the renamed account's current handle. Cross-owner DMs use --- account IDs and do not change. +-- AccountDMChannelsForRename reads same-owner DMs by actual agent-party handles. +-- An agent subject is a party directly. A user subject is a party only in an +-- owner-agent DM with exactly one agent party; user members of agent-agent DMs +-- are pulled-in owners and do not affect the deterministic pair name. -- -- name: AccountDMChannelsForRename :many SELECT c.id, other.account_id, other_handle.handle @@ -40,11 +41,51 @@ FROM channels c JOIN channel_groups cg ON cg.id = c.group_id AND cg.name = '__dm__' AND cg.parent_group_id IS NULL AND cg.visibility = 0 JOIN channel_members subject ON subject.channel_id = c.id AND subject.account_id = $1 +LEFT JOIN agent_accounts subject_party ON subject_party.account_id = subject.account_id JOIN channel_members other ON other.channel_id = c.id AND other.account_id <> $1 +LEFT JOIN agent_accounts other_party ON other_party.account_id = other.account_id JOIN account_handles other_handle ON other_handle.account_id = other.account_id JOIN account_handles subject_handle ON subject_handle.account_id = subject.account_id WHERE c.kind = 1 AND cg.owner_user_id = COALESCE((SELECT owner_user_id FROM agent_accounts WHERE account_id = $1), $1) + AND ( + ( + subject_party.account_id IS NULL + AND other_party.account_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM channel_members party_member + JOIN agent_accounts party ON party.account_id = party_member.account_id + WHERE party_member.channel_id = c.id + AND party_member.account_id <> other.account_id + ) + ) + OR ( + subject_party.account_id IS NOT NULL + AND ( + ( + other_party.account_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM channel_members party_member + JOIN agent_accounts party ON party.account_id = party_member.account_id + WHERE party_member.channel_id = c.id + AND party_member.account_id NOT IN (subject.account_id, other.account_id) + ) + ) + OR ( + other.account_id = subject_party.owner_user_id + AND NOT EXISTS ( + SELECT 1 + FROM channel_members party_member + JOIN agent_accounts party ON party.account_id = party_member.account_id + WHERE party_member.channel_id = c.id + AND party_member.account_id <> subject.account_id + ) + ) + ) + ) + ) AND c.name IN ( 'dm:' || subject_handle.handle || ':' || other_handle.handle, 'dm:' || other_handle.handle || ':' || subject_handle.handle @@ -52,7 +93,8 @@ WHERE c.kind = 1 ORDER BY c.id, other.account_id; -- name: RenameDMChannel :execrows -UPDATE channels SET name = $2 WHERE id = $1; +UPDATE channels SET name = $2 +WHERE id = $1 AND kind = 1 AND name = $3; -- name: LockOwnerDM :exec SELECT pg_advisory_xact_lock(hashtext('dm:' || $1)); diff --git a/go/server/network_door_test.go b/go/server/network_door_test.go index 4d4975991..4d6e54500 100644 --- a/go/server/network_door_test.go +++ b/go/server/network_door_test.go @@ -863,6 +863,27 @@ func TestRenameAccountPublishesAccountAndDMChanges(t *testing.T) { commsBus := events.NewBus[*compassv1.SubscribeCommsResponse]() t.Cleanup(commsBus.Close) commsSvc := comms.NewComms(st, commsBus, nil, adminID) + commsSvc.RegisterCoordinationHook(st) + if _, err := st.CreateAgent(ctx, member, store.NewAgent{ + Handle: "rename-report", DisplayName: "Rename Report", ParentAgentID: agent.ID, + }); err != nil { + t.Fatalf("CreateAgent(rename-report): %v", err) + } + channels, err := st.ListChannels(ctx, agent.ID) + if err != nil { + t.Fatalf("ListChannels(manager): %v", err) + } + var coordinationID store.ChannelID + for _, channel := range channels { + if channel.Policy.OwnerAccountID == agent.ID && channel.Policy.MandatorySubscription && + channel.Policy.PostPolicy == store.ChannelPostPolicyOwnerOnly { + coordinationID = channel.ID + break + } + } + if coordinationID == "" { + t.Fatal("manager has no coordination channel") + } svc := newService("test", bus, st, nil, nil, nil, nil).withAccountRenamer(commsSvc, adminID) client := newH2CClient(t, newH2CTestServer(t, svc)) resp, err := client.RenameAccount(ctx, connect.NewRequest(&compassv1.RenameAccountRequest{ @@ -895,9 +916,15 @@ func TestRenameAccountPublishesAccountAndDMChanges(t *testing.T) { if err != nil { t.Fatalf("comms bus Subscribe after agent rename: %v", err) } - assertOnlyAccountRenameEvent(t, agentEvents.Replay, "renamed-agent") + if len(agentEvents.Replay) != 3 { + t.Fatalf("events after agent rename = %d, want AccountChanged followed by home and coordination ChannelChanged", len(agentEvents.Replay)) + } + assertRenameAccountEvent(t, agentEvents.Replay[0].Payload, agent.ID, "renamed-agent") + assertRenameChannelEvents(t, agentEvents.Replay[1:], map[string]string{ + string(agent.Agent.HomeChannelID): "renamed-agent", + string(coordinationID): "renamed-agent-coordination", + }) agentEvents.Cancel() - bob, err := st.CreateAgent(ctx, member, store.NewAgent{Handle: "rename-peer", DisplayName: "Rename Peer"}) if err != nil { t.Fatalf("CreateAgent(rename-peer): %v", err) @@ -923,17 +950,15 @@ func TestRenameAccountPublishesAccountAndDMChanges(t *testing.T) { t.Fatalf("comms bus Subscribe after DM rename: %v", err) } defer dmRenameEvents.Cancel() - if len(dmRenameEvents.Replay) != 2 { - t.Fatalf("events after DM rename = %d, want AccountChanged followed by ChannelChanged", len(dmRenameEvents.Replay)) - } - accountChange := dmRenameEvents.Replay[0].Payload.GetAccountChanged() - if accountChange == nil || accountChange.GetAccount().GetId() != string(agent.ID) || accountChange.GetAccount().GetHandle() != "renamed-agent-two" { - t.Fatalf("first DM rename event = %v, want AccountChanged for renamed agent", dmRenameEvents.Replay[0].Payload.GetPayload()) - } - channelChange := dmRenameEvents.Replay[1].Payload.GetChannelChanged() - if channelChange == nil || channelChange.GetChannel().GetId() != dmID || channelChange.GetChannel().GetName() != store.DMChannelName("rename-peer", "renamed-agent-two") { - t.Fatalf("second DM rename event = %v, want same channel ID with re-keyed DM name", dmRenameEvents.Replay[1].Payload.GetPayload()) - } + if len(dmRenameEvents.Replay) != 4 { + t.Fatalf("events after DM rename = %d, want AccountChanged followed by home, coordination, and DM ChannelChanged", len(dmRenameEvents.Replay)) + } + assertRenameAccountEvent(t, dmRenameEvents.Replay[0].Payload, agent.ID, "renamed-agent-two") + assertRenameChannelEvents(t, dmRenameEvents.Replay[1:], map[string]string{ + string(agent.Agent.HomeChannelID): "renamed-agent-two", + string(coordinationID): "renamed-agent-two-coordination", + dmID: store.DMChannelName("rename-peer", "renamed-agent-two"), + }) } @@ -948,6 +973,34 @@ func assertOnlyAccountRenameEvent(t *testing.T, replay []events.Stamped[*compass } } +func assertRenameAccountEvent(t *testing.T, payload *compassv1.SubscribeCommsResponse, id store.AccountID, handle string) { + t.Helper() + changed := payload.GetAccountChanged() + if changed == nil || changed.GetAccount().GetId() != string(id) || changed.GetAccount().GetHandle() != handle { + t.Fatalf("account rename event = %v, want account %q with handle %q", payload.GetPayload(), id, handle) + } +} + +func assertRenameChannelEvents(t *testing.T, replay []events.Stamped[*compassv1.SubscribeCommsResponse], want map[string]string) { + t.Helper() + if len(replay) != len(want) { + t.Fatalf("channel rename events = %d, want %d: %v", len(replay), len(want), replay) + } + got := make(map[string]string, len(replay)) + for _, event := range replay { + changed := event.Payload.GetChannelChanged() + if changed == nil { + t.Fatalf("rename payload = %v, want ChannelChanged", event.Payload.GetPayload()) + } + got[changed.GetChannel().GetId()] = changed.GetChannel().GetName() + } + for id, name := range want { + if got[id] != name { + t.Fatalf("rename channel events = %v, want channel %q named %q", got, id, name) + } + } +} + func TestRenameAccountErrorCodesAndProtectedAccounts(t *testing.T) { ctx := t.Context() st, adminID, member := newNetworkStore(t) 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 5e6358959..792c572d6 100644 --- a/packages/compass-agent/src/gen/compass/v1/compass_pb.ts +++ b/packages/compass-agent/src/gen/compass/v1/compass_pb.ts @@ -3801,7 +3801,8 @@ export const CompassService: GenService<{ }, /** * Admin-gated: only an admin may rename an account; non-admin callers get - * permission_denied. Same-owner DMs are re-keyed; freed handles may be + * permission_denied. Same-owner DM, agent home, and coordination channel names + * follow the new handle; freed handles may be * reclaimed, and an agent's owner does not change. The bootstrap admin and * its root supervisor are protected from rename with failed_precondition. * 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 5e6358959..792c572d6 100644 --- a/packages/compass-client/src/gen/compass/v1/compass_pb.ts +++ b/packages/compass-client/src/gen/compass/v1/compass_pb.ts @@ -3801,7 +3801,8 @@ export const CompassService: GenService<{ }, /** * Admin-gated: only an admin may rename an account; non-admin callers get - * permission_denied. Same-owner DMs are re-keyed; freed handles may be + * permission_denied. Same-owner DM, agent home, and coordination channel names + * follow the new handle; freed handles may be * reclaimed, and an agent's owner does not change. The bootstrap admin and * its root supervisor are protected from rename with failed_precondition. * diff --git a/proto/compass/v1/compass.proto b/proto/compass/v1/compass.proto index 928581bae..97fa9dfac 100644 --- a/proto/compass/v1/compass.proto +++ b/proto/compass/v1/compass.proto @@ -133,7 +133,8 @@ service CompassService { rpc IssueToken(IssueTokenRequest) returns (IssueTokenResponse); // Admin-gated: only an admin may rename an account; non-admin callers get - // permission_denied. Same-owner DMs are re-keyed; freed handles may be + // permission_denied. Same-owner DM, agent home, and coordination channel names + // follow the new handle; freed handles may be // reclaimed, and an agent's owner does not change. The bootstrap admin and // its root supervisor are protected from rename with failed_precondition. rpc RenameAccount(RenameAccountRequest) returns (RenameAccountResponse);