diff --git a/go/gen/compass/v1/compass.pb.go b/go/gen/compass/v1/compass.pb.go index f49a3f492..f8bb75a13 100644 --- a/go/gen/compass/v1/compass.pb.go +++ b/go/gen/compass/v1/compass.pb.go @@ -3923,6 +3923,512 @@ func (*DeleteAgentConfigResponse) Descriptor() ([]byte, []int) { return file_compass_v1_compass_proto_rawDescGZIP(), []int{56} } +// One candidate in a stable name's ordered chain: an upstream (provider, +// model_id) the gateway resolver tries in order until one has a usable +// credential for the caller. The pair is opaque here — the SDK owns the +// selector grammar; the store validates only that both are non-empty and +// whitespace-free. +type ModelCandidate struct { + state protoimpl.MessageState `protogen:"open.v1"` + Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + ModelId string `protobuf:"bytes,2,opt,name=model_id,json=modelId,proto3" json:"model_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModelCandidate) Reset() { + *x = ModelCandidate{} + mi := &file_compass_v1_compass_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModelCandidate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModelCandidate) ProtoMessage() {} + +func (x *ModelCandidate) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_compass_proto_msgTypes[57] + 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 ModelCandidate.ProtoReflect.Descriptor instead. +func (*ModelCandidate) Descriptor() ([]byte, []int) { + return file_compass_v1_compass_proto_rawDescGZIP(), []int{57} +} + +func (x *ModelCandidate) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *ModelCandidate) GetModelId() string { + if x != nil { + return x.ModelId + } + return "" +} + +// The listing metadata a stable name carries, taken from its primary candidate +// (OQ-2): the shape the container /v1/models listing and the cold-boot models.yml +// seed render from. All fields optional — an entry may carry none. +type ModelMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The upstream context window in tokens, 0 when unknown. + ContextWindow int64 `protobuf:"varint,1,opt,name=context_window,json=contextWindow,proto3" json:"context_window,omitempty"` + // Cost per million input/output tokens in integer micro-USD (money is never a + // float), 0 when unknown. + InputCostMicroUsd int64 `protobuf:"varint,2,opt,name=input_cost_micro_usd,json=inputCostMicroUsd,proto3" json:"input_cost_micro_usd,omitempty"` + OutputCostMicroUsd int64 `protobuf:"varint,3,opt,name=output_cost_micro_usd,json=outputCostMicroUsd,proto3" json:"output_cost_micro_usd,omitempty"` + // The SDK Api type the primary candidate speaks (e.g. "anthropic-messages", + // "openai-completions"), empty when unset. + Api string `protobuf:"bytes,4,opt,name=api,proto3" json:"api,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModelMetadata) Reset() { + *x = ModelMetadata{} + mi := &file_compass_v1_compass_proto_msgTypes[58] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModelMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModelMetadata) ProtoMessage() {} + +func (x *ModelMetadata) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_compass_proto_msgTypes[58] + 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 ModelMetadata.ProtoReflect.Descriptor instead. +func (*ModelMetadata) Descriptor() ([]byte, []int) { + return file_compass_v1_compass_proto_rawDescGZIP(), []int{58} +} + +func (x *ModelMetadata) GetContextWindow() int64 { + if x != nil { + return x.ContextWindow + } + return 0 +} + +func (x *ModelMetadata) GetInputCostMicroUsd() int64 { + if x != nil { + return x.InputCostMicroUsd + } + return 0 +} + +func (x *ModelMetadata) GetOutputCostMicroUsd() int64 { + if x != nil { + return x.OutputCostMicroUsd + } + return 0 +} + +func (x *ModelMetadata) GetApi() string { + if x != nil { + return x.Api + } + return "" +} + +// One stable name's registry entry: a human display name, the ordered candidate +// chain, and the listing metadata. +type ModelRegistryEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + DisplayName string `protobuf:"bytes,1,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + Candidates []*ModelCandidate `protobuf:"bytes,2,rep,name=candidates,proto3" json:"candidates,omitempty"` + Metadata *ModelMetadata `protobuf:"bytes,3,opt,name=metadata,proto3" json:"metadata,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModelRegistryEntry) Reset() { + *x = ModelRegistryEntry{} + mi := &file_compass_v1_compass_proto_msgTypes[59] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModelRegistryEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModelRegistryEntry) ProtoMessage() {} + +func (x *ModelRegistryEntry) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_compass_proto_msgTypes[59] + 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 ModelRegistryEntry.ProtoReflect.Descriptor instead. +func (*ModelRegistryEntry) Descriptor() ([]byte, []int) { + return file_compass_v1_compass_proto_rawDescGZIP(), []int{59} +} + +func (x *ModelRegistryEntry) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *ModelRegistryEntry) GetCandidates() []*ModelCandidate { + if x != nil { + return x.Candidates + } + return nil +} + +func (x *ModelRegistryEntry) GetMetadata() *ModelMetadata { + if x != nil { + return x.Metadata + } + return nil +} + +// The fleet model registry payload: the stable-name → entry map. The map key is +// the stable name (the modelId the gateway resolver looks up). +type ModelRegistry struct { + state protoimpl.MessageState `protogen:"open.v1"` + Entries map[string]*ModelRegistryEntry `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModelRegistry) Reset() { + *x = ModelRegistry{} + mi := &file_compass_v1_compass_proto_msgTypes[60] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModelRegistry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModelRegistry) ProtoMessage() {} + +func (x *ModelRegistry) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_compass_proto_msgTypes[60] + 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 ModelRegistry.ProtoReflect.Descriptor instead. +func (*ModelRegistry) Descriptor() ([]byte, []int) { + return file_compass_v1_compass_proto_rawDescGZIP(), []int{60} +} + +func (x *ModelRegistry) GetEntries() map[string]*ModelRegistryEntry { + if x != nil { + return x.Entries + } + return nil +} + +// PutModelRegistry: declare the fleet model registry. The caller's identity is +// the authenticated connection, never a field here. +type PutModelRegistryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The registry payload to write, validated fail-closed at the door. + Registry *ModelRegistry `protobuf:"bytes,1,opt,name=registry,proto3" json:"registry,omitempty"` + // The version the caller read (compare-and-set). 0 seeds the first registry; + // a non-zero value must equal the row's current version or the write is + // ABORTED (a racing operator write landed first — re-read and retry). + ExpectedVersion int64 `protobuf:"varint,2,opt,name=expected_version,json=expectedVersion,proto3" json:"expected_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PutModelRegistryRequest) Reset() { + *x = PutModelRegistryRequest{} + mi := &file_compass_v1_compass_proto_msgTypes[61] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PutModelRegistryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PutModelRegistryRequest) ProtoMessage() {} + +func (x *PutModelRegistryRequest) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_compass_proto_msgTypes[61] + 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 PutModelRegistryRequest.ProtoReflect.Descriptor instead. +func (*PutModelRegistryRequest) Descriptor() ([]byte, []int) { + return file_compass_v1_compass_proto_rawDescGZIP(), []int{61} +} + +func (x *PutModelRegistryRequest) GetRegistry() *ModelRegistry { + if x != nil { + return x.Registry + } + return nil +} + +func (x *PutModelRegistryRequest) GetExpectedVersion() int64 { + if x != nil { + return x.ExpectedVersion + } + return 0 +} + +type PutModelRegistryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The new whole-registry version after the write (the prior version + 1). + Version int64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PutModelRegistryResponse) Reset() { + *x = PutModelRegistryResponse{} + mi := &file_compass_v1_compass_proto_msgTypes[62] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PutModelRegistryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PutModelRegistryResponse) ProtoMessage() {} + +func (x *PutModelRegistryResponse) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_compass_proto_msgTypes[62] + 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 PutModelRegistryResponse.ProtoReflect.Descriptor instead. +func (*PutModelRegistryResponse) Descriptor() ([]byte, []int) { + return file_compass_v1_compass_proto_rawDescGZIP(), []int{62} +} + +func (x *PutModelRegistryResponse) GetVersion() int64 { + if x != nil { + return x.Version + } + return 0 +} + +type GetModelRegistryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetModelRegistryRequest) Reset() { + *x = GetModelRegistryRequest{} + mi := &file_compass_v1_compass_proto_msgTypes[63] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetModelRegistryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetModelRegistryRequest) ProtoMessage() {} + +func (x *GetModelRegistryRequest) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_compass_proto_msgTypes[63] + 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 GetModelRegistryRequest.ProtoReflect.Descriptor instead. +func (*GetModelRegistryRequest) Descriptor() ([]byte, []int) { + return file_compass_v1_compass_proto_rawDescGZIP(), []int{63} +} + +// GetModelRegistry: the current registry version and payload. An unconfigured +// fleet reports version 0 and an empty registry. +type GetModelRegistryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version int64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + Registry *ModelRegistry `protobuf:"bytes,2,opt,name=registry,proto3" json:"registry,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetModelRegistryResponse) Reset() { + *x = GetModelRegistryResponse{} + mi := &file_compass_v1_compass_proto_msgTypes[64] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetModelRegistryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetModelRegistryResponse) ProtoMessage() {} + +func (x *GetModelRegistryResponse) ProtoReflect() protoreflect.Message { + mi := &file_compass_v1_compass_proto_msgTypes[64] + 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 GetModelRegistryResponse.ProtoReflect.Descriptor instead. +func (*GetModelRegistryResponse) Descriptor() ([]byte, []int) { + return file_compass_v1_compass_proto_rawDescGZIP(), []int{64} +} + +func (x *GetModelRegistryResponse) GetVersion() int64 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *GetModelRegistryResponse) GetRegistry() *ModelRegistry { + if x != nil { + return x.Registry + } + return nil +} + +type DeleteModelRegistryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteModelRegistryRequest) Reset() { + *x = DeleteModelRegistryRequest{} + mi := &file_compass_v1_compass_proto_msgTypes[65] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteModelRegistryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteModelRegistryRequest) ProtoMessage() {} + +func (x *DeleteModelRegistryRequest) 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 DeleteModelRegistryRequest.ProtoReflect.Descriptor instead. +func (*DeleteModelRegistryRequest) Descriptor() ([]byte, []int) { + return file_compass_v1_compass_proto_rawDescGZIP(), []int{65} +} + +type DeleteModelRegistryResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteModelRegistryResponse) Reset() { + *x = DeleteModelRegistryResponse{} + mi := &file_compass_v1_compass_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteModelRegistryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteModelRegistryResponse) ProtoMessage() {} + +func (x *DeleteModelRegistryResponse) 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 DeleteModelRegistryResponse.ProtoReflect.Descriptor instead. +func (*DeleteModelRegistryResponse) Descriptor() ([]byte, []int) { + return file_compass_v1_compass_proto_rawDescGZIP(), []int{66} +} + // The Compass agent attribution parsed from the owner header at ingestion — a // plain display fact (DL-094, compass-attribution-simplification, Active). If a // header names an agent, that is the displayed author: NO forge-login @@ -3942,7 +4448,7 @@ type AgentAttribution struct { func (x *AgentAttribution) Reset() { *x = AgentAttribution{} - mi := &file_compass_v1_compass_proto_msgTypes[57] + mi := &file_compass_v1_compass_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3954,7 +4460,7 @@ func (x *AgentAttribution) String() string { func (*AgentAttribution) ProtoMessage() {} func (x *AgentAttribution) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[57] + mi := &file_compass_v1_compass_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3967,7 +4473,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{57} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{67} } func (x *AgentAttribution) GetAgentHandle() string { @@ -3987,7 +4493,7 @@ type ForgeRef struct { func (x *ForgeRef) Reset() { *x = ForgeRef{} - mi := &file_compass_v1_compass_proto_msgTypes[58] + mi := &file_compass_v1_compass_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3999,7 +4505,7 @@ func (x *ForgeRef) String() string { func (*ForgeRef) ProtoMessage() {} func (x *ForgeRef) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[58] + mi := &file_compass_v1_compass_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4012,7 +4518,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{58} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{68} } func (x *ForgeRef) GetProvider() ForgeProvider { @@ -4072,7 +4578,7 @@ type Issue struct { func (x *Issue) Reset() { *x = Issue{} - mi := &file_compass_v1_compass_proto_msgTypes[59] + mi := &file_compass_v1_compass_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4084,7 +4590,7 @@ func (x *Issue) String() string { func (*Issue) ProtoMessage() {} func (x *Issue) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[59] + mi := &file_compass_v1_compass_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4097,7 +4603,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{59} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{69} } func (x *Issue) GetId() string { @@ -4259,7 +4765,7 @@ type PullRequest struct { func (x *PullRequest) Reset() { *x = PullRequest{} - mi := &file_compass_v1_compass_proto_msgTypes[60] + mi := &file_compass_v1_compass_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4271,7 +4777,7 @@ func (x *PullRequest) String() string { func (*PullRequest) ProtoMessage() {} func (x *PullRequest) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[60] + mi := &file_compass_v1_compass_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4284,7 +4790,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{60} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{70} } func (x *PullRequest) GetForge() *ForgeRef { @@ -4405,7 +4911,7 @@ type ChecksSummary struct { func (x *ChecksSummary) Reset() { *x = ChecksSummary{} - mi := &file_compass_v1_compass_proto_msgTypes[61] + mi := &file_compass_v1_compass_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4417,7 +4923,7 @@ func (x *ChecksSummary) String() string { func (*ChecksSummary) ProtoMessage() {} func (x *ChecksSummary) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[61] + mi := &file_compass_v1_compass_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4430,7 +4936,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{61} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{71} } func (x *ChecksSummary) GetHeadSha() string { @@ -4466,7 +4972,7 @@ type Check struct { func (x *Check) Reset() { *x = Check{} - mi := &file_compass_v1_compass_proto_msgTypes[62] + mi := &file_compass_v1_compass_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4478,7 +4984,7 @@ func (x *Check) String() string { func (*Check) ProtoMessage() {} func (x *Check) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[62] + mi := &file_compass_v1_compass_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4491,7 +4997,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{62} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{72} } func (x *Check) GetName() string { @@ -4535,7 +5041,7 @@ type ChangedStats struct { func (x *ChangedStats) Reset() { *x = ChangedStats{} - mi := &file_compass_v1_compass_proto_msgTypes[63] + mi := &file_compass_v1_compass_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4547,7 +5053,7 @@ func (x *ChangedStats) String() string { func (*ChangedStats) ProtoMessage() {} func (x *ChangedStats) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[63] + mi := &file_compass_v1_compass_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4560,7 +5066,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{63} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{73} } func (x *ChangedStats) GetFiles() uint32 { @@ -4598,7 +5104,7 @@ type TrackerRef struct { func (x *TrackerRef) Reset() { *x = TrackerRef{} - mi := &file_compass_v1_compass_proto_msgTypes[64] + mi := &file_compass_v1_compass_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4610,7 +5116,7 @@ func (x *TrackerRef) String() string { func (*TrackerRef) ProtoMessage() {} func (x *TrackerRef) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[64] + mi := &file_compass_v1_compass_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4623,7 +5129,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{64} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{74} } func (x *TrackerRef) GetKind() string { @@ -4672,7 +5178,7 @@ type Review struct { func (x *Review) Reset() { *x = Review{} - mi := &file_compass_v1_compass_proto_msgTypes[65] + mi := &file_compass_v1_compass_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4684,7 +5190,7 @@ func (x *Review) String() string { func (*Review) ProtoMessage() {} func (x *Review) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[65] + mi := &file_compass_v1_compass_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4697,7 +5203,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{65} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{75} } func (x *Review) GetAuthor() string { @@ -4739,7 +5245,7 @@ type ReviewThread struct { func (x *ReviewThread) Reset() { *x = ReviewThread{} - mi := &file_compass_v1_compass_proto_msgTypes[66] + mi := &file_compass_v1_compass_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4751,7 +5257,7 @@ func (x *ReviewThread) String() string { func (*ReviewThread) ProtoMessage() {} func (x *ReviewThread) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[66] + mi := &file_compass_v1_compass_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4764,7 +5270,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{66} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{76} } func (x *ReviewThread) GetPath() string { @@ -4799,7 +5305,7 @@ type Comment struct { func (x *Comment) Reset() { *x = Comment{} - mi := &file_compass_v1_compass_proto_msgTypes[67] + mi := &file_compass_v1_compass_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4811,7 +5317,7 @@ func (x *Comment) String() string { func (*Comment) ProtoMessage() {} func (x *Comment) ProtoReflect() protoreflect.Message { - mi := &file_compass_v1_compass_proto_msgTypes[67] + mi := &file_compass_v1_compass_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4824,7 +5330,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{67} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{77} } func (x *Comment) GetAuthor() string { @@ -5060,7 +5566,37 @@ const file_compass_v1_compass_proto_rawDesc = "" + "\aprompts\x18\n" + " \x03(\tR\aprompts\"\x1a\n" + "\x18DeleteAgentConfigRequest\"\x1b\n" + - "\x19DeleteAgentConfigResponse\"5\n" + + "\x19DeleteAgentConfigResponse\"G\n" + + "\x0eModelCandidate\x12\x1a\n" + + "\bprovider\x18\x01 \x01(\tR\bprovider\x12\x19\n" + + "\bmodel_id\x18\x02 \x01(\tR\amodelId\"\xac\x01\n" + + "\rModelMetadata\x12%\n" + + "\x0econtext_window\x18\x01 \x01(\x03R\rcontextWindow\x12/\n" + + "\x14input_cost_micro_usd\x18\x02 \x01(\x03R\x11inputCostMicroUsd\x121\n" + + "\x15output_cost_micro_usd\x18\x03 \x01(\x03R\x12outputCostMicroUsd\x12\x10\n" + + "\x03api\x18\x04 \x01(\tR\x03api\"\xaa\x01\n" + + "\x12ModelRegistryEntry\x12!\n" + + "\fdisplay_name\x18\x01 \x01(\tR\vdisplayName\x12:\n" + + "\n" + + "candidates\x18\x02 \x03(\v2\x1a.compass.v1.ModelCandidateR\n" + + "candidates\x125\n" + + "\bmetadata\x18\x03 \x01(\v2\x19.compass.v1.ModelMetadataR\bmetadata\"\xad\x01\n" + + "\rModelRegistry\x12@\n" + + "\aentries\x18\x01 \x03(\v2&.compass.v1.ModelRegistry.EntriesEntryR\aentries\x1aZ\n" + + "\fEntriesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x124\n" + + "\x05value\x18\x02 \x01(\v2\x1e.compass.v1.ModelRegistryEntryR\x05value:\x028\x01\"{\n" + + "\x17PutModelRegistryRequest\x125\n" + + "\bregistry\x18\x01 \x01(\v2\x19.compass.v1.ModelRegistryR\bregistry\x12)\n" + + "\x10expected_version\x18\x02 \x01(\x03R\x0fexpectedVersion\"4\n" + + "\x18PutModelRegistryResponse\x12\x18\n" + + "\aversion\x18\x01 \x01(\x03R\aversion\"\x19\n" + + "\x17GetModelRegistryRequest\"k\n" + + "\x18GetModelRegistryResponse\x12\x18\n" + + "\aversion\x18\x01 \x01(\x03R\aversion\x125\n" + + "\bregistry\x18\x02 \x01(\v2\x19.compass.v1.ModelRegistryR\bregistry\"\x1c\n" + + "\x1aDeleteModelRegistryRequest\"\x1d\n" + + "\x1bDeleteModelRegistryResponse\"5\n" + "\x10AgentAttribution\x12!\n" + "\fagent_handle\x18\x01 \x01(\tR\vagentHandle\"U\n" + "\bForgeRef\x125\n" + @@ -5191,7 +5727,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\xad\f\n" + + "\x15FORGE_PROVIDER_LINEAR\x10\x042\xd3\x0e\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\x12\\\n" + @@ -5211,7 +5747,10 @@ const file_compass_v1_compass_proto_rawDesc = "" + "\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" + - "\x11DeleteAgentConfig\x12$.compass.v1.DeleteAgentConfigRequest\x1a%.compass.v1.DeleteAgentConfigResponse2\xfd\x01\n" + + "\x11DeleteAgentConfig\x12$.compass.v1.DeleteAgentConfigRequest\x1a%.compass.v1.DeleteAgentConfigResponse\x12]\n" + + "\x10PutModelRegistry\x12#.compass.v1.PutModelRegistryRequest\x1a$.compass.v1.PutModelRegistryResponse\x12]\n" + + "\x10GetModelRegistry\x12#.compass.v1.GetModelRegistryRequest\x1a$.compass.v1.GetModelRegistryResponse\x12f\n" + + "\x13DeleteModelRegistry\x12&.compass.v1.DeleteModelRegistryRequest\x1a'.compass.v1.DeleteModelRegistryResponse2\xfd\x01\n" + "\x0eSecretsService\x12H\n" + "\tSetSecret\x12\x1c.compass.v1.SetSecretRequest\x1a\x1d.compass.v1.SetSecretResponse\x12N\n" + "\vListSecrets\x12\x1e.compass.v1.ListSecretsRequest\x1a\x1f.compass.v1.ListSecretsResponse\x12Q\n" + @@ -5230,7 +5769,7 @@ func file_compass_v1_compass_proto_rawDescGZIP() []byte { } var file_compass_v1_compass_proto_enumTypes = make([]protoimpl.EnumInfo, 9) -var file_compass_v1_compass_proto_msgTypes = make([]protoimpl.MessageInfo, 68) +var file_compass_v1_compass_proto_msgTypes = make([]protoimpl.MessageInfo, 79) var file_compass_v1_compass_proto_goTypes = []any{ (SecretDelivery)(0), // 0: compass.v1.SecretDelivery (SecretKind)(0), // 1: compass.v1.SecretKind @@ -5298,18 +5837,29 @@ var file_compass_v1_compass_proto_goTypes = []any{ (*GetAgentConfigInfoResponse)(nil), // 63: compass.v1.GetAgentConfigInfoResponse (*DeleteAgentConfigRequest)(nil), // 64: compass.v1.DeleteAgentConfigRequest (*DeleteAgentConfigResponse)(nil), // 65: compass.v1.DeleteAgentConfigResponse - (*AgentAttribution)(nil), // 66: compass.v1.AgentAttribution - (*ForgeRef)(nil), // 67: compass.v1.ForgeRef - (*Issue)(nil), // 68: compass.v1.Issue - (*PullRequest)(nil), // 69: compass.v1.PullRequest - (*ChecksSummary)(nil), // 70: compass.v1.ChecksSummary - (*Check)(nil), // 71: compass.v1.Check - (*ChangedStats)(nil), // 72: compass.v1.ChangedStats - (*TrackerRef)(nil), // 73: compass.v1.TrackerRef - (*Review)(nil), // 74: compass.v1.Review - (*ReviewThread)(nil), // 75: compass.v1.ReviewThread - (*Comment)(nil), // 76: compass.v1.Comment - (*timestamppb.Timestamp)(nil), // 77: google.protobuf.Timestamp + (*ModelCandidate)(nil), // 66: compass.v1.ModelCandidate + (*ModelMetadata)(nil), // 67: compass.v1.ModelMetadata + (*ModelRegistryEntry)(nil), // 68: compass.v1.ModelRegistryEntry + (*ModelRegistry)(nil), // 69: compass.v1.ModelRegistry + (*PutModelRegistryRequest)(nil), // 70: compass.v1.PutModelRegistryRequest + (*PutModelRegistryResponse)(nil), // 71: compass.v1.PutModelRegistryResponse + (*GetModelRegistryRequest)(nil), // 72: compass.v1.GetModelRegistryRequest + (*GetModelRegistryResponse)(nil), // 73: compass.v1.GetModelRegistryResponse + (*DeleteModelRegistryRequest)(nil), // 74: compass.v1.DeleteModelRegistryRequest + (*DeleteModelRegistryResponse)(nil), // 75: compass.v1.DeleteModelRegistryResponse + (*AgentAttribution)(nil), // 76: compass.v1.AgentAttribution + (*ForgeRef)(nil), // 77: compass.v1.ForgeRef + (*Issue)(nil), // 78: compass.v1.Issue + (*PullRequest)(nil), // 79: compass.v1.PullRequest + (*ChecksSummary)(nil), // 80: compass.v1.ChecksSummary + (*Check)(nil), // 81: compass.v1.Check + (*ChangedStats)(nil), // 82: compass.v1.ChangedStats + (*TrackerRef)(nil), // 83: compass.v1.TrackerRef + (*Review)(nil), // 84: compass.v1.Review + (*ReviewThread)(nil), // 85: compass.v1.ReviewThread + (*Comment)(nil), // 86: compass.v1.Comment + nil, // 87: compass.v1.ModelRegistry.EntriesEntry + (*timestamppb.Timestamp)(nil), // 88: google.protobuf.Timestamp } var file_compass_v1_compass_proto_depIdxs = []int32{ 0, // 0: compass.v1.SetSecretRequest.delivery:type_name -> compass.v1.SecretDelivery @@ -5323,8 +5873,8 @@ var file_compass_v1_compass_proto_depIdxs = []int32{ 27, // 8: compass.v1.SubscribeEventsResponse.agent_message_chunk:type_name -> compass.v1.AgentMessageChunk 28, // 9: compass.v1.SubscribeEventsResponse.agent_tool_call:type_name -> compass.v1.AgentToolCall 29, // 10: compass.v1.SubscribeEventsResponse.agent_plan:type_name -> compass.v1.AgentPlan - 68, // 11: compass.v1.SubscribeEventsResponse.issue:type_name -> compass.v1.Issue - 68, // 12: compass.v1.ListBoardIssuesResponse.issues:type_name -> compass.v1.Issue + 78, // 11: compass.v1.SubscribeEventsResponse.issue:type_name -> compass.v1.Issue + 78, // 12: compass.v1.ListBoardIssuesResponse.issues:type_name -> compass.v1.Issue 2, // 13: compass.v1.ServerStatus.state:type_name -> compass.v1.ServerState 3, // 14: compass.v1.AgentSessionStatus.state:type_name -> compass.v1.AgentSessionState 4, // 15: compass.v1.AgentToolCall.status:type_name -> compass.v1.AgentToolCallStatus @@ -5345,66 +5895,78 @@ var file_compass_v1_compass_proto_depIdxs = []int32{ 31, // 30: compass.v1.AgentSessionFrame.event:type_name -> compass.v1.SessionEvent 3, // 31: compass.v1.AgentSessionFrame.state:type_name -> compass.v1.AgentSessionState 26, // 32: compass.v1.GetAgentStatusResponse.statuses:type_name -> compass.v1.AgentSessionStatus - 8, // 33: compass.v1.ForgeRef.provider:type_name -> compass.v1.ForgeProvider - 67, // 34: compass.v1.Issue.forge:type_name -> compass.v1.ForgeRef - 66, // 35: compass.v1.Issue.agent:type_name -> compass.v1.AgentAttribution - 77, // 36: compass.v1.Issue.updated_at:type_name -> google.protobuf.Timestamp - 7, // 37: compass.v1.Issue.state:type_name -> compass.v1.IssueState - 69, // 38: compass.v1.Issue.prs:type_name -> compass.v1.PullRequest - 73, // 39: compass.v1.Issue.tracker:type_name -> compass.v1.TrackerRef - 67, // 40: compass.v1.PullRequest.forge:type_name -> compass.v1.ForgeRef - 66, // 41: compass.v1.PullRequest.agent:type_name -> compass.v1.AgentAttribution - 72, // 42: compass.v1.PullRequest.changed:type_name -> compass.v1.ChangedStats - 70, // 43: compass.v1.PullRequest.checks:type_name -> compass.v1.ChecksSummary - 74, // 44: compass.v1.PullRequest.reviews:type_name -> compass.v1.Review - 75, // 45: compass.v1.PullRequest.threads:type_name -> compass.v1.ReviewThread - 71, // 46: compass.v1.ChecksSummary.checks:type_name -> compass.v1.Check - 76, // 47: compass.v1.ReviewThread.comments:type_name -> compass.v1.Comment - 16, // 48: compass.v1.CompassService.GetServerInfo:input_type -> compass.v1.GetServerInfoRequest - 18, // 49: compass.v1.CompassService.WhoAmI:input_type -> compass.v1.WhoAmIRequest - 20, // 50: compass.v1.CompassService.SubscribeEvents:input_type -> compass.v1.SubscribeEventsRequest - 22, // 51: compass.v1.CompassService.ListBoardIssues:input_type -> compass.v1.ListBoardIssuesRequest - 42, // 52: compass.v1.CompassService.ProvisionAgentWorkspace:input_type -> compass.v1.ProvisionAgentWorkspaceRequest - 46, // 53: compass.v1.CompassService.StartAgentSession:input_type -> compass.v1.StartAgentSessionRequest - 48, // 54: compass.v1.CompassService.SpawnAgent:input_type -> compass.v1.SpawnAgentRequest - 50, // 55: compass.v1.CompassService.StopAgentSession:input_type -> compass.v1.StopAgentSessionRequest - 44, // 56: compass.v1.CompassService.RemoveAgentWorkspace:input_type -> compass.v1.RemoveAgentWorkspaceRequest - 52, // 57: compass.v1.CompassService.ReloadAgentSession:input_type -> compass.v1.ReloadAgentSessionRequest - 54, // 58: compass.v1.CompassService.GetAgentStatus:input_type -> compass.v1.GetAgentStatusRequest - 40, // 59: compass.v1.CompassService.SubscribeAgentSession:input_type -> compass.v1.SubscribeAgentSessionRequest - 56, // 60: compass.v1.CompassService.IssueToken:input_type -> compass.v1.IssueTokenRequest - 58, // 61: compass.v1.CompassService.RevokeToken:input_type -> compass.v1.RevokeTokenRequest - 60, // 62: compass.v1.CompassService.PutAgentConfig:input_type -> compass.v1.PutAgentConfigRequest - 62, // 63: compass.v1.CompassService.GetAgentConfigInfo:input_type -> compass.v1.GetAgentConfigInfoRequest - 64, // 64: compass.v1.CompassService.DeleteAgentConfig:input_type -> compass.v1.DeleteAgentConfigRequest - 9, // 65: compass.v1.SecretsService.SetSecret:input_type -> compass.v1.SetSecretRequest - 11, // 66: compass.v1.SecretsService.ListSecrets:input_type -> compass.v1.ListSecretsRequest - 14, // 67: compass.v1.SecretsService.DeleteSecret:input_type -> compass.v1.DeleteSecretRequest - 17, // 68: compass.v1.CompassService.GetServerInfo:output_type -> compass.v1.GetServerInfoResponse - 19, // 69: compass.v1.CompassService.WhoAmI:output_type -> compass.v1.WhoAmIResponse - 21, // 70: compass.v1.CompassService.SubscribeEvents:output_type -> compass.v1.SubscribeEventsResponse - 23, // 71: compass.v1.CompassService.ListBoardIssues:output_type -> compass.v1.ListBoardIssuesResponse - 43, // 72: compass.v1.CompassService.ProvisionAgentWorkspace:output_type -> compass.v1.ProvisionAgentWorkspaceResponse - 47, // 73: compass.v1.CompassService.StartAgentSession:output_type -> compass.v1.StartAgentSessionResponse - 49, // 74: compass.v1.CompassService.SpawnAgent:output_type -> compass.v1.SpawnAgentResponse - 51, // 75: compass.v1.CompassService.StopAgentSession:output_type -> compass.v1.StopAgentSessionResponse - 45, // 76: compass.v1.CompassService.RemoveAgentWorkspace:output_type -> compass.v1.RemoveAgentWorkspaceResponse - 53, // 77: compass.v1.CompassService.ReloadAgentSession:output_type -> compass.v1.ReloadAgentSessionResponse - 55, // 78: compass.v1.CompassService.GetAgentStatus:output_type -> compass.v1.GetAgentStatusResponse - 41, // 79: compass.v1.CompassService.SubscribeAgentSession:output_type -> compass.v1.AgentSessionFrame - 57, // 80: compass.v1.CompassService.IssueToken:output_type -> compass.v1.IssueTokenResponse - 59, // 81: compass.v1.CompassService.RevokeToken:output_type -> compass.v1.RevokeTokenResponse - 61, // 82: compass.v1.CompassService.PutAgentConfig:output_type -> compass.v1.PutAgentConfigResponse - 63, // 83: compass.v1.CompassService.GetAgentConfigInfo:output_type -> compass.v1.GetAgentConfigInfoResponse - 65, // 84: compass.v1.CompassService.DeleteAgentConfig:output_type -> compass.v1.DeleteAgentConfigResponse - 10, // 85: compass.v1.SecretsService.SetSecret:output_type -> compass.v1.SetSecretResponse - 12, // 86: compass.v1.SecretsService.ListSecrets:output_type -> compass.v1.ListSecretsResponse - 15, // 87: compass.v1.SecretsService.DeleteSecret:output_type -> compass.v1.DeleteSecretResponse - 68, // [68:88] is the sub-list for method output_type - 48, // [48:68] is the sub-list for method input_type - 48, // [48:48] is the sub-list for extension type_name - 48, // [48:48] is the sub-list for extension extendee - 0, // [0:48] is the sub-list for field type_name + 66, // 33: compass.v1.ModelRegistryEntry.candidates:type_name -> compass.v1.ModelCandidate + 67, // 34: compass.v1.ModelRegistryEntry.metadata:type_name -> compass.v1.ModelMetadata + 87, // 35: compass.v1.ModelRegistry.entries:type_name -> compass.v1.ModelRegistry.EntriesEntry + 69, // 36: compass.v1.PutModelRegistryRequest.registry:type_name -> compass.v1.ModelRegistry + 69, // 37: compass.v1.GetModelRegistryResponse.registry:type_name -> compass.v1.ModelRegistry + 8, // 38: compass.v1.ForgeRef.provider:type_name -> compass.v1.ForgeProvider + 77, // 39: compass.v1.Issue.forge:type_name -> compass.v1.ForgeRef + 76, // 40: compass.v1.Issue.agent:type_name -> compass.v1.AgentAttribution + 88, // 41: compass.v1.Issue.updated_at:type_name -> google.protobuf.Timestamp + 7, // 42: compass.v1.Issue.state:type_name -> compass.v1.IssueState + 79, // 43: compass.v1.Issue.prs:type_name -> compass.v1.PullRequest + 83, // 44: compass.v1.Issue.tracker:type_name -> compass.v1.TrackerRef + 77, // 45: compass.v1.PullRequest.forge:type_name -> compass.v1.ForgeRef + 76, // 46: compass.v1.PullRequest.agent:type_name -> compass.v1.AgentAttribution + 82, // 47: compass.v1.PullRequest.changed:type_name -> compass.v1.ChangedStats + 80, // 48: compass.v1.PullRequest.checks:type_name -> compass.v1.ChecksSummary + 84, // 49: compass.v1.PullRequest.reviews:type_name -> compass.v1.Review + 85, // 50: compass.v1.PullRequest.threads:type_name -> compass.v1.ReviewThread + 81, // 51: compass.v1.ChecksSummary.checks:type_name -> compass.v1.Check + 86, // 52: compass.v1.ReviewThread.comments:type_name -> compass.v1.Comment + 68, // 53: compass.v1.ModelRegistry.EntriesEntry.value:type_name -> compass.v1.ModelRegistryEntry + 16, // 54: compass.v1.CompassService.GetServerInfo:input_type -> compass.v1.GetServerInfoRequest + 18, // 55: compass.v1.CompassService.WhoAmI:input_type -> compass.v1.WhoAmIRequest + 20, // 56: compass.v1.CompassService.SubscribeEvents:input_type -> compass.v1.SubscribeEventsRequest + 22, // 57: compass.v1.CompassService.ListBoardIssues:input_type -> compass.v1.ListBoardIssuesRequest + 42, // 58: compass.v1.CompassService.ProvisionAgentWorkspace:input_type -> compass.v1.ProvisionAgentWorkspaceRequest + 46, // 59: compass.v1.CompassService.StartAgentSession:input_type -> compass.v1.StartAgentSessionRequest + 48, // 60: compass.v1.CompassService.SpawnAgent:input_type -> compass.v1.SpawnAgentRequest + 50, // 61: compass.v1.CompassService.StopAgentSession:input_type -> compass.v1.StopAgentSessionRequest + 44, // 62: compass.v1.CompassService.RemoveAgentWorkspace:input_type -> compass.v1.RemoveAgentWorkspaceRequest + 52, // 63: compass.v1.CompassService.ReloadAgentSession:input_type -> compass.v1.ReloadAgentSessionRequest + 54, // 64: compass.v1.CompassService.GetAgentStatus:input_type -> compass.v1.GetAgentStatusRequest + 40, // 65: compass.v1.CompassService.SubscribeAgentSession:input_type -> compass.v1.SubscribeAgentSessionRequest + 56, // 66: compass.v1.CompassService.IssueToken:input_type -> compass.v1.IssueTokenRequest + 58, // 67: compass.v1.CompassService.RevokeToken:input_type -> compass.v1.RevokeTokenRequest + 60, // 68: compass.v1.CompassService.PutAgentConfig:input_type -> compass.v1.PutAgentConfigRequest + 62, // 69: compass.v1.CompassService.GetAgentConfigInfo:input_type -> compass.v1.GetAgentConfigInfoRequest + 64, // 70: compass.v1.CompassService.DeleteAgentConfig:input_type -> compass.v1.DeleteAgentConfigRequest + 70, // 71: compass.v1.CompassService.PutModelRegistry:input_type -> compass.v1.PutModelRegistryRequest + 72, // 72: compass.v1.CompassService.GetModelRegistry:input_type -> compass.v1.GetModelRegistryRequest + 74, // 73: compass.v1.CompassService.DeleteModelRegistry:input_type -> compass.v1.DeleteModelRegistryRequest + 9, // 74: compass.v1.SecretsService.SetSecret:input_type -> compass.v1.SetSecretRequest + 11, // 75: compass.v1.SecretsService.ListSecrets:input_type -> compass.v1.ListSecretsRequest + 14, // 76: compass.v1.SecretsService.DeleteSecret:input_type -> compass.v1.DeleteSecretRequest + 17, // 77: compass.v1.CompassService.GetServerInfo:output_type -> compass.v1.GetServerInfoResponse + 19, // 78: compass.v1.CompassService.WhoAmI:output_type -> compass.v1.WhoAmIResponse + 21, // 79: compass.v1.CompassService.SubscribeEvents:output_type -> compass.v1.SubscribeEventsResponse + 23, // 80: compass.v1.CompassService.ListBoardIssues:output_type -> compass.v1.ListBoardIssuesResponse + 43, // 81: compass.v1.CompassService.ProvisionAgentWorkspace:output_type -> compass.v1.ProvisionAgentWorkspaceResponse + 47, // 82: compass.v1.CompassService.StartAgentSession:output_type -> compass.v1.StartAgentSessionResponse + 49, // 83: compass.v1.CompassService.SpawnAgent:output_type -> compass.v1.SpawnAgentResponse + 51, // 84: compass.v1.CompassService.StopAgentSession:output_type -> compass.v1.StopAgentSessionResponse + 45, // 85: compass.v1.CompassService.RemoveAgentWorkspace:output_type -> compass.v1.RemoveAgentWorkspaceResponse + 53, // 86: compass.v1.CompassService.ReloadAgentSession:output_type -> compass.v1.ReloadAgentSessionResponse + 55, // 87: compass.v1.CompassService.GetAgentStatus:output_type -> compass.v1.GetAgentStatusResponse + 41, // 88: compass.v1.CompassService.SubscribeAgentSession:output_type -> compass.v1.AgentSessionFrame + 57, // 89: compass.v1.CompassService.IssueToken:output_type -> compass.v1.IssueTokenResponse + 59, // 90: compass.v1.CompassService.RevokeToken:output_type -> compass.v1.RevokeTokenResponse + 61, // 91: compass.v1.CompassService.PutAgentConfig:output_type -> compass.v1.PutAgentConfigResponse + 63, // 92: compass.v1.CompassService.GetAgentConfigInfo:output_type -> compass.v1.GetAgentConfigInfoResponse + 65, // 93: compass.v1.CompassService.DeleteAgentConfig:output_type -> compass.v1.DeleteAgentConfigResponse + 71, // 94: compass.v1.CompassService.PutModelRegistry:output_type -> compass.v1.PutModelRegistryResponse + 73, // 95: compass.v1.CompassService.GetModelRegistry:output_type -> compass.v1.GetModelRegistryResponse + 75, // 96: compass.v1.CompassService.DeleteModelRegistry:output_type -> compass.v1.DeleteModelRegistryResponse + 10, // 97: compass.v1.SecretsService.SetSecret:output_type -> compass.v1.SetSecretResponse + 12, // 98: compass.v1.SecretsService.ListSecrets:output_type -> compass.v1.ListSecretsResponse + 15, // 99: compass.v1.SecretsService.DeleteSecret:output_type -> compass.v1.DeleteSecretResponse + 77, // [77:100] is the sub-list for method output_type + 54, // [54:77] is the sub-list for method input_type + 54, // [54:54] is the sub-list for extension type_name + 54, // [54:54] is the sub-list for extension extendee + 0, // [0:54] is the sub-list for field type_name } func init() { file_compass_v1_compass_proto_init() } @@ -5438,7 +6000,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: 9, - NumMessages: 68, + NumMessages: 79, 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 61308ac92..afc834f94 100644 --- a/go/gen/compass/v1/compassv1connect/compass.connect.go +++ b/go/gen/compass/v1/compassv1connect/compass.connect.go @@ -93,6 +93,15 @@ const ( // CompassServiceDeleteAgentConfigProcedure is the fully-qualified name of the CompassService's // DeleteAgentConfig RPC. CompassServiceDeleteAgentConfigProcedure = "/compass.v1.CompassService/DeleteAgentConfig" + // CompassServicePutModelRegistryProcedure is the fully-qualified name of the CompassService's + // PutModelRegistry RPC. + CompassServicePutModelRegistryProcedure = "/compass.v1.CompassService/PutModelRegistry" + // CompassServiceGetModelRegistryProcedure is the fully-qualified name of the CompassService's + // GetModelRegistry RPC. + CompassServiceGetModelRegistryProcedure = "/compass.v1.CompassService/GetModelRegistry" + // CompassServiceDeleteModelRegistryProcedure is the fully-qualified name of the CompassService's + // DeleteModelRegistry RPC. + CompassServiceDeleteModelRegistryProcedure = "/compass.v1.CompassService/DeleteModelRegistry" // SecretsServiceSetSecretProcedure is the fully-qualified name of the SecretsService's SetSecret // RPC. SecretsServiceSetSecretProcedure = "/compass.v1.SecretsService/SetSecret" @@ -228,6 +237,26 @@ type CompassServiceClient interface { // when already empty succeeds. On success every live Runner is signalled with // an empty config version so it re-materializes the empty config dir. DeleteAgentConfig(context.Context, *connect.Request[v1.DeleteAgentConfigRequest]) (*connect.Response[v1.DeleteAgentConfigResponse], error) + // Declare the fleet model registry: the stable-name → candidate-chain map the + // gateway resolver routes with (compass-stable-name-routing §P2). Compare-and- + // set on the whole-registry version — the caller carries the version it read + // (0 to seed the first registry) and the write lands only if the row still + // holds it, so a racing operator write is never clobbered; a stale version is + // ABORTED. Admin-gated operator write (operator-scoped only: agents never + // author). The payload is validated fail-closed at the RPC boundary (schema + // shape, candidate provider/model_id shape, and no removal that would orphan a + // published profile's models.* reference) before a row is written. + PutModelRegistry(context.Context, *connect.Request[v1.PutModelRegistryRequest]) (*connect.Response[v1.PutModelRegistryResponse], error) + // Report the current model registry: its version and full payload. An + // unconfigured fleet is a valid state — the response is empty (version 0, no + // entries), never an error. Value-free of credentials by construction (the + // registry names providers/models, never holds keys), so it is open to any + // authenticated account like GetAgentConfigInfo. + GetModelRegistry(context.Context, *connect.Request[v1.GetModelRegistryRequest]) (*connect.Response[v1.GetModelRegistryResponse], error) + // Clear the fleet model registry back to the unconfigured state. Admin-gated + // like PutModelRegistry. Fails closed if the registry being cleared holds any + // stable name still referenced by a published profile's models.* map. + DeleteModelRegistry(context.Context, *connect.Request[v1.DeleteModelRegistryRequest]) (*connect.Response[v1.DeleteModelRegistryResponse], error) } // NewCompassServiceClient constructs a client for the compass.v1.CompassService service. By @@ -343,6 +372,24 @@ func NewCompassServiceClient(httpClient connect.HTTPClient, baseURL string, opts connect.WithSchema(compassServiceMethods.ByName("DeleteAgentConfig")), connect.WithClientOptions(opts...), ), + putModelRegistry: connect.NewClient[v1.PutModelRegistryRequest, v1.PutModelRegistryResponse]( + httpClient, + baseURL+CompassServicePutModelRegistryProcedure, + connect.WithSchema(compassServiceMethods.ByName("PutModelRegistry")), + connect.WithClientOptions(opts...), + ), + getModelRegistry: connect.NewClient[v1.GetModelRegistryRequest, v1.GetModelRegistryResponse]( + httpClient, + baseURL+CompassServiceGetModelRegistryProcedure, + connect.WithSchema(compassServiceMethods.ByName("GetModelRegistry")), + connect.WithClientOptions(opts...), + ), + deleteModelRegistry: connect.NewClient[v1.DeleteModelRegistryRequest, v1.DeleteModelRegistryResponse]( + httpClient, + baseURL+CompassServiceDeleteModelRegistryProcedure, + connect.WithSchema(compassServiceMethods.ByName("DeleteModelRegistry")), + connect.WithClientOptions(opts...), + ), } } @@ -365,6 +412,9 @@ type compassServiceClient struct { putAgentConfig *connect.Client[v1.PutAgentConfigRequest, v1.PutAgentConfigResponse] getAgentConfigInfo *connect.Client[v1.GetAgentConfigInfoRequest, v1.GetAgentConfigInfoResponse] deleteAgentConfig *connect.Client[v1.DeleteAgentConfigRequest, v1.DeleteAgentConfigResponse] + putModelRegistry *connect.Client[v1.PutModelRegistryRequest, v1.PutModelRegistryResponse] + getModelRegistry *connect.Client[v1.GetModelRegistryRequest, v1.GetModelRegistryResponse] + deleteModelRegistry *connect.Client[v1.DeleteModelRegistryRequest, v1.DeleteModelRegistryResponse] } // GetServerInfo calls compass.v1.CompassService.GetServerInfo. @@ -452,6 +502,21 @@ func (c *compassServiceClient) DeleteAgentConfig(ctx context.Context, req *conne return c.deleteAgentConfig.CallUnary(ctx, req) } +// PutModelRegistry calls compass.v1.CompassService.PutModelRegistry. +func (c *compassServiceClient) PutModelRegistry(ctx context.Context, req *connect.Request[v1.PutModelRegistryRequest]) (*connect.Response[v1.PutModelRegistryResponse], error) { + return c.putModelRegistry.CallUnary(ctx, req) +} + +// GetModelRegistry calls compass.v1.CompassService.GetModelRegistry. +func (c *compassServiceClient) GetModelRegistry(ctx context.Context, req *connect.Request[v1.GetModelRegistryRequest]) (*connect.Response[v1.GetModelRegistryResponse], error) { + return c.getModelRegistry.CallUnary(ctx, req) +} + +// DeleteModelRegistry calls compass.v1.CompassService.DeleteModelRegistry. +func (c *compassServiceClient) DeleteModelRegistry(ctx context.Context, req *connect.Request[v1.DeleteModelRegistryRequest]) (*connect.Response[v1.DeleteModelRegistryResponse], error) { + return c.deleteModelRegistry.CallUnary(ctx, req) +} + // CompassServiceHandler is an implementation of the compass.v1.CompassService service. type CompassServiceHandler interface { // Liveness probe plus the server's build + contract version. The first @@ -576,6 +641,26 @@ type CompassServiceHandler interface { // when already empty succeeds. On success every live Runner is signalled with // an empty config version so it re-materializes the empty config dir. DeleteAgentConfig(context.Context, *connect.Request[v1.DeleteAgentConfigRequest]) (*connect.Response[v1.DeleteAgentConfigResponse], error) + // Declare the fleet model registry: the stable-name → candidate-chain map the + // gateway resolver routes with (compass-stable-name-routing §P2). Compare-and- + // set on the whole-registry version — the caller carries the version it read + // (0 to seed the first registry) and the write lands only if the row still + // holds it, so a racing operator write is never clobbered; a stale version is + // ABORTED. Admin-gated operator write (operator-scoped only: agents never + // author). The payload is validated fail-closed at the RPC boundary (schema + // shape, candidate provider/model_id shape, and no removal that would orphan a + // published profile's models.* reference) before a row is written. + PutModelRegistry(context.Context, *connect.Request[v1.PutModelRegistryRequest]) (*connect.Response[v1.PutModelRegistryResponse], error) + // Report the current model registry: its version and full payload. An + // unconfigured fleet is a valid state — the response is empty (version 0, no + // entries), never an error. Value-free of credentials by construction (the + // registry names providers/models, never holds keys), so it is open to any + // authenticated account like GetAgentConfigInfo. + GetModelRegistry(context.Context, *connect.Request[v1.GetModelRegistryRequest]) (*connect.Response[v1.GetModelRegistryResponse], error) + // Clear the fleet model registry back to the unconfigured state. Admin-gated + // like PutModelRegistry. Fails closed if the registry being cleared holds any + // stable name still referenced by a published profile's models.* map. + DeleteModelRegistry(context.Context, *connect.Request[v1.DeleteModelRegistryRequest]) (*connect.Response[v1.DeleteModelRegistryResponse], error) } // NewCompassServiceHandler builds an HTTP handler from the service implementation. It returns the @@ -687,6 +772,24 @@ func NewCompassServiceHandler(svc CompassServiceHandler, opts ...connect.Handler connect.WithSchema(compassServiceMethods.ByName("DeleteAgentConfig")), connect.WithHandlerOptions(opts...), ) + compassServicePutModelRegistryHandler := connect.NewUnaryHandler( + CompassServicePutModelRegistryProcedure, + svc.PutModelRegistry, + connect.WithSchema(compassServiceMethods.ByName("PutModelRegistry")), + connect.WithHandlerOptions(opts...), + ) + compassServiceGetModelRegistryHandler := connect.NewUnaryHandler( + CompassServiceGetModelRegistryProcedure, + svc.GetModelRegistry, + connect.WithSchema(compassServiceMethods.ByName("GetModelRegistry")), + connect.WithHandlerOptions(opts...), + ) + compassServiceDeleteModelRegistryHandler := connect.NewUnaryHandler( + CompassServiceDeleteModelRegistryProcedure, + svc.DeleteModelRegistry, + connect.WithSchema(compassServiceMethods.ByName("DeleteModelRegistry")), + connect.WithHandlerOptions(opts...), + ) return "/compass.v1.CompassService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case CompassServiceGetServerInfoProcedure: @@ -723,6 +826,12 @@ func NewCompassServiceHandler(svc CompassServiceHandler, opts ...connect.Handler compassServiceGetAgentConfigInfoHandler.ServeHTTP(w, r) case CompassServiceDeleteAgentConfigProcedure: compassServiceDeleteAgentConfigHandler.ServeHTTP(w, r) + case CompassServicePutModelRegistryProcedure: + compassServicePutModelRegistryHandler.ServeHTTP(w, r) + case CompassServiceGetModelRegistryProcedure: + compassServiceGetModelRegistryHandler.ServeHTTP(w, r) + case CompassServiceDeleteModelRegistryProcedure: + compassServiceDeleteModelRegistryHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -800,6 +909,18 @@ func (UnimplementedCompassServiceHandler) DeleteAgentConfig(context.Context, *co return nil, connect.NewError(connect.CodeUnimplemented, errors.New("compass.v1.CompassService.DeleteAgentConfig is not implemented")) } +func (UnimplementedCompassServiceHandler) PutModelRegistry(context.Context, *connect.Request[v1.PutModelRegistryRequest]) (*connect.Response[v1.PutModelRegistryResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("compass.v1.CompassService.PutModelRegistry is not implemented")) +} + +func (UnimplementedCompassServiceHandler) GetModelRegistry(context.Context, *connect.Request[v1.GetModelRegistryRequest]) (*connect.Response[v1.GetModelRegistryResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("compass.v1.CompassService.GetModelRegistry is not implemented")) +} + +func (UnimplementedCompassServiceHandler) DeleteModelRegistry(context.Context, *connect.Request[v1.DeleteModelRegistryRequest]) (*connect.Response[v1.DeleteModelRegistryResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("compass.v1.CompassService.DeleteModelRegistry is not implemented")) +} + // SecretsServiceClient is a client for the compass.v1.SecretsService service. type SecretsServiceClient interface { // Declare a secret's registry row (name/delivery/kind/routing) and write its diff --git a/go/internal/auth/admin_gate.go b/go/internal/auth/admin_gate.go index e51c798d0..39f13683e 100644 --- a/go/internal/auth/admin_gate.go +++ b/go/internal/auth/admin_gate.go @@ -65,6 +65,8 @@ func classifyProcedure(procedure string) (privilege, bool) { compassv1connect.CompassServiceIssueTokenProcedure, compassv1connect.CompassServiceRevokeTokenProcedure, compassv1connect.CompassServicePutAgentConfigProcedure, + compassv1connect.CompassServicePutModelRegistryProcedure, + compassv1connect.CompassServiceDeleteModelRegistryProcedure, compassv1connect.CompassServiceDeleteAgentConfigProcedure: return adminOnly{}, true @@ -85,7 +87,8 @@ func classifyProcedure(procedure string) (privilege, bool) { compassv1connect.CompassServiceSubscribeEventsProcedure, compassv1connect.CompassServiceListBoardIssuesProcedure, compassv1connect.CompassServiceSubscribeAgentSessionProcedure, - compassv1connect.CompassServiceGetAgentConfigInfoProcedure: + compassv1connect.CompassServiceGetAgentConfigInfoProcedure, + compassv1connect.CompassServiceGetModelRegistryProcedure: return authenticatedOpen{}, true // Every CommsService method: open to any authenticated account (the service diff --git a/go/internal/store/agent_config.go b/go/internal/store/agent_config.go index 150fee11b..d0e2b638a 100644 --- a/go/internal/store/agent_config.go +++ b/go/internal/store/agent_config.go @@ -142,9 +142,12 @@ func (c *cappedReader) Read(p []byte) (int, error) { // absolute or ".." paths, carry a symlink or hardlink member, violate the // grammar, exceed the decompressed-size or file-count cap, or contain an // mcp/*.json that is not valid JSON, is rejected as a %w-wrapped -// ErrInvalidArgument and NO row is written. actor is the operator-scoped writer -// (empty → ErrInvalidArgument); the singleton has no per-actor column, but the -// param documents the operator-scoped write and matches the record. +// ErrInvalidArgument and NO row is written. A bundle whose profile references a +// model stable name absent from the current registry (the reverse orphan guard, +// design.md §P2 L530-532) is likewise ErrInvalidArgument. actor is the +// operator-scoped writer (empty → ErrInvalidArgument); the singleton has no +// per-actor column, but the param documents the operator-scoped write and +// matches the record. func (s *Store) PutAgentConfig(ctx context.Context, actor AccountID, bundle []byte) (version string, err error) { if actor == "" { return "", fmt.Errorf("%w: config-bundle writer account id is required", ErrInvalidArgument) @@ -153,6 +156,14 @@ func (s *Store) PutAgentConfig(ctx context.Context, actor AccountID, bundle []by if err != nil { return "", err } + // Reverse orphan guard (design.md §P2 L530-532): a profile that pins a model + // stable name absent from the current registry (and not an escape-hatch + // provider/id selector) fails closed here rather than publishing a stranded + // reference. See checkBundleProfileRefsAgainstRegistry for the accepted + // non-transactional two-store TOCTOU window this shares with PutModelRegistry. + if err := s.checkBundleProfileRefsAgainstRegistry(ctx, bundle); err != nil { + return "", err + } if err := s.q.PutAgentConfig(ctx, db.PutAgentConfigParams{ Version: version, Bundle: bundle, @@ -338,6 +349,63 @@ func configBundleMemberNames(bundle []byte) (AgentConfigInfoResult, error) { return info, nil } +// configBundleProfileBodies walks a stored, already-validated config bundle and +// returns each published profile's raw profile.yml body, keyed by the profile +// . It reuses the same tar-walk + grammar (configMemberParts + the +// cappedReader gzip-bomb guard) the store door enforced at Put, so it reads only +// trusted content. The model-registry orphan cross-check +// (publishedProfileModelRefs) consumes it to learn which stable names the +// published profiles reference. A bundle with no profiles yields an empty map. +// +// This deliberately RE-WALKS the bundle validateAndHashConfigBundle already +// walked at Put (which collects the same profile bodies into profileBodies for +// the agent-key lint): on the operator write path a bundle is gunzipped and +// tar-walked twice. The duplication is accepted for now to keep the Put door's +// signature unchanged; folding the two walks into one (threading the already- +// collected bodies through) is tracked as a follow-up (RIG-3220). If either walk's +// grammar changes, the other MUST change in step or the lint reads a different +// member set than the door validated. +func configBundleProfileBodies(bundle []byte) (map[string][]byte, error) { + gz, err := gzip.NewReader(bytes.NewReader(bundle)) + if err != nil { + return nil, fmt.Errorf("%w: bundle is not a valid gzip stream: %w", ErrInvalidArgument, err) + } + // Read-only gunzip: Close only releases the decompressor, so its error is + // not actionable here (nothing was written to flush). + defer func() { _ = gz.Close() }() + + bodies := make(map[string][]byte) + tr := tar.NewReader(&cappedReader{r: gz}) + for { + hdr, err := tr.Next() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + if errors.Is(err, errBundleTooLarge) { + return nil, errBundleTooLarge + } + return nil, fmt.Errorf("%w: bundle is not a valid tar stream: %w", ErrInvalidArgument, err) + } + parts, err := configMemberParts(hdr.Name) + if err != nil { + return nil, err + } + if hdr.Typeflag == tar.TypeDir { + continue + } + // A profile is exactly profiles//profile.yml (door grammar). + if len(parts) == 3 && parts[0] == topDirProfiles && parts[2] == memberProfileYML { + content, err := io.ReadAll(tr) + if err != nil { + return nil, fmt.Errorf("%w: reading profile member %q: %w", ErrInvalidArgument, hdr.Name, err) + } + bodies[parts[1]] = content + } + } + return bodies, nil +} + // trimAnySuffix trims the first matching suffix from s, else returns s. func trimAnySuffix(s string, suffixes ...string) string { for _, suf := range suffixes { diff --git a/go/internal/store/db/model_registry.sql.go b/go/internal/store/db/model_registry.sql.go new file mode 100644 index 000000000..899c47858 --- /dev/null +++ b/go/internal/store/db/model_registry.sql.go @@ -0,0 +1,84 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: model_registry.sql + +package db + +import ( + "context" +) + +const currentModelRegistry = `-- name: CurrentModelRegistry :one + +SELECT version, registry FROM model_registry WHERE singleton = TRUE +` + +type CurrentModelRegistryRow struct { + Version int64 + Registry []byte +} + +// Model-registry queries (RIG-3122 P2). Back the hand-written Store methods in +// internal/store/model_registry.go, which own the fail-closed payload +// validation (ValidateModelRegistry), the JSONB marshal/unmarshal, and the +// ErrVersionConflict/ErrNotFound mapping. The registry is a fleet-wide singleton +// row (singleton = TRUE) with a monotonic version supplying the CAS substrate: +// a write only lands if the row still holds the version the caller read. +func (q *Queries) CurrentModelRegistry(ctx context.Context) (CurrentModelRegistryRow, error) { + row := q.db.QueryRow(ctx, currentModelRegistry) + var i CurrentModelRegistryRow + err := row.Scan(&i.Version, &i.Registry) + return i, err +} + +const deleteModelRegistry = `-- name: DeleteModelRegistry :exec +DELETE FROM model_registry WHERE singleton = TRUE +` + +func (q *Queries) DeleteModelRegistry(ctx context.Context) error { + _, err := q.db.Exec(ctx, deleteModelRegistry) + return err +} + +const insertModelRegistry = `-- name: InsertModelRegistry :one +INSERT INTO model_registry (singleton, version, registry) +VALUES (TRUE, 1, $1) +ON CONFLICT (singleton) DO NOTHING +RETURNING version +` + +// InsertModelRegistry seeds the FIRST registry (the caller read no row, expected +// version 0). ON CONFLICT DO NOTHING makes it a CAS: it lands only when the +// singleton is still absent, so a racing seed loses (zero rows, ErrNoRows via +// RETURNING) rather than clobbering the winner. The seeded version is 1. +func (q *Queries) InsertModelRegistry(ctx context.Context, registry []byte) (int64, error) { + row := q.db.QueryRow(ctx, insertModelRegistry, registry) + var version int64 + err := row.Scan(&version) + return version, err +} + +const updateModelRegistry = `-- name: UpdateModelRegistry :one +UPDATE model_registry + SET registry = $1, version = version + 1, updated_at = now() + WHERE singleton = TRUE AND version = $2 +RETURNING version +` + +type UpdateModelRegistryParams struct { + Registry []byte + Version int64 +} + +// UpdateModelRegistry is the compare-and-set write over an existing row: it +// lands only when the row still holds $2 (the version the caller read), bumping +// to version + 1 and returning the new version. A stale/racing expected version +// matches no row (ErrNoRows via RETURNING) — the caller maps that to +// ErrVersionConflict. +func (q *Queries) UpdateModelRegistry(ctx context.Context, arg UpdateModelRegistryParams) (int64, error) { + row := q.db.QueryRow(ctx, updateModelRegistry, arg.Registry, arg.Version) + var version int64 + err := row.Scan(&version) + return version, err +} diff --git a/go/internal/store/db/models.go b/go/internal/store/db/models.go index fe8f8ce4b..76f968c39 100644 --- a/go/internal/store/db/models.go +++ b/go/internal/store/db/models.go @@ -238,6 +238,14 @@ type Message struct { TenantID string } +type ModelRegistry struct { + Singleton bool + Version int64 + Registry []byte + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz +} + type OwedMention struct { AgentAccountID string MessageID string diff --git a/go/internal/store/db/querier.go b/go/internal/store/db/querier.go index 82c7333df..5c1aa2d5b 100644 --- a/go/internal/store/db/querier.go +++ b/go/internal/store/db/querier.go @@ -73,6 +73,13 @@ type Querier interface { CountOwedMentions(ctx context.Context) (int64, error) CountRootAgents(ctx context.Context, ownerUserID string) (int64, error) CurrentAgentConfig(ctx context.Context) (CurrentAgentConfigRow, error) + // Model-registry queries (RIG-3122 P2). Back the hand-written Store methods in + // internal/store/model_registry.go, which own the fail-closed payload + // validation (ValidateModelRegistry), the JSONB marshal/unmarshal, and the + // ErrVersionConflict/ErrNotFound mapping. The registry is a fleet-wide singleton + // row (singleton = TRUE) with a monotonic version supplying the CAS substrate: + // a write only lands if the row still holds the version the caller read. + CurrentModelRegistry(ctx context.Context) (CurrentModelRegistryRow, error) DeclaredSecrets(ctx context.Context) ([]DeclaredSecretsRow, error) DeleteAgentConfig(ctx context.Context) error // Scoped to the calling agent (id AND agent). RETURNING the coordinate drives the @@ -82,6 +89,7 @@ type Querier interface { DeleteChannelMember(ctx context.Context, arg DeleteChannelMemberParams) (int64, error) DeleteChannelPin(ctx context.Context, arg DeleteChannelPinParams) error DeleteChannelPinReturningPosition(ctx context.Context, arg DeleteChannelPinReturningPositionParams) (int32, error) + DeleteModelRegistry(ctx context.Context) error DeleteSecret(ctx context.Context, name string) (int64, error) DeleteTopic(ctx context.Context, id string) error // Agent-forge-subscription / artifact-cursor queries (sqlc adoption T6, @@ -209,6 +217,11 @@ type Querier interface { InsertDMChannel(ctx context.Context, arg InsertDMChannelParams) (string, error) InsertHomeChannel(ctx context.Context, arg InsertHomeChannelParams) error InsertMessage(ctx context.Context, arg InsertMessageParams) (InsertMessageRow, error) + // InsertModelRegistry seeds the FIRST registry (the caller read no row, expected + // version 0). ON CONFLICT DO NOTHING makes it a CAS: it lands only when the + // singleton is still absent, so a racing seed loses (zero rows, ErrNoRows via + // RETURNING) rather than clobbering the winner. The seeded version is 1. + InsertModelRegistry(ctx context.Context, registry []byte) (int64, error) InsertOwnerDMGroup(ctx context.Context, arg InsertOwnerDMGroupParams) error // Secrets-registry queries (sqlc adoption T6, RIG-3034). These replace the inline // SQL literals in internal/store/secrets.go; the hand-written Store methods keep @@ -397,6 +410,12 @@ type Querier interface { UpdateChannelPolicy(ctx context.Context, arg UpdateChannelPolicyParams) error UpdateMessageBlocks(ctx context.Context, arg UpdateMessageBlocksParams) (int64, error) UpdateMessageBlocksAsAuthor(ctx context.Context, arg UpdateMessageBlocksAsAuthorParams) (UpdateMessageBlocksAsAuthorRow, error) + // UpdateModelRegistry is the compare-and-set write over an existing row: it + // lands only when the row still holds $2 (the version the caller read), bumping + // to version + 1 and returning the new version. A stale/racing expected version + // matches no row (ErrNoRows via RETURNING) — the caller maps that to + // ErrVersionConflict. + UpdateModelRegistry(ctx context.Context, arg UpdateModelRegistryParams) (int64, error) UpdateTopicLastSeq(ctx context.Context, arg UpdateTopicLastSeqParams) error UpsertChannelMember(ctx context.Context, arg UpsertChannelMemberParams) error UpsertForgeArtifactCursor(ctx context.Context, arg UpsertForgeArtifactCursorParams) error diff --git a/go/internal/store/errors.go b/go/internal/store/errors.go index 3f059492c..278db8949 100644 --- a/go/internal/store/errors.go +++ b/go/internal/store/errors.go @@ -55,6 +55,16 @@ var ( // edge maps it to FAILED_PRECONDITION. ErrFailedPrecondition = errors.New("store: failed precondition") + // ErrVersionConflict is returned by a compare-and-set store write whose + // expected version does not match the row's current version — a racing + // operator write landed first. Distinct from ErrConflict (a uniqueness or + // terminal-state refusal) so the RPC edge can map a failed CAS to + // CodeAborted (the connect/gRPC convention for "retry after re-reading"), + // never to InvalidArgument. The model_registry store is the first CAS-on- + // version writer (RIG-3122 P2); the sentinel lives here so the mapping is + // stable and %w-wrapped context can ride along. + ErrVersionConflict = errors.New("store: version conflict") + // ErrSchemaVersion is returned by Open when the database's applied schema // version does not match the version this binary's embedded migrations // define — the refuse-to-serve guard (design.md:1136-1137). A newer diff --git a/go/internal/store/migrations/0001_init.sql b/go/internal/store/migrations/0001_init.sql index a7c883dde..1c107cbb9 100644 --- a/go/internal/store/migrations/0001_init.sql +++ b/go/internal/store/migrations/0001_init.sql @@ -626,6 +626,37 @@ CREATE TABLE agent_config_bundle ( updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); +-- ── Model registry (fleet singleton, CAS-versioned) ────────────────────────── +-- The Server-side STABLE-NAME registry (RIG-3122 P2): the ONE fleet-wide map +-- from a stable model name to its ordered candidate chain + listing metadata, +-- which the gateway resolver reads to route a request's modelId (design.md +-- compass-stable-name-routing §P1/P2). Like agent_config_bundle it is a fleet +-- SINGLETON — one registry for the whole fleet — but UNLIKE it the write path +-- is COMPARE-AND-SET on a monotonic version, not a content-hash upsert: +-- * singleton BOOLEAN PRIMARY KEY DEFAULT TRUE CHECK (singleton) — the PK is a +-- constant TRUE (same one-row pin as agent_config_bundle): a second INSERT +-- collides on the PK and the CHECK forbids any other value, so the table +-- holds exactly one row. +-- * version BIGINT — a monotonic whole-registry version (NOT a content hash). +-- It supplies the CAS substrate the gateway credentials store also uses (a +-- monotonic version per row, compass-server-llm-gateway/design.md:324-329): +-- a Put carries the version it read and only lands if the row still holds +-- it (version = version + 1), so a racing operator write is never clobbered. +-- The version also keys the gateway resolver's in-memory ref (P1), which +-- re-reads only on a version change. +-- * registry JSONB — the whole registry payload (name -> {display_name, +-- ordered candidates [{provider, model_id}], metadata}); validated +-- fail-closed at the RPC boundary (ValidateModelRegistry) before any write. +-- * CURRENT-ONLY retention — no history; a Put REPLACES the row's payload in +-- place and bumps the version. +CREATE TABLE model_registry ( + singleton BOOLEAN PRIMARY KEY DEFAULT TRUE CHECK (singleton), + version BIGINT NOT NULL, + registry JSONB NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + -- ── Issues: the durable board issue ────────────────────────────────────────── -- The store-of-record for a Compass board issue (RIG-1728, DL-019): the -- forge-derived facts a poll ingests, plus the Compass-owned machinery a board diff --git a/go/internal/store/model_registry.go b/go/internal/store/model_registry.go new file mode 100644 index 000000000..4cdd0cb5c --- /dev/null +++ b/go/internal/store/model_registry.go @@ -0,0 +1,559 @@ +package store + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + "unicode" + + "github.com/RigelBuild/compass/go/internal/store/db" +) + +// The fleet MODEL REGISTRY store (RIG-3122 P2). One fleet-wide singleton row +// (model_registry, 0001_init.sql) holds the stable-name registry: per name, a +// display name, an ordered candidate chain of {provider, model_id}, and listing +// metadata. The gateway resolver reads it to route a request's modelId +// (compass-stable-name-routing §P1/P2). +// +// UNLIKE agent_config_bundle (a content-hash upsert, current-only), the write +// path is COMPARE-AND-SET on a monotonic whole-registry version: a Put carries +// the version it read and only lands if the row still holds it, so a racing +// operator write is never clobbered (the versioned-CAS discipline the sibling +// gateway_credentials store also uses, compass-server-llm-gateway/design.md: +// 324-329). The version keys the gateway resolver's in-memory ref (P1). This is +// the first CAS-on-version writer in the store, so the discipline is built fresh +// here; ErrVersionConflict (errors.go) is its sentinel. +// +// The payload is validated fail-closed at the RPC boundary via the pure +// ValidateModelRegistry (schema shape + candidate shape); the orphan cross-check +// (a removal that would strand a published profile's models.* reference) needs +// the current config bundle, so it lives on the store methods (PutModelRegistry +// for removals, DeleteModelRegistry for a full clear), not the pure validator. + +// ModelCandidate is one upstream (provider, model_id) in a stable name's ordered +// chain — the pair the gateway resolver tries in order until one has a usable +// credential. Both fields are opaque here (the SDK owns the selector grammar); +// the store validates only that each is non-empty and whitespace-free. +type ModelCandidate struct { + Provider string `json:"provider"` + ModelID string `json:"model_id"` +} + +// ModelMetadata is the listing shape a stable name carries, taken from its +// primary candidate (OQ-2). All fields optional. Money is integer micro-USD, +// never a float (Global Constraints). +type ModelMetadata struct { + ContextWindow int64 `json:"context_window,omitempty"` + InputCostMicroUSD int64 `json:"input_cost_micro_usd,omitempty"` + OutputCostMicroUSD int64 `json:"output_cost_micro_usd,omitempty"` + API string `json:"api,omitempty"` +} + +// ModelRegistryEntry is one stable name's entry: a display name, the ordered +// candidate chain, and listing metadata. +type ModelRegistryEntry struct { + DisplayName string `json:"display_name"` + Candidates []ModelCandidate `json:"candidates"` + Metadata ModelMetadata `json:"metadata"` +} + +// ModelRegistry is the fleet registry payload: the stable-name → entry map. The +// key is the stable name (the modelId the gateway resolver looks up). +type ModelRegistry struct { + Entries map[string]ModelRegistryEntry `json:"entries"` +} + +// Fail-closed size caps on the registry payload (M1). The registry is small +// operator config — a fleet names on the order of dozens of stable names, each +// a short ordered candidate chain — but nothing downstream bounds it: the store +// marshals whatever it is handed into a single JSONB row (PutModelRegistry) and +// GetModelRegistry serves the whole payload to every authenticated account +// (authenticatedOpen), so an unbounded registry is both an unbounded-allocation +// write and a read-amplification vector. These caps mirror the config-bundle +// door's posture (agent_config.go maxDecompressedBytes / maxFileCount): generous +// headroom over any realistic registry while still bounding a single Put's +// memory and the served row. The write is admin-gated, so this is defense in +// depth, not an untrusted-input gate — but a fail-closed bound belongs at the +// door regardless. Each breach is a %w-wrapped ErrInvalidArgument. +const ( + // maxRegistryEntries caps the number of stable-name entries. A fleet's + // stable-name set is operator-curated and small; hundreds is already far + // beyond any realistic roster. + maxRegistryEntries = 512 + // maxCandidatesPerEntry caps one stable name's ordered candidate chain. The + // chain is the fallback list the resolver tries in order; a handful is + // typical, tens is generous. + maxCandidatesPerEntry = 32 + // maxRegistryBytes caps the serialized (json.Marshal) payload size — the + // bytes actually written to the JSONB row and served on Get. 1 MiB is a low- + // MB bound comfortably above the entry/candidate caps' worst case of short + // text fields, while keeping the singleton row and every Get response small. + maxRegistryBytes = 1 << 20 // 1 MiB + // maxRegistryFieldBytes caps a single string field (stable name, display + // name, candidate provider/model_id, metadata api). Checked in the per-entry + // loop BEFORE the aggregate json.Marshal, so no single field forces an + // unbounded transient allocation inside Marshal ahead of the byte cap (L1). + // This composes with the entry/candidate caps to bound the pre-marshal + // content at maxRegistryEntries * (3 + 2*maxCandidatesPerEntry) * + // maxRegistryFieldBytes (~= 35 MB worst case, not near maxRegistryBytes); the + // aggregate maxRegistryBytes check on the marshaled payload is the final, + // tight gate. Generous over any real name. + maxRegistryFieldBytes = 1024 +) + +// ValidateModelRegistry is the pure, DB-free door check the write path runs +// before any row write (RIG-3122 P2, design.md §P2 fail-closed). It enforces the +// registry SCHEMA SHAPE and each candidate's SHAPE; it does NOT do the orphan +// cross-check (that needs the current config bundle and lives on the store +// methods). Cross-family / model-choice judgment stays ADVISORY, not enforced +// (design.md §P2 L533-535). Rejections are %w-wrapped ErrInvalidArgument so the +// RPC edge maps them to CodeInvalidArgument. +// +// - the entry count, per-entry candidate count, and serialized byte size are +// within the fail-closed size caps (M1); +// - every stable-name KEY is non-empty, within the per-field byte cap, and +// contains no '/', ':', or whitespace — the grammar the reverse lint's +// escape-hatch ('/') and split-on-last-colon (':') discriminators depend +// on, rejected at the door that mints the vocabulary (design.md §P2 +// L530-532, L583-585); +// - every entry has a non-empty display_name and at least one candidate; +// - every candidate's provider and model_id are non-empty and whitespace-free +// (no existing model-id grammar in the tree — the SDK owns split-on-last- +// colon — so the store enforces only the minimal non-empty/no-whitespace +// shape, per the brief). +func ValidateModelRegistry(reg ModelRegistry) error { + if len(reg.Entries) > maxRegistryEntries { + return fmt.Errorf("%w: model registry has %d entries, exceeds the cap of %d", ErrInvalidArgument, len(reg.Entries), maxRegistryEntries) + } + for name, entry := range reg.Entries { + if err := validateStableName(name); err != nil { + return err + } + if entry.DisplayName == "" { + return fmt.Errorf("%w: model registry entry %q has an empty display_name", ErrInvalidArgument, name) + } + if len(entry.DisplayName) > maxRegistryFieldBytes { + return fmt.Errorf("%w: model registry entry %q display_name is %d bytes, exceeds the field cap of %d", ErrInvalidArgument, name, len(entry.DisplayName), maxRegistryFieldBytes) + } + if len(entry.Metadata.API) > maxRegistryFieldBytes { + return fmt.Errorf("%w: model registry entry %q metadata api is %d bytes, exceeds the field cap of %d", ErrInvalidArgument, name, len(entry.Metadata.API), maxRegistryFieldBytes) + } + if len(entry.Candidates) == 0 { + return fmt.Errorf("%w: model registry entry %q has no candidates (>=1 required)", ErrInvalidArgument, name) + } + if len(entry.Candidates) > maxCandidatesPerEntry { + return fmt.Errorf("%w: model registry entry %q has %d candidates, exceeds the cap of %d", ErrInvalidArgument, name, len(entry.Candidates), maxCandidatesPerEntry) + } + for i, c := range entry.Candidates { + if err := validateCandidateField(name, i, "provider", c.Provider); err != nil { + return err + } + if err := validateCandidateField(name, i, "model_id", c.ModelID); err != nil { + return err + } + } + } + // Serialized-size cap: bound the bytes actually written to the JSONB row and + // served on Get. Marshalling here mirrors the store's own json.Marshal(reg) + // at the write, so this catches an over-cap payload before any row write. + payload, err := json.Marshal(reg) + if err != nil { + return fmt.Errorf("%w: model registry is not serializable: %w", ErrInvalidArgument, err) + } + if len(payload) > maxRegistryBytes { + return fmt.Errorf("%w: model registry serializes to %d bytes, exceeds the cap of %d", ErrInvalidArgument, len(payload), maxRegistryBytes) + } + return nil +} + +// validateStableName enforces the stable-name KEY grammar the registry's two +// downstream discriminators depend on. The bundle-door reverse lint reads a +// profile selector as an escape-hatch provider/id when it contains "/" +// (checkBundleProfileRefsAgainstRegistry) and strips a trailing reasoning-tier +// by splitting on the LAST ":" (stableNameOfSelector), so a key containing "/" +// is UNREACHABLE (only ever read as an escape hatch) and a key containing ":" +// is UNREACHABLE (the tier split always truncates it). Either breaks the +// design's own discriminators (design.md §P2 L530-532, L583-585), so the door +// that mints the vocabulary rejects both, plus whitespace (matching +// validateCandidateField's candidate-field strictness one field away) and the +// per-field byte cap (L1). +func validateStableName(name string) error { + if name == "" { + return fmt.Errorf("%w: model registry has an empty stable name", ErrInvalidArgument) + } + if len(name) > maxRegistryFieldBytes { + return fmt.Errorf("%w: model registry stable name %q is %d bytes, exceeds the field cap of %d", ErrInvalidArgument, name, len(name), maxRegistryFieldBytes) + } + if strings.ContainsRune(name, '/') { + return fmt.Errorf("%w: model registry stable name %q contains '/' (reserved for the provider/id escape-hatch selector — a slash-bearing name is unreachable)", ErrInvalidArgument, name) + } + if strings.ContainsRune(name, ':') { + return fmt.Errorf("%w: model registry stable name %q contains ':' (reserved for the reasoning-tier suffix — a colon-bearing name is unreachable)", ErrInvalidArgument, name) + } + if strings.ContainsFunc(name, unicode.IsSpace) { + return fmt.Errorf("%w: model registry stable name %q contains whitespace", ErrInvalidArgument, name) + } + return nil +} + +// validateCandidateField enforces the minimal candidate-field shape: non-empty +// and containing no whitespace (a whitespace-bearing provider/model_id is a +// malformed selector, never a real upstream name). Whitespace is the full +// Unicode set (unicode.IsSpace), not just ASCII: a selector bearing exotic +// whitespace (NBSP, vertical tab, U+2028) is as malformed as one with a space, +// and enumerating only the four ASCII runes would fail open on the rest. +func validateCandidateField(name string, idx int, field, value string) error { + if value == "" { + return fmt.Errorf("%w: model registry entry %q candidate %d has an empty %s", ErrInvalidArgument, name, idx, field) + } + if len(value) > maxRegistryFieldBytes { + return fmt.Errorf("%w: model registry entry %q candidate %d %s is %d bytes, exceeds the field cap of %d", ErrInvalidArgument, name, idx, field, len(value), maxRegistryFieldBytes) + } + if strings.ContainsFunc(value, unicode.IsSpace) { + return fmt.Errorf("%w: model registry entry %q candidate %d %s %q contains whitespace", ErrInvalidArgument, name, idx, field, value) + } + return nil +} + +// CurrentModelRegistry returns the current registry and its monotonic version. +// ErrNotFound when no registry has been declared — a valid state downstream (the +// gateway resolver treats an absent registry as an empty one), but the store +// still reports the absence; the caller decides empty-is-ok, mirroring +// CurrentAgentConfig. +func (s *Store) CurrentModelRegistry(ctx context.Context) (version int64, reg ModelRegistry, err error) { + row, err := s.q.CurrentModelRegistry(ctx) + if err != nil { + if noRows(err) { + return 0, ModelRegistry{}, fmt.Errorf("%w: no model registry declared", ErrNotFound) + } + return 0, ModelRegistry{}, fmt.Errorf("store: read model registry: %w", err) + } + if err := json.Unmarshal(row.Registry, ®); err != nil { + return 0, ModelRegistry{}, fmt.Errorf("store: unmarshal model registry: %w", err) + } + return row.Version, reg, nil +} + +// PutModelRegistry declares the fleet model registry under a compare-and-set on +// the whole-registry version, returning the new version. It validates the +// payload shape (ValidateModelRegistry) and, for a write that REMOVES a stable +// name, fails closed if a published profile still references that name (the +// orphan cross-check). actor is the operator-scoped writer (empty → +// ErrInvalidArgument); the singleton has no per-actor column, but the param +// documents the operator-scoped write and matches the record. +// +// CAS semantics keyed on expectedVersion: +// - 0 seeds the FIRST registry — lands only if the singleton is still absent +// (ON CONFLICT DO NOTHING); a racing seed that lost gets ErrVersionConflict. +// - N>0 replaces an existing registry — lands only if the row still holds N, +// bumping to N+1; a stale/racing version gets ErrVersionConflict. +func (s *Store) PutModelRegistry(ctx context.Context, actor AccountID, reg ModelRegistry, expectedVersion int64) (version int64, err error) { + if actor == "" { + return 0, fmt.Errorf("%w: model-registry writer account id is required", ErrInvalidArgument) + } + if expectedVersion < 0 { + return 0, fmt.Errorf("%w: expected_version must be >= 0, got %d", ErrInvalidArgument, expectedVersion) + } + if err := ValidateModelRegistry(reg); err != nil { + return 0, err + } + + // Orphan cross-check for a REPLACE: any stable name present in the prior + // registry but absent from the new one is being removed, and a removal that + // strands a published profile's models.* reference fails closed. A seed + // (expectedVersion 0) has no prior registry, so nothing is removed. + // + // TOCTOU: this cross-check spans TWO stores non-transactionally. It reads the + // config bundle (CurrentAgentConfig, via checkNoOrphanedProfileRefs) here, + // then CAS-writes the registry row below; the version CAS protects only the + // registry row, never the separate config-bundle row. A PutAgentConfig that + // publishes a new profile reference in the window between this read and the + // write is unseen, so this removal can still strand that just-published ref. + // The reverse bundle-door lint (checkBundleProfileRefsAgainstRegistry, called from + // PutAgentConfig) has the symmetric window against a concurrent registry + // write. This is the ACCEPTED, self-healing degradation: both writers are + // admin-gated and low-frequency, a stranded ref surfaces only at gateway + // resolve, and the operator re-runs the write to clear it. We deliberately do + // NOT take a cross-store lock or span both rows in one transaction — a heavier + // guarantee judged unnecessary for admin-gated, low-frequency operator writes. + if expectedVersion > 0 { + priorVersion, prior, rerr := s.CurrentModelRegistry(ctx) + if rerr != nil && !errors.Is(rerr, ErrNotFound) { + return 0, rerr + } + // Only run the removal check against the version the caller intends to + // replace; a divergent version will fail the CAS below regardless. + if rerr == nil && priorVersion == expectedVersion { + if err := s.checkNoOrphanedProfileRefs(ctx, removedNames(prior, reg)); err != nil { + return 0, err + } + } + } + + payload, err := json.Marshal(reg) + if err != nil { + return 0, fmt.Errorf("store: marshal model registry: %w", err) + } + + if expectedVersion == 0 { + version, err = s.q.InsertModelRegistry(ctx, payload) + if err != nil { + if noRows(err) { + // ON CONFLICT DO NOTHING matched an existing row: a registry + // already exists, so a seed (expected 0) is a stale CAS. + return 0, fmt.Errorf("%w: a model registry already exists (seed expected none)", ErrVersionConflict) + } + return 0, fmt.Errorf("store: seed model registry: %w", err) + } + return version, nil + } + + version, err = s.q.UpdateModelRegistry(ctx, db.UpdateModelRegistryParams{ + Registry: payload, + Version: expectedVersion, + }) + if err != nil { + if noRows(err) { + // No row held expectedVersion: either it was already bumped by a + // racing write, or the registry is unconfigured. Either way the + // caller's read is stale — CAS conflict. + return 0, fmt.Errorf("%w: model registry not at version %d", ErrVersionConflict, expectedVersion) + } + return 0, fmt.Errorf("store: update model registry: %w", err) + } + return version, nil +} + +// DeleteModelRegistry clears the fleet model registry, returning to the +// unconfigured state. Fails closed if any stable name in the registry being +// cleared is still referenced by a published profile (clearing it would strand +// that reference). Idempotent: deleting an already-absent registry is a no-op +// success (mirroring DeleteAgentConfig), and there is nothing to orphan. +func (s *Store) DeleteModelRegistry(ctx context.Context) error { + _, reg, err := s.CurrentModelRegistry(ctx) + if err != nil { + if errors.Is(err, ErrNotFound) { + return nil // already unconfigured — idempotent, nothing to orphan + } + return err + } + names := make(map[string]bool, len(reg.Entries)) + for name := range reg.Entries { + names[name] = true + } + if err := s.checkNoOrphanedProfileRefs(ctx, names); err != nil { + return err + } + if err := s.q.DeleteModelRegistry(ctx); err != nil { + return fmt.Errorf("store: delete model registry: %w", err) + } + return nil +} + +// checkBundleProfileRefsAgainstRegistry is the REVERSE orphan guard mandated by +// the frozen design (compass-stable-name-routing/design.md §P2 L530-532): the +// bundle-door profile lint. PutAgentConfig calls it before the bundle row write, +// so a config bundle whose profile pins a stable name absent from the current +// model registry fails closed (ErrInvalidArgument) rather than publishing a +// stranded reference that would fail only at gateway resolve. +// +// Every profile models.* value must be EITHER a known stable name present in the +// current registry OR an explicit escape-hatch selector (a "provider/id" form, +// which always contains a "/"). An escape-hatch selector names no registry entry +// and is accepted unconditionally. A bare selector is stripped to its stable +// name (split-on-last-colon, stableNameOfSelector) and must be a registry key. +// Cross-family / model-choice judgment stays ADVISORY (design.md §P2 L533-535, +// RIG-2936 posture): this lint enforces only known-name presence, never whether +// a chosen model is appropriate. +// +// An unconfigured registry (CurrentModelRegistry ErrNotFound) has NO known +// names, so any bare stable-name reference fails closed — the operator must +// declare the registry before publishing a profile that pins a name from it. +// +// TOCTOU: this reads the registry then the caller CAS-writes the bundle row, +// two stores with no shared transaction — symmetric to the removal-side window +// documented on PutModelRegistry. A concurrent PutModelRegistry removing a name +// in this window is unseen; the accepted, self-healing degradation is the same +// (admin-gated low-frequency writers, a stranded ref surfaces at resolve, the +// operator re-runs). No cross-store lock is taken. +func (s *Store) checkBundleProfileRefsAgainstRegistry(ctx context.Context, bundle []byte) error { + profiles, err := configBundleProfileBodies(bundle) + if err != nil { + return err + } + if len(profiles) == 0 { + return nil // no profiles → no stable-name references to check + } + _, reg, err := s.CurrentModelRegistry(ctx) + if err != nil && !errors.Is(err, ErrNotFound) { + return err + } + // ErrNotFound leaves reg the zero registry (no entries) — every bare + // reference is then unknown and fails closed. + for profileName, body := range profiles { + mapping, err := parseYAMLMapping(body, "profiles/"+profileName+"/"+memberProfileYML) + if err != nil { + // The bundle was shape-validated at the door before this call, so a + // parse failure here is a logic error; fail closed regardless. + return err + } + for _, sel := range profileModelSelectors(mapping) { + if strings.Contains(sel, "/") { + continue // escape-hatch provider/id selector — accepted + } + name := stableNameOfSelector(sel) + if name == "" { + // A selector that strips to an empty stable name (e.g. ":high", a + // bare tier with no name) is malformed, not exempt — fail closed + // rather than admit it (M1). The registry door's own grammar + // (validateStableName) guarantees no stored key is empty. + return fmt.Errorf("%w: profile %q has a malformed model selector %q (empty stable name)", ErrInvalidArgument, profileName, sel) + } + if _, known := reg.Entries[name]; !known { + return fmt.Errorf("%w: profile %q references unknown model stable name %q (declare it in the model registry or use a provider/id escape-hatch selector)", ErrInvalidArgument, profileName, name) + } + } + } + return nil +} + +// removedNames returns the set of stable names present in prior but absent from +// next — the names a Put would REMOVE from the registry. +func removedNames(prior, next ModelRegistry) map[string]bool { + removed := make(map[string]bool) + for name := range prior.Entries { + if _, kept := next.Entries[name]; !kept { + removed[name] = true + } + } + return removed +} + +// checkNoOrphanedProfileRefs fails closed (ErrInvalidArgument) if any name in +// the given set is still referenced by a published profile's models.* map. The +// published profiles live in the current config bundle (agent_config_bundle); +// an unconfigured fleet (no bundle) references nothing. An empty name set is a +// no-op. This is the fail-closed orphan guard of design.md §P2 L528-532: a +// registry removal must not strand a profile that pins a stable name. +func (s *Store) checkNoOrphanedProfileRefs(ctx context.Context, names map[string]bool) error { + if len(names) == 0 { + return nil + } + refs, err := s.publishedProfileModelRefs(ctx) + if err != nil { + return err + } + for name := range names { + if profile, referenced := refs[name]; referenced { + return fmt.Errorf("%w: stable name %q is still referenced by published profile %q (remove the profile reference before removing the registry entry)", ErrInvalidArgument, name, profile) + } + } + return nil +} + +// publishedProfileModelRefs reads the current config bundle and returns the set +// of stable names its profiles reference through models.* selectors, mapped to +// the profile name that references each (for a precise error). Only STABLE-NAME +// references count: an explicit provider/id escape-hatch selector always +// contains a "/" (design.md §P2 L530-532) and names no registry entry, so it is +// skipped. An unconfigured fleet (no bundle) references nothing. +// +// It reuses the same tar-walk + YAML-mapping grammar the config-bundle door +// enforced at Put, so it reads only already-validated, trusted content. +func (s *Store) publishedProfileModelRefs(ctx context.Context) (map[string]string, error) { + _, bundle, err := s.CurrentAgentConfig(ctx) + if err != nil { + if errors.Is(err, ErrNotFound) { + return map[string]string{}, nil // no bundle → no published profiles + } + return nil, err + } + profiles, err := configBundleProfileBodies(bundle) + if err != nil { + return nil, err + } + refs := make(map[string]string) + for profileName, body := range profiles { + mapping, err := parseYAMLMapping(body, "profiles/"+profileName+"/"+memberProfileYML) + if err != nil { + // The bundle was validated at Put, so a parse failure here is a + // logic error; fail closed regardless (never silently skip a + // profile whose refs we could not read). + return nil, err + } + for _, sel := range profileModelSelectors(mapping) { + // An escape-hatch provider/id selector always carries a "/" and + // names no registry entry; only a bare stable name (optionally + // carrying a trailing :tier suffix, split-on-last-colon) is a + // registry reference. + if strings.Contains(sel, "/") { + continue + } + name := stableNameOfSelector(sel) + if name == "" { + // Reads already-stored, door-validated content: the Put-side lint + // (checkBundleProfileRefsAgainstRegistry) fails closed on an + // empty-name selector, so a stored bundle cannot carry one. Skip + // rather than error — this path must not fail on trusted content. + continue + } + if _, seen := refs[name]; !seen { + refs[name] = profileName + } + } + } + return refs, nil +} + +// profileModelSelectors returns every models.* string selector declared in a +// parsed profile mapping: models.manager (when a string) and every string value +// under models.agents. It mirrors validateProfileModelSelectors' traversal (the +// door already proved these are string-shaped), collecting the values rather +// than shape-checking them. A non-string or absent axis contributes nothing. +func profileModelSelectors(mapping map[string]any) []string { + modelsRaw, present := mapping["models"] + if !present || modelsRaw == nil { + return nil + } + models, ok := modelsRaw.(map[string]any) + if !ok { + return nil + } + var out []string + if v, present := models["manager"]; present { + if s, ok := v.(string); ok && s != "" { + out = append(out, s) + } + } + if agentsRaw, present := models["agents"]; present && agentsRaw != nil { + if agents, ok := agentsRaw.(map[string]any); ok { + for _, v := range agents { + if s, ok := v.(string); ok && s != "" { + out = append(out, s) + } + } + } + } + return out +} + +// stableNameOfSelector returns the stable-name portion of a bare (no "/") +// profile selector: the SDK's selector grammar splits a trailing reasoning-tier +// on the LAST colon (e.g. "claude-opus-4-8:high" → name "claude-opus-4-8"), so +// the registry key is the pre-last-colon segment. A selector with no colon is +// its own stable name. This is the ONE place the split-on-last-colon grammar is +// applied on the server, and only to fail CLOSED: matching the stripped name +// against the registry catches an orphaning removal that a literal-only match +// would miss. This split is sound only because a registry stable name may +// contain neither ":" nor "/" (validateStableName enforces that grammar at the +// registry door; design.md §P2 L530-532, L583-585): a ":"-bearing key would be +// unreachable through its own selector, a "/"-bearing key indistinguishable +// from an escape hatch. +func stableNameOfSelector(sel string) string { + if i := strings.LastIndexByte(sel, ':'); i >= 0 { + return sel[:i] + } + return sel +} diff --git a/go/internal/store/model_registry_pgtest_test.go b/go/internal/store/model_registry_pgtest_test.go new file mode 100644 index 000000000..70d2ed895 --- /dev/null +++ b/go/internal/store/model_registry_pgtest_test.go @@ -0,0 +1,385 @@ +//go:build pgtest && unix + +package store + +// Model-registry store contracts (RIG-3122 P2), pgtest gate — the CAS write +// discipline and the fail-closed orphan cross-check against a real Postgres via +// the shared harness (newTestStore/mustUser/mkBundle only exist under this tag). +// The pure payload validation is proven in the default-gate sibling; here we +// prove what only a real DB can: the compare-and-set on the whole-registry +// version (a stale/racing expected_version → ErrVersionConflict; the correct +// version → a bump), the seed-then-replace round trip, ErrNotFound on an +// unconfigured store, and that a removal/clear stranding a published profile +// reference fails closed. Behind `pgtest && unix` (SKIP when no runtime). + +import ( + "context" + "errors" + "testing" +) + +// reg1 builds a single-entry registry keyed on the given stable name. +func reg1(name string) ModelRegistry { + return ModelRegistry{Entries: map[string]ModelRegistryEntry{ + name: { + DisplayName: name, + Candidates: []ModelCandidate{{Provider: "anthropic", ModelID: "claude-" + name}}, + }, + }} +} + +// TestPutModelRegistrySeedThenReadRoundTrip: seeding at expected version 0 lands +// version 1, and CurrentModelRegistry reads back that version and payload. +func TestPutModelRegistrySeedThenReadRoundTrip(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + actor := mustUser(t, s, "operator") + + v, err := s.PutModelRegistry(ctx, actor.ID, reg1("opus"), 0) + if err != nil { + t.Fatalf("seed PutModelRegistry: %v", err) + } + if v != 1 { + t.Fatalf("seed version = %d, want 1", v) + } + gotV, gotReg, err := s.CurrentModelRegistry(ctx) + if err != nil { + t.Fatalf("CurrentModelRegistry: %v", err) + } + if gotV != 1 { + t.Errorf("read version = %d, want 1", gotV) + } + if _, ok := gotReg.Entries["opus"]; !ok { + t.Errorf("read registry missing 'opus' entry: %+v", gotReg) + } +} + +// TestCurrentModelRegistryEmptyNotFound: an unconfigured store reports ErrNotFound. +func TestCurrentModelRegistryEmptyNotFound(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + if _, _, err := s.CurrentModelRegistry(ctx); !errors.Is(err, ErrNotFound) { + t.Fatalf("want ErrNotFound on empty store, got %v", err) + } +} + +// TestPutModelRegistryEmptyActorInvalid: an empty writer id is rejected before +// any row write. +func TestPutModelRegistryEmptyActorInvalid(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + if _, err := s.PutModelRegistry(ctx, "", reg1("opus"), 0); !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("want ErrInvalidArgument for empty actor, got %v", err) + } +} + +// TestPutModelRegistryCASBumpsOnCorrectVersion: a replace at the current version +// lands and bumps the version; the payload is the new one. +func TestPutModelRegistryCASBumpsOnCorrectVersion(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + actor := mustUser(t, s, "operator") + + v1, err := s.PutModelRegistry(ctx, actor.ID, reg1("opus"), 0) + if err != nil { + t.Fatalf("seed: %v", err) + } + v2, err := s.PutModelRegistry(ctx, actor.ID, reg1("sonnet"), v1) + if err != nil { + t.Fatalf("replace at correct version: %v", err) + } + if v2 != v1+1 { + t.Fatalf("replace version = %d, want %d", v2, v1+1) + } + _, gotReg, err := s.CurrentModelRegistry(ctx) + if err != nil { + t.Fatalf("CurrentModelRegistry: %v", err) + } + if _, ok := gotReg.Entries["sonnet"]; !ok { + t.Errorf("current registry did not take the replace: %+v", gotReg) + } +} + +// TestPutModelRegistryStaleVersionConflict is the CAS heart: a write carrying a +// stale expected_version (the value read BEFORE a racing write bumped it) matches +// no row and returns ErrVersionConflict — the racing operator write is never +// clobbered. +func TestPutModelRegistryStaleVersionConflict(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + actor := mustUser(t, s, "operator") + + v1, err := s.PutModelRegistry(ctx, actor.ID, reg1("opus"), 0) + if err != nil { + t.Fatalf("seed: %v", err) + } + // A first operator advances the version. + if _, err := s.PutModelRegistry(ctx, actor.ID, reg1("sonnet"), v1); err != nil { + t.Fatalf("first replace: %v", err) + } + // A second operator still holding the stale v1 tries to write — CAS refuses. + if _, err := s.PutModelRegistry(ctx, actor.ID, reg1("haiku"), v1); !errors.Is(err, ErrVersionConflict) { + t.Fatalf("stale-version write: want ErrVersionConflict, got %v", err) + } + // The losing write left the winner in place. + _, gotReg, err := s.CurrentModelRegistry(ctx) + if err != nil { + t.Fatalf("CurrentModelRegistry: %v", err) + } + if _, ok := gotReg.Entries["sonnet"]; !ok { + t.Errorf("stale write clobbered the winner: %+v", gotReg) + } +} + +// TestPutModelRegistrySeedConflictWhenAlreadyExists: a seed (expected 0) after a +// registry already exists is a stale CAS, not a silent overwrite. +func TestPutModelRegistrySeedConflictWhenAlreadyExists(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + actor := mustUser(t, s, "operator") + + if _, err := s.PutModelRegistry(ctx, actor.ID, reg1("opus"), 0); err != nil { + t.Fatalf("seed: %v", err) + } + if _, err := s.PutModelRegistry(ctx, actor.ID, reg1("sonnet"), 0); !errors.Is(err, ErrVersionConflict) { + t.Fatalf("re-seed over an existing registry: want ErrVersionConflict, got %v", err) + } +} + +// TestPutModelRegistryRemovalOrphaningPublishedProfileFailsClosed: a replace that +// REMOVES a stable name still referenced by a published profile's models.* map +// fails closed (ErrInvalidArgument). The profile lives in the config bundle, so +// a bundle is published first, then a registry holding the referenced name, then +// a replace dropping it is rejected. +func TestPutModelRegistryRemovalOrphaningPublishedProfileFailsClosed(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + actor := mustUser(t, s, "operator") + + // Seed a registry holding "opus" FIRST — the reverse bundle-door lint rejects + // publishing a profile that pins a name absent from the registry. + v1, err := s.PutModelRegistry(ctx, actor.ID, reg1("opus"), 0) + if err != nil { + t.Fatalf("seed: %v", err) + } + // Publish a config bundle whose profile pins the stable name "opus". + if _, err := s.PutAgentConfig(ctx, actor.ID, mkBundle(t, map[string]string{ + "profiles/candidate/profile.yml": "models:\n manager: opus\n", + })); err != nil { + t.Fatalf("PutAgentConfig: %v", err) + } + _, err = s.PutModelRegistry(ctx, actor.ID, reg1("sonnet"), v1) // drops "opus" + if !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("orphaning removal: want ErrInvalidArgument, got %v", err) + } + // A replace that KEEPS "opus" (adds alongside) is allowed. + both := ModelRegistry{Entries: map[string]ModelRegistryEntry{ + "opus": reg1("opus").Entries["opus"], + "sonnet": reg1("sonnet").Entries["sonnet"], + }} + if _, err := s.PutModelRegistry(ctx, actor.ID, both, v1); err != nil { + t.Fatalf("non-orphaning replace rejected: %v", err) + } +} + +// TestDeleteModelRegistryRoundTrip: Delete on a registry not referenced by any +// profile clears it so CurrentModelRegistry reports ErrNotFound. +func TestDeleteModelRegistryRoundTrip(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + actor := mustUser(t, s, "operator") + + if _, err := s.PutModelRegistry(ctx, actor.ID, reg1("opus"), 0); err != nil { + t.Fatalf("seed: %v", err) + } + if err := s.DeleteModelRegistry(ctx); err != nil { + t.Fatalf("DeleteModelRegistry: %v", err) + } + if _, _, err := s.CurrentModelRegistry(ctx); !errors.Is(err, ErrNotFound) { + t.Fatalf("CurrentModelRegistry after delete: want ErrNotFound, got %v", err) + } +} + +// TestDeleteModelRegistryIdempotent: deleting an already-unconfigured registry is +// a no-op success. +func TestDeleteModelRegistryIdempotent(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + if err := s.DeleteModelRegistry(ctx); err != nil { + t.Fatalf("DeleteModelRegistry on empty store: want nil, got %v", err) + } +} + +// TestDeleteModelRegistryOrphaningPublishedProfileFailsClosed: clearing a +// registry whose stable name is still referenced by a published profile fails +// closed (ErrInvalidArgument), leaving the registry in place. +func TestDeleteModelRegistryOrphaningPublishedProfileFailsClosed(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + actor := mustUser(t, s, "operator") + + // Seed the registry FIRST so the reverse bundle-door lint admits the profile. + if _, err := s.PutModelRegistry(ctx, actor.ID, reg1("opus"), 0); err != nil { + t.Fatalf("seed: %v", err) + } + if _, err := s.PutAgentConfig(ctx, actor.ID, mkBundle(t, map[string]string{ + "profiles/candidate/profile.yml": "models:\n manager: opus\n", + })); err != nil { + t.Fatalf("PutAgentConfig: %v", err) + } + if err := s.DeleteModelRegistry(ctx); !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("orphaning delete: want ErrInvalidArgument, got %v", err) + } + if _, _, err := s.CurrentModelRegistry(ctx); err != nil { + t.Fatalf("registry cleared despite orphaning delete: %v", err) + } +} + +// TestPutAgentConfigReverseLintRejectsUnknownStableName is the M2 reverse +// bundle-door lint (design.md §P2 L530-532): publishing a profile that pins a +// bare stable name absent from the current registry fails closed +// (ErrInvalidArgument), rather than stranding a reference that would fail only at +// gateway resolve. Here the registry holds "opus" but the profile pins "sonnet". +func TestPutAgentConfigReverseLintRejectsUnknownStableName(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + actor := mustUser(t, s, "operator") + + if _, err := s.PutModelRegistry(ctx, actor.ID, reg1("opus"), 0); err != nil { + t.Fatalf("seed registry: %v", err) + } + _, err := s.PutAgentConfig(ctx, actor.ID, mkBundle(t, map[string]string{ + "profiles/candidate/profile.yml": "models:\n manager: sonnet\n", + })) + if !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("profile pinning an unknown stable name: want ErrInvalidArgument, got %v", err) + } +} + +// TestPutAgentConfigReverseLintAcceptsEscapeHatchSelector: a profile using an +// explicit provider/id escape-hatch selector (contains "/") names no registry +// entry and is accepted even against an empty/unconfigured registry — the +// escape hatch is the sanctioned way to reference a model outside the registry. +func TestPutAgentConfigReverseLintAcceptsEscapeHatchSelector(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + actor := mustUser(t, s, "operator") + + if _, err := s.PutAgentConfig(ctx, actor.ID, mkBundle(t, map[string]string{ + "profiles/candidate/profile.yml": "models:\n manager: openrouter/anthropic/claude\n", + })); err != nil { + t.Fatalf("escape-hatch selector rejected: %v", err) + } +} + +// TestPutAgentConfigReverseLintFailsClosedOnUnconfiguredRegistry (L3b) exercises +// the load-bearing nil-map path the doc comment calls out: with NO registry ever +// declared, CurrentModelRegistry returns ErrNotFound, reg stays the zero value, +// and every bare stable-name reference misses the empty entry map and rejects. +// TestPutAgentConfigReverseLintRejectsUnknownStableName seeds a registry first, +// so it never reaches this branch; a regression making ErrNotFound fail OPEN +// would stay green without this test. +func TestPutAgentConfigReverseLintFailsClosedOnUnconfiguredRegistry(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + actor := mustUser(t, s, "operator") + + // No PutModelRegistry call at all — the registry is unconfigured. + _, err := s.PutAgentConfig(ctx, actor.ID, mkBundle(t, map[string]string{ + "profiles/candidate/profile.yml": "models:\n manager: opus\n", + })) + if !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("bare stable name against unconfigured registry: want ErrInvalidArgument (fail closed), got %v", err) + } +} + +// TestPutAgentConfigReverseLintAcceptsKnownStableName: a profile pinning a bare +// stable name that IS present in the registry is accepted. +func TestPutAgentConfigReverseLintAcceptsKnownStableName(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + actor := mustUser(t, s, "operator") + + if _, err := s.PutModelRegistry(ctx, actor.ID, reg1("opus"), 0); err != nil { + t.Fatalf("seed registry: %v", err) + } + if _, err := s.PutAgentConfig(ctx, actor.ID, mkBundle(t, map[string]string{ + "profiles/candidate/profile.yml": "models:\n manager: opus\n", + })); err != nil { + t.Fatalf("profile pinning a known stable name rejected: %v", err) + } +} + +// TestPutModelRegistryNegativeVersionInvalid is the L1 guard: an expected_version +// below zero is rejected as ErrInvalidArgument before any row write. +func TestPutModelRegistryNegativeVersionInvalid(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + actor := mustUser(t, s, "operator") + + if _, err := s.PutModelRegistry(ctx, actor.ID, reg1("x"), -1); !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("negative expected_version: want ErrInvalidArgument, got %v", err) + } +} + +// TestPutModelRegistryMetadataRoundTrip is the M3 metadata proto<->store<->JSONB +// round trip and the L4 candidate-order assertion. registryFromProto/ToProto +// hand-map four transposition-prone metadata fields; no other test populates +// Metadata, so a cost-field swap or a dropped field would ship green. Here a +// fully-populated entry with DISTINCT non-zero values for each field is seeded +// and read back, asserting each field equals what was written (a transposition +// reddens because the values differ), and a 2-candidate chain's read-back order +// equals the written order (L4). +func TestPutModelRegistryMetadataRoundTrip(t *testing.T) { + ctx := context.Background() + s := newTestStore(t) + actor := mustUser(t, s, "operator") + + // Distinct non-zero values so a field swap cannot pass silently. + want := ModelRegistryEntry{ + DisplayName: "Claude Opus", + Candidates: []ModelCandidate{ + {Provider: "anthropic", ModelID: "claude-opus-4"}, // primary, order index 0 + {Provider: "openrouter", ModelID: "anthropic/opus"}, // fallback, order index 1 + }, + Metadata: ModelMetadata{ + ContextWindow: 200000, + InputCostMicroUSD: 15, + OutputCostMicroUSD: 75, + API: "anthropic-messages", + }, + } + reg := ModelRegistry{Entries: map[string]ModelRegistryEntry{"opus": want}} + if _, err := s.PutModelRegistry(ctx, actor.ID, reg, 0); err != nil { + t.Fatalf("seed: %v", err) + } + _, got, err := s.CurrentModelRegistry(ctx) + if err != nil { + t.Fatalf("CurrentModelRegistry: %v", err) + } + entry, ok := got.Entries["opus"] + if !ok { + t.Fatalf("read registry missing 'opus': %+v", got) + } + if entry.Metadata.ContextWindow != want.Metadata.ContextWindow { + t.Errorf("context_window = %d, want %d", entry.Metadata.ContextWindow, want.Metadata.ContextWindow) + } + if entry.Metadata.InputCostMicroUSD != want.Metadata.InputCostMicroUSD { + t.Errorf("input_cost_micro_usd = %d, want %d", entry.Metadata.InputCostMicroUSD, want.Metadata.InputCostMicroUSD) + } + if entry.Metadata.OutputCostMicroUSD != want.Metadata.OutputCostMicroUSD { + t.Errorf("output_cost_micro_usd = %d, want %d", entry.Metadata.OutputCostMicroUSD, want.Metadata.OutputCostMicroUSD) + } + if entry.Metadata.API != want.Metadata.API { + t.Errorf("api = %q, want %q", entry.Metadata.API, want.Metadata.API) + } + // L4: candidate order is the resolver's try order — read-back must preserve it. + if len(entry.Candidates) != len(want.Candidates) { + t.Fatalf("candidate count = %d, want %d", len(entry.Candidates), len(want.Candidates)) + } + for i := range want.Candidates { + if entry.Candidates[i] != want.Candidates[i] { + t.Errorf("candidate[%d] = %+v, want %+v (order must be preserved)", i, entry.Candidates[i], want.Candidates[i]) + } + } +} diff --git a/go/internal/store/model_registry_test.go b/go/internal/store/model_registry_test.go new file mode 100644 index 000000000..8e2cc09e7 --- /dev/null +++ b/go/internal/store/model_registry_test.go @@ -0,0 +1,298 @@ +package store + +// Model-registry DOOR contracts (RIG-3122 P2), default gate — pure functions, no +// Postgres. ValidateModelRegistry is the fail-closed write validator (design.md +// §P2 L524-535): it enforces schema shape (every entry has a display_name and +// >=1 candidate) and candidate shape (provider/model_id non-empty, no +// whitespace). The profile-reference helpers (configBundleProfileBodies + +// profileModelSelectors + stableNameOfSelector) are the DB-free half of the +// orphan cross-check, exercised here over in-test bundles; the DB-backed CAS + +// orphan-rejection contracts live in the pgtest-tagged sibling. +import ( + "compress/gzip" + "errors" + "fmt" + "slices" + "strings" + "testing" + "time" +) + +// validRegistry is a minimal well-formed registry: one stable name, one +// candidate, a display name. The shape ValidateModelRegistry must accept. +func validRegistry() ModelRegistry { + return ModelRegistry{Entries: map[string]ModelRegistryEntry{ + "claude-opus": { + DisplayName: "Claude Opus", + Candidates: []ModelCandidate{{Provider: "anthropic", ModelID: "claude-opus-4"}}, + Metadata: ModelMetadata{ContextWindow: 200000, API: "anthropic-messages"}, + }, + }} +} + +// TestValidateModelRegistryAcceptsValid pins the accept side: a well-formed +// registry (and the empty registry — a valid unconfigured/cleared state) pass. +func TestValidateModelRegistryAcceptsValid(t *testing.T) { + if err := ValidateModelRegistry(validRegistry()); err != nil { + t.Fatalf("valid registry rejected: %v", err) + } + if err := ValidateModelRegistry(ModelRegistry{}); err != nil { + t.Fatalf("empty registry rejected: %v", err) + } + // Multiple ordered candidates are the point of the chain — accepted. + multi := ModelRegistry{Entries: map[string]ModelRegistryEntry{ + "opus": {DisplayName: "Opus", Candidates: []ModelCandidate{ + {Provider: "anthropic", ModelID: "claude-opus-4"}, + {Provider: "openrouter", ModelID: "anthropic/claude-opus"}, + }}, + }} + if err := ValidateModelRegistry(multi); err != nil { + t.Fatalf("multi-candidate registry rejected: %v", err) + } +} + +// TestValidateModelRegistryRejectsMalformed is the RED-first schema/candidate +// battery: each case is a single malformed field, and every one must reject with +// ErrInvalidArgument (a bad input reddens). A regression that fails open on any +// one of these turns that case green here. +func TestValidateModelRegistryRejectsMalformed(t *testing.T) { + cases := []struct { + name string + reg ModelRegistry + }{ + {"empty stable name", ModelRegistry{Entries: map[string]ModelRegistryEntry{ + "": {DisplayName: "x", Candidates: []ModelCandidate{{Provider: "a", ModelID: "m"}}}, + }}}, + {"empty display_name", ModelRegistry{Entries: map[string]ModelRegistryEntry{ + "opus": {DisplayName: "", Candidates: []ModelCandidate{{Provider: "a", ModelID: "m"}}}, + }}}, + {"no candidates", ModelRegistry{Entries: map[string]ModelRegistryEntry{ + "opus": {DisplayName: "Opus", Candidates: nil}, + }}}, + {"empty candidate provider", ModelRegistry{Entries: map[string]ModelRegistryEntry{ + "opus": {DisplayName: "Opus", Candidates: []ModelCandidate{{Provider: "", ModelID: "m"}}}, + }}}, + {"empty candidate model_id", ModelRegistry{Entries: map[string]ModelRegistryEntry{ + "opus": {DisplayName: "Opus", Candidates: []ModelCandidate{{Provider: "a", ModelID: ""}}}, + }}}, + {"whitespace in provider", ModelRegistry{Entries: map[string]ModelRegistryEntry{ + "opus": {DisplayName: "Opus", Candidates: []ModelCandidate{{Provider: "an thropic", ModelID: "m"}}}, + }}}, + {"whitespace in model_id", ModelRegistry{Entries: map[string]ModelRegistryEntry{ + "opus": {DisplayName: "Opus", Candidates: []ModelCandidate{{Provider: "a", ModelID: "claude opus"}}}, + }}}, + {"newline in model_id", ModelRegistry{Entries: map[string]ModelRegistryEntry{ + "opus": {DisplayName: "Opus", Candidates: []ModelCandidate{{Provider: "a", ModelID: "claude\nopus"}}}, + }}}, + // L2: a non-ASCII whitespace rune (U+00A0 NBSP) must reject too — the + // four-ASCII-rune predicate would have failed open on it. + {"non-breaking space in model_id", ModelRegistry{Entries: map[string]ModelRegistryEntry{ + "opus": {DisplayName: "Opus", Candidates: []ModelCandidate{{Provider: "a", ModelID: "claude\u00a0opus"}}}, + }}}, + // M1: the stable-name KEY grammar the reverse lint's discriminators + // depend on — a '/' key is indistinguishable from an escape hatch, a + // ':' key is unreachable through the tier split, whitespace matches the + // candidate-field strictness one field away. Each must reject at the door. + {"slash in stable name", ModelRegistry{Entries: map[string]ModelRegistryEntry{ + "anthropic/claude": {DisplayName: "x", Candidates: []ModelCandidate{{Provider: "a", ModelID: "m"}}}, + }}}, + {"colon in stable name", ModelRegistry{Entries: map[string]ModelRegistryEntry{ + "gpt-5:turbo": {DisplayName: "x", Candidates: []ModelCandidate{{Provider: "a", ModelID: "m"}}}, + }}}, + {"whitespace in stable name", ModelRegistry{Entries: map[string]ModelRegistryEntry{ + "opus high": {DisplayName: "x", Candidates: []ModelCandidate{{Provider: "a", ModelID: "m"}}}, + }}}, + {"newline in stable name", ModelRegistry{Entries: map[string]ModelRegistryEntry{ + " opus\n": {DisplayName: "x", Candidates: []ModelCandidate{{Provider: "a", ModelID: "m"}}}, + }}}, + // L1: an oversized single string field rejects before the aggregate cap. + {"oversized display_name", ModelRegistry{Entries: map[string]ModelRegistryEntry{ + "opus": {DisplayName: strings.Repeat("x", maxRegistryFieldBytes+1), Candidates: []ModelCandidate{{Provider: "a", ModelID: "m"}}}, + }}}, + {"oversized candidate model_id", ModelRegistry{Entries: map[string]ModelRegistryEntry{ + "opus": {DisplayName: "Opus", Candidates: []ModelCandidate{{Provider: "a", ModelID: strings.Repeat("m", maxRegistryFieldBytes+1)}}}, + }}}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := ValidateModelRegistry(tc.reg) + if !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("want ErrInvalidArgument for %s, got %v", tc.name, err) + } + }) + } +} + +// TestValidateModelRegistryRejectsOverCap is the M1 RED battery: an over-cap +// entry count, an over-cap per-entry candidate count, and an over-cap serialized +// byte size each reject with ErrInvalidArgument. A regression dropping any cap +// turns its case green here. The caps are fail-closed size bounds on the payload +// the store marshals into a JSONB row and serves to every authenticated account. +func TestValidateModelRegistryRejectsOverCap(t *testing.T) { + t.Run("over entry count cap", func(t *testing.T) { + entries := make(map[string]ModelRegistryEntry, maxRegistryEntries+1) + for i := range maxRegistryEntries + 1 { + name := fmt.Sprintf("name-%d", i) + entries[name] = ModelRegistryEntry{ + DisplayName: name, + Candidates: []ModelCandidate{{Provider: "anthropic", ModelID: "claude-" + name}}, + } + } + if err := ValidateModelRegistry(ModelRegistry{Entries: entries}); !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("want ErrInvalidArgument over entry cap, got %v", err) + } + }) + + t.Run("over per-entry candidate cap", func(t *testing.T) { + cands := make([]ModelCandidate, maxCandidatesPerEntry+1) + for i := range cands { + cands[i] = ModelCandidate{Provider: "anthropic", ModelID: fmt.Sprintf("claude-%d", i)} + } + reg := ModelRegistry{Entries: map[string]ModelRegistryEntry{ + "opus": {DisplayName: "Opus", Candidates: cands}, + }} + if err := ValidateModelRegistry(reg); !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("want ErrInvalidArgument over candidate cap, got %v", err) + } + }) + + t.Run("over serialized byte cap", func(t *testing.T) { + // One entry within the entry/candidate caps but whose display_name alone + // pushes the serialized payload past maxRegistryBytes. + big := strings.Repeat("x", maxRegistryBytes+1) + reg := ModelRegistry{Entries: map[string]ModelRegistryEntry{ + "opus": {DisplayName: big, Candidates: []ModelCandidate{{Provider: "anthropic", ModelID: "claude-opus"}}}, + }} + if err := ValidateModelRegistry(reg); !errors.Is(err, ErrInvalidArgument) { + t.Fatalf("want ErrInvalidArgument over byte cap, got %v", err) + } + }) + + t.Run("at entry cap accepted", func(t *testing.T) { + entries := make(map[string]ModelRegistryEntry, maxRegistryEntries) + for i := range maxRegistryEntries { + name := fmt.Sprintf("name-%d", i) + entries[name] = ModelRegistryEntry{ + DisplayName: name, + Candidates: []ModelCandidate{{Provider: "anthropic", ModelID: "claude-" + name}}, + } + } + if err := ValidateModelRegistry(ModelRegistry{Entries: entries}); err != nil { + t.Fatalf("registry at the entry cap rejected: %v", err) + } + }) +} + +// TestStableNameOfSelector pins the split-on-last-colon grammar the orphan check +// applies to a bare profile selector: a trailing reasoning tier is stripped so +// the registry key is the pre-last-colon segment; a colon-free selector is its +// own name. +func TestStableNameOfSelector(t *testing.T) { + cases := map[string]string{ + "claude-opus": "claude-opus", + "claude-opus-4-8:high": "claude-opus-4-8", + "name:with:two:colons": "name:with:two", + "trailingcolon:": "trailingcolon", + } + for sel, want := range cases { + if got := stableNameOfSelector(sel); got != want { + t.Errorf("stableNameOfSelector(%q) = %q, want %q", sel, got, want) + } + } +} + +// TestValidateStableNameGrammar pins the M1 key grammar directly: the P5 day-1 +// stable names (hyphenated, no '/' or ':') pass, and each of the three +// discriminator-breaking shapes the reverse lint depends on ('/', ':', +// whitespace) rejects with ErrInvalidArgument. The '/' and ':' rejections are +// what make stableNameOfSelector's split and the escape-hatch discriminator +// total over any stored key (design.md §P2 L530-532, L583-585). +func TestValidateStableNameGrammar(t *testing.T) { + for _, ok := range []string{"claude-opus-4-8", "gpt-5-5", "gemini-3-1-pro"} { + if err := validateStableName(ok); err != nil { + t.Errorf("validateStableName(%q) rejected a valid P5 name: %v", ok, err) + } + } + for _, bad := range []string{"", "anthropic/claude", "gpt-5:turbo", "opus high", " opus\n", strings.Repeat("x", maxRegistryFieldBytes+1)} { + if err := validateStableName(bad); !errors.Is(err, ErrInvalidArgument) { + t.Errorf("validateStableName(%q) = %v, want ErrInvalidArgument", bad, err) + } + } +} + +// TestProfileModelSelectors pins selector collection from a parsed profile: the +// manager selector and every models.agents value are returned; absent/non-string +// axes contribute nothing. +func TestProfileModelSelectors(t *testing.T) { + mapping := map[string]any{ + "models": map[string]any{ + "manager": "claude-opus:high", + "agents": map[string]any{ + "impl": "claude-sonnet", + "review": "gpt-5", + }, + }, + } + got := profileModelSelectors(mapping) + slices.Sort(got) + want := []string{"claude-opus:high", "claude-sonnet", "gpt-5"} + if !slices.Equal(got, want) { + t.Fatalf("profileModelSelectors = %v, want %v", got, want) + } + // No models axis → no selectors. + if s := profileModelSelectors(map[string]any{"corpus": map[string]any{}}); len(s) != 0 { + t.Fatalf("profileModelSelectors with no models axis = %v, want empty", s) + } +} + +// TestConfigBundleProfileModelRefsExtraction proves the DB-free half of the +// orphan check end-to-end over a real bundle: a bundle whose profile references +// a bare stable name (`claude-opus`) surfaces that name, keyed to the profile, +// while an explicit provider/id escape-hatch selector (contains "/") is NOT a +// registry reference and is skipped. This is the exact discrimination the +// fail-closed delete/removal guard relies on (design.md §P2 L530-532). +func TestConfigBundleProfileModelRefsExtraction(t *testing.T) { + // A profile pinning a bare stable name for the manager and an escape-hatch + // provider/id selector for an agent. + bundle := buildBundle(t, gzip.DefaultCompression, time.Unix(1000, 0), + tarEntry{name: "agents/impl.md", content: "---\nname: impl\n---\nx"}, + tarEntry{name: "profiles/candidate/profile.yml", content: "models:\n manager: claude-opus:high\n agents:\n impl: openrouter/anthropic/claude\n"}, + ) + bodies, err := configBundleProfileBodies(bundle) + if err != nil { + t.Fatalf("configBundleProfileBodies: %v", err) + } + body, ok := bodies["candidate"] + if !ok { + t.Fatalf("profile 'candidate' not found in %v", bodies) + } + mapping, err := parseYAMLMapping(body, "profiles/candidate/profile.yml") + if err != nil { + t.Fatalf("parse profile: %v", err) + } + var bareRefs, escapeRefs []string + for _, sel := range profileModelSelectors(mapping) { + if selectorHasSlash(sel) { + escapeRefs = append(escapeRefs, sel) + } else { + bareRefs = append(bareRefs, stableNameOfSelector(sel)) + } + } + if !slices.Equal(bareRefs, []string{"claude-opus"}) { + t.Fatalf("bare stable-name refs = %v, want [claude-opus]", bareRefs) + } + if !slices.Equal(escapeRefs, []string{"openrouter/anthropic/claude"}) { + t.Fatalf("escape-hatch refs = %v, want the provider/id selector (skipped by the orphan check)", escapeRefs) + } +} + +// selectorHasSlash mirrors the orphan check's escape-hatch discriminator, kept +// local to the test so a change to the production predicate is caught here. +func selectorHasSlash(sel string) bool { + for _, r := range sel { + if r == '/' { + return true + } + } + return false +} diff --git a/go/internal/store/queries/model_registry.sql b/go/internal/store/queries/model_registry.sql new file mode 100644 index 000000000..18dec1bfa --- /dev/null +++ b/go/internal/store/queries/model_registry.sql @@ -0,0 +1,33 @@ +-- Model-registry queries (RIG-3122 P2). Back the hand-written Store methods in +-- internal/store/model_registry.go, which own the fail-closed payload +-- validation (ValidateModelRegistry), the JSONB marshal/unmarshal, and the +-- ErrVersionConflict/ErrNotFound mapping. The registry is a fleet-wide singleton +-- row (singleton = TRUE) with a monotonic version supplying the CAS substrate: +-- a write only lands if the row still holds the version the caller read. + +-- name: CurrentModelRegistry :one +SELECT version, registry FROM model_registry WHERE singleton = TRUE; + +-- InsertModelRegistry seeds the FIRST registry (the caller read no row, expected +-- version 0). ON CONFLICT DO NOTHING makes it a CAS: it lands only when the +-- singleton is still absent, so a racing seed loses (zero rows, ErrNoRows via +-- RETURNING) rather than clobbering the winner. The seeded version is 1. +-- name: InsertModelRegistry :one +INSERT INTO model_registry (singleton, version, registry) +VALUES (TRUE, 1, $1) +ON CONFLICT (singleton) DO NOTHING +RETURNING version; + +-- UpdateModelRegistry is the compare-and-set write over an existing row: it +-- lands only when the row still holds $2 (the version the caller read), bumping +-- to version + 1 and returning the new version. A stale/racing expected version +-- matches no row (ErrNoRows via RETURNING) — the caller maps that to +-- ErrVersionConflict. +-- name: UpdateModelRegistry :one +UPDATE model_registry + SET registry = $1, version = version + 1, updated_at = now() + WHERE singleton = TRUE AND version = $2 +RETURNING version; + +-- name: DeleteModelRegistry :exec +DELETE FROM model_registry WHERE singleton = TRUE; diff --git a/go/server/model_registry_service.go b/go/server/model_registry_service.go new file mode 100644 index 000000000..8d2cd69a8 --- /dev/null +++ b/go/server/model_registry_service.go @@ -0,0 +1,156 @@ +//go:build unix + +// The fleet model-registry handlers — the operator-facing write path on +// CompassService (RIG-3122 P2). PutModelRegistry / DeleteModelRegistry are +// admin-gated on the network door (admin_gate.go classifies them adminOnly: +// operator-scoped only, agents never author); GetModelRegistry is value-free of +// credentials (the registry names providers/models, never holds keys) and +// classified authenticatedOpen, mirroring GetAgentConfigInfo. They sit on the +// same service struct as the rest of CompassService (service.go). +// +// The write is a COMPARE-AND-SET on the whole-registry version: the caller +// carries the version it read (0 to seed), the store bumps only if the row still +// holds it, and a stale version maps to CodeAborted — the connect/gRPC +// convention for a failed CAS ("re-read and retry"), never CodeInvalidArgument. +// A malformed payload or an orphaning removal is CodeInvalidArgument (fail +// closed at the door). No version signal is wired here: the gateway read surface +// is a later PR (deliverable 4), so there is no live consumer to prod yet. +package server + +import ( + "context" + "errors" + + "connectrpc.com/connect" + + compassv1 "github.com/RigelBuild/compass/go/gen/compass/v1" + "github.com/RigelBuild/compass/go/internal/store" +) + +// PutModelRegistry declares the fleet model registry under a compare-and-set on +// the version the caller read. Admin-gated on the network door (adminOnly); the +// authenticated caller is the operator-scoped writer the store records. A +// payload that fails validation (empty display_name, no candidates, malformed +// candidate) or a removal that would orphan a published profile reference is +// CodeInvalidArgument; a stale expected_version is CodeAborted (a racing +// operator write landed first). +func (s *service) PutModelRegistry( + ctx context.Context, + req *connect.Request[compassv1.PutModelRegistryRequest], +) (*connect.Response[compassv1.PutModelRegistryResponse], error) { + caller, err := s.requireCaller(ctx) + if err != nil { + return nil, err + } + version, err := s.store.PutModelRegistry(ctx, caller, registryFromProto(req.Msg.GetRegistry()), req.Msg.GetExpectedVersion()) + if err != nil { + return nil, mapModelRegistryErr(err) + } + return connect.NewResponse(&compassv1.PutModelRegistryResponse{Version: version}), nil +} + +// GetModelRegistry reports the current registry version and payload. An +// unconfigured fleet (store ErrNotFound) is a valid state, NOT an error: it +// returns an empty-but-valid response (version 0, empty registry), the same +// value-free posture GetAgentConfigInfo takes on an unconfigured fleet. +func (s *service) GetModelRegistry( + ctx context.Context, + _ *connect.Request[compassv1.GetModelRegistryRequest], +) (*connect.Response[compassv1.GetModelRegistryResponse], error) { + version, reg, err := s.store.CurrentModelRegistry(ctx) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + return connect.NewResponse(&compassv1.GetModelRegistryResponse{ + Registry: registryToProto(store.ModelRegistry{}), + }), nil + } + return nil, connect.NewError(connect.CodeInternal, err) + } + return connect.NewResponse(&compassv1.GetModelRegistryResponse{ + Version: version, + Registry: registryToProto(reg), + }), nil +} + +// DeleteModelRegistry clears the fleet model registry back to unconfigured. +// Admin-gated (adminOnly). Fails closed (CodeInvalidArgument) if the registry +// being cleared holds a stable name still referenced by a published profile. +// Idempotent at the store: clearing an already-empty registry succeeds. +func (s *service) DeleteModelRegistry( + ctx context.Context, + _ *connect.Request[compassv1.DeleteModelRegistryRequest], +) (*connect.Response[compassv1.DeleteModelRegistryResponse], error) { + if err := s.store.DeleteModelRegistry(ctx); err != nil { + return nil, mapModelRegistryErr(err) + } + return connect.NewResponse(&compassv1.DeleteModelRegistryResponse{}), nil +} + +// mapModelRegistryErr maps a store error to its connect status code: a malformed +// payload or an orphaning removal (store.ErrInvalidArgument) is +// CodeInvalidArgument; a failed CAS (store.ErrVersionConflict) is CodeAborted +// (the connect/gRPC convention for compare-and-set contention — the caller +// re-reads and retries); anything else is CodeInternal. +func mapModelRegistryErr(err error) error { + switch { + case errors.Is(err, store.ErrInvalidArgument): + return connect.NewError(connect.CodeInvalidArgument, err) + case errors.Is(err, store.ErrVersionConflict): + return connect.NewError(connect.CodeAborted, err) + default: + return connect.NewError(connect.CodeInternal, err) + } +} + +// registryFromProto converts the wire registry into the store domain type. A nil +// message (an omitted registry field) is the empty registry — a valid input the +// door then validates (an empty registry has no entries to fail schema shape). +func registryFromProto(msg *compassv1.ModelRegistry) store.ModelRegistry { + entries := make(map[string]store.ModelRegistryEntry, len(msg.GetEntries())) + for name, e := range msg.GetEntries() { + candidates := make([]store.ModelCandidate, 0, len(e.GetCandidates())) + for _, c := range e.GetCandidates() { + candidates = append(candidates, store.ModelCandidate{ + Provider: c.GetProvider(), + ModelID: c.GetModelId(), + }) + } + entries[name] = store.ModelRegistryEntry{ + DisplayName: e.GetDisplayName(), + Candidates: candidates, + Metadata: store.ModelMetadata{ + ContextWindow: e.GetMetadata().GetContextWindow(), + InputCostMicroUSD: e.GetMetadata().GetInputCostMicroUsd(), + OutputCostMicroUSD: e.GetMetadata().GetOutputCostMicroUsd(), + API: e.GetMetadata().GetApi(), + }, + } + } + return store.ModelRegistry{Entries: entries} +} + +// registryToProto converts the store domain registry into the wire message. Each +// entry carries a Metadata message (never nil) so a round-trip is shape-stable. +func registryToProto(reg store.ModelRegistry) *compassv1.ModelRegistry { + entries := make(map[string]*compassv1.ModelRegistryEntry, len(reg.Entries)) + for name, e := range reg.Entries { + candidates := make([]*compassv1.ModelCandidate, 0, len(e.Candidates)) + for _, c := range e.Candidates { + candidates = append(candidates, &compassv1.ModelCandidate{ + Provider: c.Provider, + ModelId: c.ModelID, + }) + } + entries[name] = &compassv1.ModelRegistryEntry{ + DisplayName: e.DisplayName, + Candidates: candidates, + Metadata: &compassv1.ModelMetadata{ + ContextWindow: e.Metadata.ContextWindow, + InputCostMicroUsd: e.Metadata.InputCostMicroUSD, + OutputCostMicroUsd: e.Metadata.OutputCostMicroUSD, + Api: e.Metadata.API, + }, + } + } + return &compassv1.ModelRegistry{Entries: entries} +} diff --git a/go/server/model_registry_service_pgtest_test.go b/go/server/model_registry_service_pgtest_test.go new file mode 100644 index 000000000..54fc5335f --- /dev/null +++ b/go/server/model_registry_service_pgtest_test.go @@ -0,0 +1,350 @@ +//go:build pgtest && unix + +package server + +// Store-gated CompassService model-registry handler contracts (RIG-3122 P2): +// PutModelRegistry persists under a compare-and-set and returns the new version; +// GetModelRegistry returns an empty-but-valid response on an unconfigured fleet +// and the payload on a configured one; a malformed payload is CodeInvalidArgument; +// an orphaning removal is CodeInvalidArgument; a stale expected_version is +// CodeAborted; and the two write RPCs are admin-gated on the network door. They +// need a real Postgres because the writes persist the singleton row and the +// handler reads a genuine caller identity. The admin-gate denial runs through the +// full network-door chain (networkDoorHandler); the handler-contract cases run +// through the bearer-only fixture. Behind `pgtest && unix` (SKIP when no runtime). + +import ( + "context" + "testing" + + "connectrpc.com/connect" + + "github.com/RigelBuild/compass/go/events" + compassv1 "github.com/RigelBuild/compass/go/gen/compass/v1" + "github.com/RigelBuild/compass/go/internal/auth" +) + +// wireReg1 builds a single-entry wire registry keyed on the given stable name. +func wireReg1(name string) *compassv1.ModelRegistry { + return &compassv1.ModelRegistry{Entries: map[string]*compassv1.ModelRegistryEntry{ + name: { + DisplayName: name, + Candidates: []*compassv1.ModelCandidate{{Provider: "anthropic", ModelId: "claude-" + name}}, + }, + }} +} + +// TestPutModelRegistryPersistsAndReadsBack: a valid seed returns version 1 and +// GetModelRegistry reads that version and payload back. +func TestPutModelRegistryPersistsAndReadsBack(t *testing.T) { + f := newConfigFixture(t) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + putResp, err := f.client.PutModelRegistry(ctx, authReq(f, &compassv1.PutModelRegistryRequest{ + Registry: wireReg1("opus"), ExpectedVersion: 0, + })) + if err != nil { + t.Fatalf("PutModelRegistry: %v", err) + } + if putResp.Msg.GetVersion() != 1 { + t.Fatalf("seed version = %d, want 1", putResp.Msg.GetVersion()) + } + getResp, err := f.client.GetModelRegistry(ctx, authReq(f, &compassv1.GetModelRegistryRequest{})) + if err != nil { + t.Fatalf("GetModelRegistry: %v", err) + } + if getResp.Msg.GetVersion() != 1 { + t.Errorf("read version = %d, want 1", getResp.Msg.GetVersion()) + } + if _, ok := getResp.Msg.GetRegistry().GetEntries()["opus"]; !ok { + t.Errorf("read registry missing 'opus': %+v", getResp.Msg.GetRegistry()) + } +} + +// TestGetModelRegistryUnconfiguredIsEmpty: an unconfigured fleet reports version +// 0 and an empty registry, never an error. +func TestGetModelRegistryUnconfiguredIsEmpty(t *testing.T) { + f := newConfigFixture(t) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + resp, err := f.client.GetModelRegistry(ctx, authReq(f, &compassv1.GetModelRegistryRequest{})) + if err != nil { + t.Fatalf("GetModelRegistry on unconfigured fleet: %v", err) + } + if resp.Msg.GetVersion() != 0 { + t.Errorf("unconfigured version = %d, want 0", resp.Msg.GetVersion()) + } + if n := len(resp.Msg.GetRegistry().GetEntries()); n != 0 { + t.Errorf("unconfigured registry has %d entries, want 0", n) + } +} + +// TestPutModelRegistryVersionBumpObservableViaGet: a replace at the current +// version bumps it, and the new version + payload are observable via Get. +func TestPutModelRegistryVersionBumpObservableViaGet(t *testing.T) { + f := newConfigFixture(t) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + v1, err := f.client.PutModelRegistry(ctx, authReq(f, &compassv1.PutModelRegistryRequest{ + Registry: wireReg1("opus"), ExpectedVersion: 0, + })) + if err != nil { + t.Fatalf("seed: %v", err) + } + v2, err := f.client.PutModelRegistry(ctx, authReq(f, &compassv1.PutModelRegistryRequest{ + Registry: wireReg1("sonnet"), ExpectedVersion: v1.Msg.GetVersion(), + })) + if err != nil { + t.Fatalf("replace: %v", err) + } + if v2.Msg.GetVersion() != v1.Msg.GetVersion()+1 { + t.Fatalf("replace version = %d, want %d", v2.Msg.GetVersion(), v1.Msg.GetVersion()+1) + } + getResp, err := f.client.GetModelRegistry(ctx, authReq(f, &compassv1.GetModelRegistryRequest{})) + if err != nil { + t.Fatalf("GetModelRegistry: %v", err) + } + if getResp.Msg.GetVersion() != v2.Msg.GetVersion() { + t.Errorf("Get version = %d, want %d", getResp.Msg.GetVersion(), v2.Msg.GetVersion()) + } +} + +// TestPutModelRegistryStaleVersionIsAborted: a write carrying a stale +// expected_version is CodeAborted — the connect/gRPC failed-CAS convention. +func TestPutModelRegistryStaleVersionIsAborted(t *testing.T) { + f := newConfigFixture(t) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + v1, err := f.client.PutModelRegistry(ctx, authReq(f, &compassv1.PutModelRegistryRequest{ + Registry: wireReg1("opus"), ExpectedVersion: 0, + })) + if err != nil { + t.Fatalf("seed: %v", err) + } + if _, err := f.client.PutModelRegistry(ctx, authReq(f, &compassv1.PutModelRegistryRequest{ + Registry: wireReg1("sonnet"), ExpectedVersion: v1.Msg.GetVersion(), + })); err != nil { + t.Fatalf("first replace: %v", err) + } + // Reuse the now-stale v1 version. + _, err = f.client.PutModelRegistry(ctx, authReq(f, &compassv1.PutModelRegistryRequest{ + Registry: wireReg1("haiku"), ExpectedVersion: v1.Msg.GetVersion(), + })) + if connect.CodeOf(err) != connect.CodeAborted { + t.Fatalf("stale version = %v, want CodeAborted", connect.CodeOf(err)) + } +} + +// TestPutModelRegistryMalformedIsInvalidArgument: a payload that fails the door +// (an entry with no candidates) is CodeInvalidArgument. +func TestPutModelRegistryMalformedIsInvalidArgument(t *testing.T) { + f := newConfigFixture(t) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + bad := &compassv1.ModelRegistry{Entries: map[string]*compassv1.ModelRegistryEntry{ + "opus": {DisplayName: "Opus", Candidates: nil}, // no candidates + }} + _, err := f.client.PutModelRegistry(ctx, authReq(f, &compassv1.PutModelRegistryRequest{ + Registry: bad, ExpectedVersion: 0, + })) + if connect.CodeOf(err) != connect.CodeInvalidArgument { + t.Fatalf("malformed payload = %v, want CodeInvalidArgument", connect.CodeOf(err)) + } +} + +// TestPutModelRegistryOrphaningRemovalIsInvalidArgument: a replace dropping a +// stable name still referenced by a published profile is CodeInvalidArgument. +func TestPutModelRegistryOrphaningRemovalIsInvalidArgument(t *testing.T) { + f := newConfigFixture(t) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + // Seed a registry holding "opus" FIRST — the reverse bundle-door lint rejects + // publishing a profile that pins a name absent from the registry. + v1, err := f.client.PutModelRegistry(ctx, authReq(f, &compassv1.PutModelRegistryRequest{ + Registry: wireReg1("opus"), ExpectedVersion: 0, + })) + if err != nil { + t.Fatalf("seed: %v", err) + } + // Publish a config bundle whose profile pins "opus". + if _, err := f.client.PutAgentConfig(ctx, authReq(f, &compassv1.PutAgentConfigRequest{ + Bundle: mkConfigBundle(t, map[string]string{ + "profiles/candidate/profile.yml": "models:\n manager: opus\n", + }), + })); err != nil { + t.Fatalf("PutAgentConfig: %v", err) + } + _, err = f.client.PutModelRegistry(ctx, authReq(f, &compassv1.PutModelRegistryRequest{ + Registry: wireReg1("sonnet"), ExpectedVersion: v1.Msg.GetVersion(), // drops "opus" + })) + if connect.CodeOf(err) != connect.CodeInvalidArgument { + t.Fatalf("orphaning removal = %v, want CodeInvalidArgument", connect.CodeOf(err)) + } +} + +// TestModelRegistryWriteRPCsDenyNonAdminToken pins the network-door admin gate: +// a valid NON-admin (member) bearer is denied CodePermissionDenied on both +// PutModelRegistry and DeleteModelRegistry, while GetModelRegistry (open) is +// admitted. The gate short-circuits BEFORE the handler, so it uses the full +// network-door chain via networkDoorHandler. Reddens if the gate ever stops +// classifying either write adminOnly, or classifies Get anything but open. +func TestModelRegistryWriteRPCsDenyNonAdminToken(t *testing.T) { + ctx := context.Background() + st, admin, member := newNetworkStore(t) + memberTok, err := auth.IssueAccountToken(ctx, st, member) + if err != nil { + t.Fatalf("IssueAccountToken(member): %v", err) + } + + bus := events.NewBus[busPayload]() + t.Cleanup(bus.Close) + svc := newService("rig3122-denial-test", bus, st, nil, nil, nil, nil) + client := networkDoorHandler(t, svc, st, admin) + + t.Run("PutModelRegistry as non-admin is PermissionDenied", func(t *testing.T) { + req := connect.NewRequest(&compassv1.PutModelRegistryRequest{Registry: wireReg1("opus")}) + req.Header().Set("Authorization", "Bearer "+memberTok) + cctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + if _, err := client.PutModelRegistry(cctx, req); connect.CodeOf(err) != connect.CodePermissionDenied { + t.Fatalf("non-admin PutModelRegistry = %v, want CodePermissionDenied", connect.CodeOf(err)) + } + }) + + t.Run("DeleteModelRegistry as non-admin is PermissionDenied", func(t *testing.T) { + req := connect.NewRequest(&compassv1.DeleteModelRegistryRequest{}) + req.Header().Set("Authorization", "Bearer "+memberTok) + cctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + if _, err := client.DeleteModelRegistry(cctx, req); connect.CodeOf(err) != connect.CodePermissionDenied { + t.Fatalf("non-admin DeleteModelRegistry = %v, want CodePermissionDenied", connect.CodeOf(err)) + } + }) + + t.Run("GetModelRegistry as non-admin is admitted (open)", func(t *testing.T) { + req := connect.NewRequest(&compassv1.GetModelRegistryRequest{}) + req.Header().Set("Authorization", "Bearer "+memberTok) + cctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + if _, err := client.GetModelRegistry(cctx, req); err != nil { + t.Fatalf("non-admin GetModelRegistry = %v, want admitted", err) + } + }) +} + +// TestPutModelRegistryMetadataRoundTripViaRPC is the M3 assertion at the wire +// boundary: it drives a fully-populated Metadata through registryFromProto (Put) +// and registryToProto (Get) — the four hand-mapped, transposition-prone fields — +// with DISTINCT non-zero values so a swap of the two cost fields, or a dropped +// field, reddens. It also pins the 2-candidate chain's read-back order (L4). +func TestPutModelRegistryMetadataRoundTripViaRPC(t *testing.T) { + f := newConfigFixture(t) + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + want := &compassv1.ModelRegistryEntry{ + DisplayName: "Claude Opus", + Candidates: []*compassv1.ModelCandidate{ + {Provider: "anthropic", ModelId: "claude-opus-4"}, // order index 0 + {Provider: "openrouter", ModelId: "anthropic/opus"}, // order index 1 + }, + Metadata: &compassv1.ModelMetadata{ + ContextWindow: 200000, + InputCostMicroUsd: 15, + OutputCostMicroUsd: 75, + Api: "anthropic-messages", + }, + } + reg := &compassv1.ModelRegistry{Entries: map[string]*compassv1.ModelRegistryEntry{"opus": want}} + if _, err := f.client.PutModelRegistry(ctx, authReq(f, &compassv1.PutModelRegistryRequest{ + Registry: reg, ExpectedVersion: 0, + })); err != nil { + t.Fatalf("PutModelRegistry: %v", err) + } + getResp, err := f.client.GetModelRegistry(ctx, authReq(f, &compassv1.GetModelRegistryRequest{})) + if err != nil { + t.Fatalf("GetModelRegistry: %v", err) + } + got, ok := getResp.Msg.GetRegistry().GetEntries()["opus"] + if !ok { + t.Fatalf("read registry missing 'opus': %+v", getResp.Msg.GetRegistry()) + } + md := got.GetMetadata() + if md.GetContextWindow() != 200000 { + t.Errorf("context_window = %d, want 200000", md.GetContextWindow()) + } + if md.GetInputCostMicroUsd() != 15 { + t.Errorf("input_cost_micro_usd = %d, want 15", md.GetInputCostMicroUsd()) + } + if md.GetOutputCostMicroUsd() != 75 { + t.Errorf("output_cost_micro_usd = %d, want 75", md.GetOutputCostMicroUsd()) + } + if md.GetApi() != "anthropic-messages" { + t.Errorf("api = %q, want %q", md.GetApi(), "anthropic-messages") + } + // L4: candidate read-back order equals written order. + cands := got.GetCandidates() + if len(cands) != 2 { + t.Fatalf("candidate count = %d, want 2", len(cands)) + } + if cands[0].GetModelId() != "claude-opus-4" || cands[1].GetModelId() != "anthropic/opus" { + t.Errorf("candidate order = [%q, %q], want [claude-opus-4, anthropic/opus]", cands[0].GetModelId(), cands[1].GetModelId()) + } +} + +// TestDeleteModelRegistryClearsAndOrphaningIsInvalidArgument (L3a) drives the +// DeleteModelRegistry handler + its mapModelRegistryErr path at the wire +// boundary — the store-layer clear/orphaning pair is covered, but the handler +// and error mapping were exercised only for the admin-gate denial. A clear on a +// seeded registry succeeds and Get then reports version 0; a clear that would +// strand a published profile's pin maps the store's ErrInvalidArgument to +// CodeInvalidArgument. +func TestDeleteModelRegistryClearsAndOrphaningIsInvalidArgument(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + t.Run("clear on seeded registry succeeds and Get reports version 0", func(t *testing.T) { + f := newConfigFixture(t) + if _, err := f.client.PutModelRegistry(ctx, authReq(f, &compassv1.PutModelRegistryRequest{ + Registry: wireReg1("opus"), ExpectedVersion: 0, + })); err != nil { + t.Fatalf("seed: %v", err) + } + if _, err := f.client.DeleteModelRegistry(ctx, authReq(f, &compassv1.DeleteModelRegistryRequest{})); err != nil { + t.Fatalf("DeleteModelRegistry: %v", err) + } + getResp, err := f.client.GetModelRegistry(ctx, authReq(f, &compassv1.GetModelRegistryRequest{})) + if err != nil { + t.Fatalf("GetModelRegistry after clear: %v", err) + } + if getResp.Msg.GetVersion() != 0 { + t.Fatalf("version after clear = %d, want 0", getResp.Msg.GetVersion()) + } + }) + + t.Run("clear stranding a published profile pin is CodeInvalidArgument", func(t *testing.T) { + f := newConfigFixture(t) + if _, err := f.client.PutModelRegistry(ctx, authReq(f, &compassv1.PutModelRegistryRequest{ + Registry: wireReg1("opus"), ExpectedVersion: 0, + })); err != nil { + t.Fatalf("seed: %v", err) + } + if _, err := f.client.PutAgentConfig(ctx, authReq(f, &compassv1.PutAgentConfigRequest{ + Bundle: mkConfigBundle(t, map[string]string{ + "profiles/candidate/profile.yml": "models:\n manager: opus\n", + }), + })); err != nil { + t.Fatalf("publish pinning profile: %v", err) + } + _, err := f.client.DeleteModelRegistry(ctx, authReq(f, &compassv1.DeleteModelRegistryRequest{})) + if connect.CodeOf(err) != connect.CodeInvalidArgument { + t.Fatalf("orphaning clear = %v, want CodeInvalidArgument", connect.CodeOf(err)) + } + }) +} diff --git a/go/server/network_door.go b/go/server/network_door.go index 66b5be914..c16123d25 100644 --- a/go/server/network_door.go +++ b/go/server/network_door.go @@ -37,6 +37,25 @@ import ( // token from here; it is never logged. const adminTokenFile = "admin-token" +// compassServiceMaxReadBytes caps a single inbound message on the CompassService +// network door (M1). It clears the largest legitimate message the mount carries +// — a PutAgentConfig bundle up to the store's 64 MiB decompressed cap — with +// headroom for wire framing, while bounding an operator's PutModelRegistry / +// PutAgentConfig against an unbounded in-memory buffer. +const compassServiceMaxReadBytes = 128 << 20 // 128 MiB + +// siblingServiceMaxReadBytes caps a single inbound message on the CommsService +// and SecretsService mounts, which ride the same network door as CompassService +// but carry only small unary messages (a chat turn, a secret value) — none of +// the 64 MiB config-bundle scale CompassService's PutAgentConfig reaches (M2). +// connect-go imposes NO default read cap, and CommsService is authenticatedOpen +// (any member reaches PostMessage), so an uncapped sibling mount is STRICTLY +// more exposed than the admin-gated CompassService the round-1 fix already +// capped: an ordinary account could stream an arbitrarily large PostMessage the +// server buffers whole in memory. 16 MiB is generous over any legitimate +// message here while closing that hole — the guestd vsock.go:94 posture. +const siblingServiceMaxReadBytes = 16 << 20 // 16 MiB + // boundListeners holds the TCP listeners eagerly bound before any on-disk // state, plus the network door's validated TLS config. Binding up front means a // bad address, an in-use port, or a bad keypair fails Serve before it creates a @@ -280,15 +299,27 @@ func buildNetworkServer( auth.BearerStreamInterceptor(st), auth.NewAdminGate(adminID), ) - netPath, netHandler := compassv1connect.NewCompassServiceHandler(svc, interceptors) - netCommsPath, netCommsHandler := compassv1connect.NewCommsServiceHandler(commsSvc, interceptors) + // WithReadMaxBytes caps a single inbound message on the CompassService door + // (M1, defense in depth). connect-go imposes NO default read cap, so without + // this an operator could stream an arbitrarily large PutModelRegistry (or + // PutAgentConfig) message the server buffers whole in memory before the store + // door's own caps ever run. The bound is sized to the LARGEST legitimate + // message this mount carries: PutAgentConfig accepts a config bundle up to the + // store's 64 MiB decompressed cap (agent_config.go maxDecompressedBytes), so + // the transport cap must clear that with headroom for the gzip-compressed wire + // form plus proto framing. 128 MiB is that headroom; the model-registry + // payload (1 MiB store cap) sits far below it. Mirrors the WithReadMaxBytes + // posture the internal doors already take (guestd vsock.go:94 at 16 MiB, the + // agent gateway at maxAgentMessageBytes). + netPath, netHandler := compassv1connect.NewCompassServiceHandler(svc, interceptors, connect.WithReadMaxBytes(compassServiceMaxReadBytes)) + netCommsPath, netCommsHandler := compassv1connect.NewCommsServiceHandler(commsSvc, interceptors, connect.WithReadMaxBytes(siblingServiceMaxReadBytes)) netMux := http.NewServeMux() netMux.Handle(netPath, netHandler) netMux.Handle(netCommsPath, netCommsHandler) // SecretsService rides the same bearer + admin-gate chain: the gate classifies // its 3 procedures authenticatedOpen, so any authenticated account clears it and // the handler enforces the user-only writes / user-or-agent list. - netSecretsPath, netSecretsHandler := compassv1connect.NewSecretsServiceHandler(secretsSvc, interceptors) + netSecretsPath, netSecretsHandler := compassv1connect.NewSecretsServiceHandler(secretsSvc, interceptors, connect.WithReadMaxBytes(siblingServiceMaxReadBytes)) netMux.Handle(netSecretsPath, netSecretsHandler) // The internal RunnerService door: the surface a Runner dials out to (Enroll + 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 e6375d9c7..346e13fdd 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("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEiqAEKEFNldFNlY3JldFJlcXVlc3QSDAoEbmFtZRgBIAEoCRISCgV2YWx1ZRgCIAEoCUIDgAEBEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkiEwoRU2V0U2VjcmV0UmVzcG9uc2UiFAoSTGlzdFNlY3JldHNSZXF1ZXN0IkAKE0xpc3RTZWNyZXRzUmVzcG9uc2USKQoHc2VjcmV0cxgBIAMoCzIYLmNvbXBhc3MudjEuU2VjcmV0U3RhdHVzIqABCgxTZWNyZXRTdGF0dXMSDAoEbmFtZRgBIAEoCRIOCgZpc19zZXQYAiABKAgSLAoIZGVsaXZlcnkYAyABKA4yGi5jb21wYXNzLnYxLlNlY3JldERlbGl2ZXJ5EiQKBGtpbmQYBCABKA4yFi5jb21wYXNzLnYxLlNlY3JldEtpbmQSEAoIcHJvdmlkZXIYBSABKAkSDAoEaG9zdBgGIAEoCSIjChNEZWxldGVTZWNyZXRSZXF1ZXN0EgwKBG5hbWUYASABKAkiFgoURGVsZXRlU2VjcmV0UmVzcG9uc2UiFgoUR2V0U2VydmVySW5mb1JlcXVlc3QiPQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSEwoLYXBpX3ZlcnNpb24YAiABKAkiDwoNV2hvQW1JUmVxdWVzdCIkCg5XaG9BbUlSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJIkMKFlN1YnNjcmliZUV2ZW50c1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEIuIDChdTdWJzY3JpYmVFdmVudHNSZXNwb25zZRILCgNzZXEYASABKAQSEgoKYXRfdW5peF9tcxgCIAEoAxIWCg5pbnN0YW5jZV9lcG9jaBgDIAEoBBIUCgxzbmFwc2hvdF9zZXEYBCABKAQSMQoNc2VydmVyX3N0YXR1cxgKIAEoCzIYLmNvbXBhc3MudjEuU2VydmVyU3RhdHVzSAASNQoPcmVzeW5jX3JlcXVpcmVkGAsgASgLMhouY29tcGFzcy52MS5SZXN5bmNSZXF1aXJlZEgAEj4KFGFnZW50X3Nlc3Npb25fc3RhdHVzGAwgASgLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXNIABI8ChNhZ2VudF9tZXNzYWdlX2NodW5rGA0gASgLMh0uY29tcGFzcy52MS5BZ2VudE1lc3NhZ2VDaHVua0gAEjQKD2FnZW50X3Rvb2xfY2FsbBgOIAEoCzIZLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbEgAEisKCmFnZW50X3BsYW4YDyABKAsyFS5jb21wYXNzLnYxLkFnZW50UGxhbkgAEiIKBWlzc3VlGBAgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAQgkKB3BheWxvYWQiLgoWTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPAoXTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSI2CgxTZXJ2ZXJTdGF0dXMSJgoFc3RhdGUYASABKA4yFy5jb21wYXNzLnYxLlNlcnZlclN0YXRlIhAKDlJlc3luY1JlcXVpcmVkInAKEkFnZW50U2Vzc2lvblN0YXR1cxISCgpzZXNzaW9uX2lkGAEgASgJEiwKBXN0YXRlGAIgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZRIYChBhZ2VudF9hY2NvdW50X2lkGAMgASgJIkkKEUFnZW50TWVzc2FnZUNodW5rEhIKCnNlc3Npb25faWQYASABKAkSDAoEdGV4dBgCIAEoCRISCgppc190aG91Z2h0GAMgASgIInkKDUFnZW50VG9vbENhbGwSEgoKc2Vzc2lvbl9pZBgBIAEoCRIUCgx0b29sX2NhbGxfaWQYAiABKAkSDQoFdGl0bGUYAyABKAkSLwoGc3RhdHVzGAQgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIkwKCUFnZW50UGxhbhISCgpzZXNzaW9uX2lkGAEgASgJEisKB2VudHJpZXMYAiADKAsyGi5jb21wYXNzLnYxLkFnZW50UGxhbkVudHJ5IlMKDkFnZW50UGxhbkVudHJ5Eg8KB2NvbnRlbnQYASABKAkSMAoGc3RhdHVzGAIgASgOMiAuY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeVN0YXR1cyKsAwoMU2Vzc2lvbkV2ZW50EhAKCGV2ZW50X2lkGAEgASgJEhIKCmF0X3VuaXhfbXMYAiABKAMSOgoOYXNzaXN0YW50X3RleHQYAyABKAsyIC5jb21wYXNzLnYxLlNlc3Npb25Bc3Npc3RhbnRUZXh0SAASLwoIdGhpbmtpbmcYBCABKAsyGy5jb21wYXNzLnYxLlNlc3Npb25UaGlua2luZ0gAEjAKCXRvb2xfY2FsbBgFIAEoCzIbLmNvbXBhc3MudjEuU2Vzc2lvblRvb2xDYWxsSAASPQoQdG9vbF9jYWxsX3VwZGF0ZRgGIAEoCzIhLmNvbXBhc3MudjEuU2Vzc2lvblRvb2xDYWxsVXBkYXRlSAASJwoEcGxhbhgHIAEoCzIXLmNvbXBhc3MudjEuU2Vzc2lvblBsYW5IABIrCgZub3RpY2UYCCABKAsyGS5jb21wYXNzLnYxLlNlc3Npb25Ob3RpY2VIABI5ChFzZXNzaW9uX2luamVjdGlvbhgJIAEoCzIcLmNvbXBhc3MudjEuU2Vzc2lvbkluamVjdGlvbkgAQgcKBWV2ZW50IjgKFFNlc3Npb25Bc3Npc3RhbnRUZXh0EgwKBHRleHQYASABKAkSEgoKbWVzc2FnZV9pZBgCIAEoCSIzCg9TZXNzaW9uVGhpbmtpbmcSDAoEdGV4dBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJImcKD1Nlc3Npb25Ub29sQ2FsbBIUCgx0b29sX2NhbGxfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSLwoGc3RhdHVzGAMgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIpoBChVTZXNzaW9uVG9vbENhbGxVcGRhdGUSFAoMdG9vbF9jYWxsX2lkGAEgASgJEi8KBnN0YXR1cxgCIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cxIOCgZvdXRwdXQYAyABKAkSKgoFZGlmZnMYBCADKAsyGy5jb21wYXNzLnYxLlNlc3Npb25GaWxlRGlmZiJVCg9TZXNzaW9uRmlsZURpZmYSDAoEcGF0aBgBIAEoCRIVCghvbGRfdGV4dBgCIAEoCUgAiAEBEhAKCG5ld190ZXh0GAMgASgJQgsKCV9vbGRfdGV4dCI6CgtTZXNzaW9uUGxhbhIrCgdlbnRyaWVzGAEgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSI5Cg1TZXNzaW9uTm90aWNlEgwKBHRleHQYASABKAkSEQoEbGluaxgCIAEoCUgAiAEBQgcKBV9saW5rIoMBChBTZXNzaW9uSW5qZWN0aW9uEjEKB29wX2tpbmQYASABKA4yIC5jb21wYXNzLnYxLlNlc3Npb25JbmplY3Rpb25LaW5kEhIKCm1lc3NhZ2VfaWQYAiABKAkSEwoLZnJvbV9oYW5kbGUYAyABKAkSEwoLdHJhY2VwYXJlbnQYBCABKAkiMgocU3Vic2NyaWJlQWdlbnRTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIn4KEUFnZW50U2Vzc2lvbkZyYW1lEhIKCnNlc3Npb25faWQYASABKAkSJwoFZXZlbnQYAiABKAsyGC5jb21wYXNzLnYxLlNlc3Npb25FdmVudBIsCgVzdGF0ZRgDIAEoDjIdLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdGUicAoeUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgCIAEoCRIPCgdwZXJzb25hGAMgASgJEgwKBHJvbGUYBCABKAkiOQofUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRIWCg5jb250YWluZXJfbmFtZRgBIAEoCSJQChtSZW1vdmVBZ2VudFdvcmtzcGFjZVJlcXVlc3QSFgoOY29udGFpbmVyX25hbWUYASABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAiABKAkiHgocUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXNwb25zZSJjChhTdGFydEFnZW50U2Vzc2lvblJlcXVlc3QSFgoOY29udGFpbmVyX25hbWUYASABKAkSGQoRcmVzdW1lX3Nlc3Npb25faWQYAyABKAlKBAgCEANSDmluaXRpYWxfcHJvbXB0Ii8KGVN0YXJ0QWdlbnRTZXNzaW9uUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCSJaChFTcGF3bkFnZW50UmVxdWVzdBIUCgxhZ2VudF9oYW5kbGUYASABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAyABKAlKBAgCEANSDmluaXRpYWxfcHJvbXB0IkAKElNwYXduQWdlbnRSZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJEhYKDmNvbnRhaW5lcl9uYW1lGAIgASgJIi0KF1N0b3BBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiGgoYU3RvcEFnZW50U2Vzc2lvblJlc3BvbnNlIi8KGVJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIwChpSZWxvYWRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIisKFUdldEFnZW50U3RhdHVzUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIkoKFkdldEFnZW50U3RhdHVzUmVzcG9uc2USMAoIc3RhdHVzZXMYASADKAsyHi5jb21wYXNzLnYxLkFnZW50U2Vzc2lvblN0YXR1cyIrChFJc3N1ZVRva2VuUmVxdWVzdBIWCg5hY2NvdW50X2hhbmRsZRgBIAEoCSIjChJJc3N1ZVRva2VuUmVzcG9uc2USDQoFdG9rZW4YASABKAkiKAoSUmV2b2tlVG9rZW5SZXF1ZXN0EhIKBXRva2VuGAEgASgJQgOAAQEiFQoTUmV2b2tlVG9rZW5SZXNwb25zZSInChVQdXRBZ2VudENvbmZpZ1JlcXVlc3QSDgoGYnVuZGxlGAEgASgMIikKFlB1dEFnZW50Q29uZmlnUmVzcG9uc2USDwoHdmVyc2lvbhgBIAEoCSIbChlHZXRBZ2VudENvbmZpZ0luZm9SZXF1ZXN0ItoBChpHZXRBZ2VudENvbmZpZ0luZm9SZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJEg4KBnNraWxscxgCIAMoCRISCgpleHRlbnNpb25zGAMgAygJEhMKC21jcF9zZXJ2ZXJzGAQgAygJEhQKDGhhc19zZXR0aW5ncxgFIAEoCBIVCg1oYXNfYWdlbnRzX21kGAYgASgIEg0KBXJ1bGVzGAcgAygJEhEKCXN1YmFnZW50cxgIIAMoCRISCgpoYXNfbW9kZWxzGAkgASgIEg8KB3Byb21wdHMYCiADKAkiGgoYRGVsZXRlQWdlbnRDb25maWdSZXF1ZXN0IhsKGURlbGV0ZUFnZW50Q29uZmlnUmVzcG9uc2UiKAoQQWdlbnRBdHRyaWJ1dGlvbhIUCgxhZ2VudF9oYW5kbGUYASABKAkiRQoIRm9yZ2VSZWYSKwoIcHJvdmlkZXIYASABKA4yGS5jb21wYXNzLnYxLkZvcmdlUHJvdmlkZXISDAoEaG9zdBgCIAEoCSLUAwoFSXNzdWUSCgoCaWQYASABKAkSIwoFZm9yZ2UYAiABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAyABKAkSDgoGbnVtYmVyGAQgASgNEg0KBXRpdGxlGAUgASgJEgwKBGJvZHkYBiABKAkSEwoLZm9yZ2Vfc3RhdGUYByABKAkSCwoDdXJsGAggASgJEisKBWFnZW50GAkgASgLMhwuY29tcGFzcy52MS5BZ2VudEF0dHJpYnV0aW9uEhUKDWZvcmdlX2FjY291bnQYCiABKAkSDgoGbGFiZWxzGAsgAygJEi4KCnVwZGF0ZWRfYXQYEyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEiUKBXN0YXRlGAwgASgOMhYuY29tcGFzcy52MS5Jc3N1ZVN0YXRlEhAKCHByaW9yaXR5GA0gASgJEhAKCGFzc2lnbmVlGA4gASgJEg8KB3N1bW1hcnkYDyABKAkSDgoGYnJhbmNoGBAgASgJEiQKA3BycxgRIAMoCzIXLmNvbXBhc3MudjEuUHVsbFJlcXVlc3QSJwoHdHJhY2tlchgSIAEoCzIWLmNvbXBhc3MudjEuVHJhY2tlclJlZiKeAwoLUHVsbFJlcXVlc3QSIwoFZm9yZ2UYASABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAiABKAkSDgoGbnVtYmVyGAMgASgNEg0KBXRpdGxlGAQgASgJEhMKC2ZvcmdlX3N0YXRlGAUgASgJEgsKA3VybBgGIAEoCRIQCghoZWFkX3JlZhgHIAEoCRIQCghiYXNlX3JlZhgIIAEoCRIrCgVhZ2VudBgJIAEoCzIcLmNvbXBhc3MudjEuQWdlbnRBdHRyaWJ1dGlvbhIVCg1mb3JnZV9hY2NvdW50GAogASgJEg0KBWRyYWZ0GAsgASgIEikKB2NoYW5nZWQYDCABKAsyGC5jb21wYXNzLnYxLkNoYW5nZWRTdGF0cxIpCgZjaGVja3MYDSABKAsyGS5jb21wYXNzLnYxLkNoZWNrc1N1bW1hcnkSIwoHcmV2aWV3cxgOIAMoCzISLmNvbXBhc3MudjEuUmV2aWV3EikKB3RocmVhZHMYDyADKAsyGC5jb21wYXNzLnYxLlJldmlld1RocmVhZCJTCg1DaGVja3NTdW1tYXJ5EhAKCGhlYWRfc2hhGAEgASgJEg0KBXN0YXRlGAIgASgJEiEKBmNoZWNrcxgDIAMoCzIRLmNvbXBhc3MudjEuQ2hlY2siQwoFQ2hlY2sSDAoEbmFtZRgBIAEoCRINCgVzdGF0ZRgCIAEoCRILCgN1cmwYAyABKAkSEAoIcmVxdWlyZWQYBCABKAgiQwoMQ2hhbmdlZFN0YXRzEg0KBWZpbGVzGAEgASgNEhEKCWFkZGl0aW9ucxgCIAEoDRIRCglkZWxldGlvbnMYAyABKA0iQwoKVHJhY2tlclJlZhIMCgRraW5kGAEgASgJEgoKAmlkGAIgASgJEg4KBnN0YXR1cxgDIAEoCRILCgN1cmwYBCABKAkiRwoGUmV2aWV3Eg4KBmF1dGhvchgBIAEoCRIOCgZpc19ib3QYAiABKAgSDwoHdmVyZGljdBgDIAEoCRIMCgRib2R5GAQgASgJIlUKDFJldmlld1RocmVhZBIMCgRwYXRoGAEgASgJEhAKCHJlc29sdmVkGAIgASgIEiUKCGNvbW1lbnRzGAMgAygLMhMuY29tcGFzcy52MS5Db21tZW50IjcKB0NvbW1lbnQSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIMCgRib2R5GAMgASgJKmQKDlNlY3JldERlbGl2ZXJ5Eh8KG1NFQ1JFVF9ERUxJVkVSWV9VTlNQRUNJRklFRBAAEhgKFFNFQ1JFVF9ERUxJVkVSWV9GSUxFEAESFwoTU0VDUkVUX0RFTElWRVJZX0VOVhACKnAKClNlY3JldEtpbmQSGwoXU0VDUkVUX0tJTkRfVU5TUEVDSUZJRUQQABIXChNTRUNSRVRfS0lORF9HRU5FUklDEAESGAoUU0VDUkVUX0tJTkRfUFJPVklERVIQAhISCg5TRUNSRVRfS0lORF9HSBADKkMKC1NlcnZlclN0YXRlEhwKGFNFUlZFUl9TVEFURV9VTlNQRUNJRklFRBAAEhYKElNFUlZFUl9TVEFURV9SRUFEWRABKoICChFBZ2VudFNlc3Npb25TdGF0ZRIjCh9BR0VOVF9TRVNTSU9OX1NUQVRFX1VOU1BFQ0lGSUVEEAASIAocQUdFTlRfU0VTU0lPTl9TVEFURV9TVEFSVElORxABEh0KGUFHRU5UX1NFU1NJT05fU1RBVEVfUkVBRFkQAhIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX1dPUktJTkcQAxIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX1NUT1BQRUQQBBIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX0VSUk9SRUQQBRIkCiBBR0VOVF9TRVNTSU9OX1NUQVRFX0RJU0NPTk5FQ1RFRBAGKtIBChNBZ2VudFRvb2xDYWxsU3RhdHVzEiYKIkFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfVU5TUEVDSUZJRUQQABIiCh5BR0VOVF9UT09MX0NBTExfU1RBVFVTX1BFTkRJTkcQARImCiJBR0VOVF9UT09MX0NBTExfU1RBVFVTX0lOX1BST0dSRVNTEAISJAogQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19DT01QTEVURUQQAxIhCh1BR0VOVF9UT09MX0NBTExfU1RBVFVTX0ZBSUxFRBAEKrQBChRBZ2VudFBsYW5FbnRyeVN0YXR1cxInCiNBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19VTlNQRUNJRklFRBAAEiMKH0FHRU5UX1BMQU5fRU5UUllfU1RBVFVTX1BFTkRJTkcQARInCiNBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19JTl9QUk9HUkVTUxACEiUKIUFHRU5UX1BMQU5fRU5UUllfU1RBVFVTX0NPTVBMRVRFRBADKoQBChRTZXNzaW9uSW5qZWN0aW9uS2luZBImCiJTRVNTSU9OX0lOSkVDVElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASIAocU0VTU0lPTl9JTkpFQ1RJT05fS0lORF9TVEVFUhABEiIKHlNFU1NJT05fSU5KRUNUSU9OX0tJTkRfREVMSVZFUhACKvEBCgpJc3N1ZVN0YXRlEhsKF0lTU1VFX1NUQVRFX1VOU1BFQ0lGSUVEEAASFwoTSVNTVUVfU1RBVEVfQkFDS0xPRxABEhQKEElTU1VFX1NUQVRFX1RPRE8QAhIWChJJU1NVRV9TVEFURV9RVUVVRUQQAxIXChNJU1NVRV9TVEFURV9CTE9DS0VEEAQSGwoXSVNTVUVfU1RBVEVfSU5fUFJPR1JFU1MQBRIZChVJU1NVRV9TVEFURV9JTl9SRVZJRVcQBhIUChBJU1NVRV9TVEFURV9ET05FEAcSGAoUSVNTVUVfU1RBVEVfQVJDSElWRUQQCCqcAQoNRm9yZ2VQcm92aWRlchIeChpGT1JHRV9QUk9WSURFUl9VTlNQRUNJRklFRBAAEhkKFUZPUkdFX1BST1ZJREVSX0dJVEhVQhABEhkKFUZPUkdFX1BST1ZJREVSX0dJVExBQhACEhoKFkZPUkdFX1BST1ZJREVSX0ZPUkdFSk8QAxIZChVGT1JHRV9QUk9WSURFUl9MSU5FQVIQBDKtDAoOQ29tcGFzc1NlcnZpY2USVAoNR2V0U2VydmVySW5mbxIgLmNvbXBhc3MudjEuR2V0U2VydmVySW5mb1JlcXVlc3QaIS5jb21wYXNzLnYxLkdldFNlcnZlckluZm9SZXNwb25zZRI/CgZXaG9BbUkSGS5jb21wYXNzLnYxLldob0FtSVJlcXVlc3QaGi5jb21wYXNzLnYxLldob0FtSVJlc3BvbnNlElwKD1N1YnNjcmliZUV2ZW50cxIiLmNvbXBhc3MudjEuU3Vic2NyaWJlRXZlbnRzUmVxdWVzdBojLmNvbXBhc3MudjEuU3Vic2NyaWJlRXZlbnRzUmVzcG9uc2UwARJaCg9MaXN0Qm9hcmRJc3N1ZXMSIi5jb21wYXNzLnYxLkxpc3RCb2FyZElzc3Vlc1JlcXVlc3QaIy5jb21wYXNzLnYxLkxpc3RCb2FyZElzc3Vlc1Jlc3BvbnNlEnIKF1Byb3Zpc2lvbkFnZW50V29ya3NwYWNlEiouY29tcGFzcy52MS5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QaKy5jb21wYXNzLnYxLlByb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVzcG9uc2USYAoRU3RhcnRBZ2VudFNlc3Npb24SJC5jb21wYXNzLnYxLlN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBolLmNvbXBhc3MudjEuU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRJLCgpTcGF3bkFnZW50Eh0uY29tcGFzcy52MS5TcGF3bkFnZW50UmVxdWVzdBoeLmNvbXBhc3MudjEuU3Bhd25BZ2VudFJlc3BvbnNlEl0KEFN0b3BBZ2VudFNlc3Npb24SIy5jb21wYXNzLnYxLlN0b3BBZ2VudFNlc3Npb25SZXF1ZXN0GiQuY29tcGFzcy52MS5TdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2USaQoUUmVtb3ZlQWdlbnRXb3Jrc3BhY2USJy5jb21wYXNzLnYxLlJlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBooLmNvbXBhc3MudjEuUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJjChJSZWxvYWRBZ2VudFNlc3Npb24SJS5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QaJi5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlc3BvbnNlElcKDkdldEFnZW50U3RhdHVzEiEuY29tcGFzcy52MS5HZXRBZ2VudFN0YXR1c1JlcXVlc3QaIi5jb21wYXNzLnYxLkdldEFnZW50U3RhdHVzUmVzcG9uc2USYgoVU3Vic2NyaWJlQWdlbnRTZXNzaW9uEiguY29tcGFzcy52MS5TdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0Gh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25GcmFtZTABEksKCklzc3VlVG9rZW4SHS5jb21wYXNzLnYxLklzc3VlVG9rZW5SZXF1ZXN0Gh4uY29tcGFzcy52MS5Jc3N1ZVRva2VuUmVzcG9uc2USTgoLUmV2b2tlVG9rZW4SHi5jb21wYXNzLnYxLlJldm9rZVRva2VuUmVxdWVzdBofLmNvbXBhc3MudjEuUmV2b2tlVG9rZW5SZXNwb25zZRJXCg5QdXRBZ2VudENvbmZpZxIhLmNvbXBhc3MudjEuUHV0QWdlbnRDb25maWdSZXF1ZXN0GiIuY29tcGFzcy52MS5QdXRBZ2VudENvbmZpZ1Jlc3BvbnNlEmMKEkdldEFnZW50Q29uZmlnSW5mbxIlLmNvbXBhc3MudjEuR2V0QWdlbnRDb25maWdJbmZvUmVxdWVzdBomLmNvbXBhc3MudjEuR2V0QWdlbnRDb25maWdJbmZvUmVzcG9uc2USYAoRRGVsZXRlQWdlbnRDb25maWcSJC5jb21wYXNzLnYxLkRlbGV0ZUFnZW50Q29uZmlnUmVxdWVzdBolLmNvbXBhc3MudjEuRGVsZXRlQWdlbnRDb25maWdSZXNwb25zZTL9AQoOU2VjcmV0c1NlcnZpY2USSAoJU2V0U2VjcmV0EhwuY29tcGFzcy52MS5TZXRTZWNyZXRSZXF1ZXN0Gh0uY29tcGFzcy52MS5TZXRTZWNyZXRSZXNwb25zZRJOCgtMaXN0U2VjcmV0cxIeLmNvbXBhc3MudjEuTGlzdFNlY3JldHNSZXF1ZXN0Gh8uY29tcGFzcy52MS5MaXN0U2VjcmV0c1Jlc3BvbnNlElEKDERlbGV0ZVNlY3JldBIfLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVxdWVzdBogLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVzcG9uc2ViBnByb3RvMw", [file_google_protobuf_timestamp]); + fileDesc("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEiqAEKEFNldFNlY3JldFJlcXVlc3QSDAoEbmFtZRgBIAEoCRISCgV2YWx1ZRgCIAEoCUIDgAEBEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkiEwoRU2V0U2VjcmV0UmVzcG9uc2UiFAoSTGlzdFNlY3JldHNSZXF1ZXN0IkAKE0xpc3RTZWNyZXRzUmVzcG9uc2USKQoHc2VjcmV0cxgBIAMoCzIYLmNvbXBhc3MudjEuU2VjcmV0U3RhdHVzIqABCgxTZWNyZXRTdGF0dXMSDAoEbmFtZRgBIAEoCRIOCgZpc19zZXQYAiABKAgSLAoIZGVsaXZlcnkYAyABKA4yGi5jb21wYXNzLnYxLlNlY3JldERlbGl2ZXJ5EiQKBGtpbmQYBCABKA4yFi5jb21wYXNzLnYxLlNlY3JldEtpbmQSEAoIcHJvdmlkZXIYBSABKAkSDAoEaG9zdBgGIAEoCSIjChNEZWxldGVTZWNyZXRSZXF1ZXN0EgwKBG5hbWUYASABKAkiFgoURGVsZXRlU2VjcmV0UmVzcG9uc2UiFgoUR2V0U2VydmVySW5mb1JlcXVlc3QiPQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSEwoLYXBpX3ZlcnNpb24YAiABKAkiDwoNV2hvQW1JUmVxdWVzdCIkCg5XaG9BbUlSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJIkMKFlN1YnNjcmliZUV2ZW50c1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEIuIDChdTdWJzY3JpYmVFdmVudHNSZXNwb25zZRILCgNzZXEYASABKAQSEgoKYXRfdW5peF9tcxgCIAEoAxIWCg5pbnN0YW5jZV9lcG9jaBgDIAEoBBIUCgxzbmFwc2hvdF9zZXEYBCABKAQSMQoNc2VydmVyX3N0YXR1cxgKIAEoCzIYLmNvbXBhc3MudjEuU2VydmVyU3RhdHVzSAASNQoPcmVzeW5jX3JlcXVpcmVkGAsgASgLMhouY29tcGFzcy52MS5SZXN5bmNSZXF1aXJlZEgAEj4KFGFnZW50X3Nlc3Npb25fc3RhdHVzGAwgASgLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXNIABI8ChNhZ2VudF9tZXNzYWdlX2NodW5rGA0gASgLMh0uY29tcGFzcy52MS5BZ2VudE1lc3NhZ2VDaHVua0gAEjQKD2FnZW50X3Rvb2xfY2FsbBgOIAEoCzIZLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbEgAEisKCmFnZW50X3BsYW4YDyABKAsyFS5jb21wYXNzLnYxLkFnZW50UGxhbkgAEiIKBWlzc3VlGBAgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAQgkKB3BheWxvYWQiLgoWTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPAoXTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSI2CgxTZXJ2ZXJTdGF0dXMSJgoFc3RhdGUYASABKA4yFy5jb21wYXNzLnYxLlNlcnZlclN0YXRlIhAKDlJlc3luY1JlcXVpcmVkInAKEkFnZW50U2Vzc2lvblN0YXR1cxISCgpzZXNzaW9uX2lkGAEgASgJEiwKBXN0YXRlGAIgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZRIYChBhZ2VudF9hY2NvdW50X2lkGAMgASgJIkkKEUFnZW50TWVzc2FnZUNodW5rEhIKCnNlc3Npb25faWQYASABKAkSDAoEdGV4dBgCIAEoCRISCgppc190aG91Z2h0GAMgASgIInkKDUFnZW50VG9vbENhbGwSEgoKc2Vzc2lvbl9pZBgBIAEoCRIUCgx0b29sX2NhbGxfaWQYAiABKAkSDQoFdGl0bGUYAyABKAkSLwoGc3RhdHVzGAQgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIkwKCUFnZW50UGxhbhISCgpzZXNzaW9uX2lkGAEgASgJEisKB2VudHJpZXMYAiADKAsyGi5jb21wYXNzLnYxLkFnZW50UGxhbkVudHJ5IlMKDkFnZW50UGxhbkVudHJ5Eg8KB2NvbnRlbnQYASABKAkSMAoGc3RhdHVzGAIgASgOMiAuY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeVN0YXR1cyKsAwoMU2Vzc2lvbkV2ZW50EhAKCGV2ZW50X2lkGAEgASgJEhIKCmF0X3VuaXhfbXMYAiABKAMSOgoOYXNzaXN0YW50X3RleHQYAyABKAsyIC5jb21wYXNzLnYxLlNlc3Npb25Bc3Npc3RhbnRUZXh0SAASLwoIdGhpbmtpbmcYBCABKAsyGy5jb21wYXNzLnYxLlNlc3Npb25UaGlua2luZ0gAEjAKCXRvb2xfY2FsbBgFIAEoCzIbLmNvbXBhc3MudjEuU2Vzc2lvblRvb2xDYWxsSAASPQoQdG9vbF9jYWxsX3VwZGF0ZRgGIAEoCzIhLmNvbXBhc3MudjEuU2Vzc2lvblRvb2xDYWxsVXBkYXRlSAASJwoEcGxhbhgHIAEoCzIXLmNvbXBhc3MudjEuU2Vzc2lvblBsYW5IABIrCgZub3RpY2UYCCABKAsyGS5jb21wYXNzLnYxLlNlc3Npb25Ob3RpY2VIABI5ChFzZXNzaW9uX2luamVjdGlvbhgJIAEoCzIcLmNvbXBhc3MudjEuU2Vzc2lvbkluamVjdGlvbkgAQgcKBWV2ZW50IjgKFFNlc3Npb25Bc3Npc3RhbnRUZXh0EgwKBHRleHQYASABKAkSEgoKbWVzc2FnZV9pZBgCIAEoCSIzCg9TZXNzaW9uVGhpbmtpbmcSDAoEdGV4dBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJImcKD1Nlc3Npb25Ub29sQ2FsbBIUCgx0b29sX2NhbGxfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSLwoGc3RhdHVzGAMgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIpoBChVTZXNzaW9uVG9vbENhbGxVcGRhdGUSFAoMdG9vbF9jYWxsX2lkGAEgASgJEi8KBnN0YXR1cxgCIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cxIOCgZvdXRwdXQYAyABKAkSKgoFZGlmZnMYBCADKAsyGy5jb21wYXNzLnYxLlNlc3Npb25GaWxlRGlmZiJVCg9TZXNzaW9uRmlsZURpZmYSDAoEcGF0aBgBIAEoCRIVCghvbGRfdGV4dBgCIAEoCUgAiAEBEhAKCG5ld190ZXh0GAMgASgJQgsKCV9vbGRfdGV4dCI6CgtTZXNzaW9uUGxhbhIrCgdlbnRyaWVzGAEgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSI5Cg1TZXNzaW9uTm90aWNlEgwKBHRleHQYASABKAkSEQoEbGluaxgCIAEoCUgAiAEBQgcKBV9saW5rIoMBChBTZXNzaW9uSW5qZWN0aW9uEjEKB29wX2tpbmQYASABKA4yIC5jb21wYXNzLnYxLlNlc3Npb25JbmplY3Rpb25LaW5kEhIKCm1lc3NhZ2VfaWQYAiABKAkSEwoLZnJvbV9oYW5kbGUYAyABKAkSEwoLdHJhY2VwYXJlbnQYBCABKAkiMgocU3Vic2NyaWJlQWdlbnRTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIn4KEUFnZW50U2Vzc2lvbkZyYW1lEhIKCnNlc3Npb25faWQYASABKAkSJwoFZXZlbnQYAiABKAsyGC5jb21wYXNzLnYxLlNlc3Npb25FdmVudBIsCgVzdGF0ZRgDIAEoDjIdLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdGUicAoeUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgCIAEoCRIPCgdwZXJzb25hGAMgASgJEgwKBHJvbGUYBCABKAkiOQofUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRIWCg5jb250YWluZXJfbmFtZRgBIAEoCSJQChtSZW1vdmVBZ2VudFdvcmtzcGFjZVJlcXVlc3QSFgoOY29udGFpbmVyX25hbWUYASABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAiABKAkiHgocUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXNwb25zZSJjChhTdGFydEFnZW50U2Vzc2lvblJlcXVlc3QSFgoOY29udGFpbmVyX25hbWUYASABKAkSGQoRcmVzdW1lX3Nlc3Npb25faWQYAyABKAlKBAgCEANSDmluaXRpYWxfcHJvbXB0Ii8KGVN0YXJ0QWdlbnRTZXNzaW9uUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCSJaChFTcGF3bkFnZW50UmVxdWVzdBIUCgxhZ2VudF9oYW5kbGUYASABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAyABKAlKBAgCEANSDmluaXRpYWxfcHJvbXB0IkAKElNwYXduQWdlbnRSZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJEhYKDmNvbnRhaW5lcl9uYW1lGAIgASgJIi0KF1N0b3BBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiGgoYU3RvcEFnZW50U2Vzc2lvblJlc3BvbnNlIi8KGVJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIwChpSZWxvYWRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIisKFUdldEFnZW50U3RhdHVzUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIkoKFkdldEFnZW50U3RhdHVzUmVzcG9uc2USMAoIc3RhdHVzZXMYASADKAsyHi5jb21wYXNzLnYxLkFnZW50U2Vzc2lvblN0YXR1cyIrChFJc3N1ZVRva2VuUmVxdWVzdBIWCg5hY2NvdW50X2hhbmRsZRgBIAEoCSIjChJJc3N1ZVRva2VuUmVzcG9uc2USDQoFdG9rZW4YASABKAkiKAoSUmV2b2tlVG9rZW5SZXF1ZXN0EhIKBXRva2VuGAEgASgJQgOAAQEiFQoTUmV2b2tlVG9rZW5SZXNwb25zZSInChVQdXRBZ2VudENvbmZpZ1JlcXVlc3QSDgoGYnVuZGxlGAEgASgMIikKFlB1dEFnZW50Q29uZmlnUmVzcG9uc2USDwoHdmVyc2lvbhgBIAEoCSIbChlHZXRBZ2VudENvbmZpZ0luZm9SZXF1ZXN0ItoBChpHZXRBZ2VudENvbmZpZ0luZm9SZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJEg4KBnNraWxscxgCIAMoCRISCgpleHRlbnNpb25zGAMgAygJEhMKC21jcF9zZXJ2ZXJzGAQgAygJEhQKDGhhc19zZXR0aW5ncxgFIAEoCBIVCg1oYXNfYWdlbnRzX21kGAYgASgIEg0KBXJ1bGVzGAcgAygJEhEKCXN1YmFnZW50cxgIIAMoCRISCgpoYXNfbW9kZWxzGAkgASgIEg8KB3Byb21wdHMYCiADKAkiGgoYRGVsZXRlQWdlbnRDb25maWdSZXF1ZXN0IhsKGURlbGV0ZUFnZW50Q29uZmlnUmVzcG9uc2UiNAoOTW9kZWxDYW5kaWRhdGUSEAoIcHJvdmlkZXIYASABKAkSEAoIbW9kZWxfaWQYAiABKAkicQoNTW9kZWxNZXRhZGF0YRIWCg5jb250ZXh0X3dpbmRvdxgBIAEoAxIcChRpbnB1dF9jb3N0X21pY3JvX3VzZBgCIAEoAxIdChVvdXRwdXRfY29zdF9taWNyb191c2QYAyABKAMSCwoDYXBpGAQgASgJIocBChJNb2RlbFJlZ2lzdHJ5RW50cnkSFAoMZGlzcGxheV9uYW1lGAEgASgJEi4KCmNhbmRpZGF0ZXMYAiADKAsyGi5jb21wYXNzLnYxLk1vZGVsQ2FuZGlkYXRlEisKCG1ldGFkYXRhGAMgASgLMhkuY29tcGFzcy52MS5Nb2RlbE1ldGFkYXRhIpgBCg1Nb2RlbFJlZ2lzdHJ5EjcKB2VudHJpZXMYASADKAsyJi5jb21wYXNzLnYxLk1vZGVsUmVnaXN0cnkuRW50cmllc0VudHJ5Gk4KDEVudHJpZXNFbnRyeRILCgNrZXkYASABKAkSLQoFdmFsdWUYAiABKAsyHi5jb21wYXNzLnYxLk1vZGVsUmVnaXN0cnlFbnRyeToCOAEiYAoXUHV0TW9kZWxSZWdpc3RyeVJlcXVlc3QSKwoIcmVnaXN0cnkYASABKAsyGS5jb21wYXNzLnYxLk1vZGVsUmVnaXN0cnkSGAoQZXhwZWN0ZWRfdmVyc2lvbhgCIAEoAyIrChhQdXRNb2RlbFJlZ2lzdHJ5UmVzcG9uc2USDwoHdmVyc2lvbhgBIAEoAyIZChdHZXRNb2RlbFJlZ2lzdHJ5UmVxdWVzdCJYChhHZXRNb2RlbFJlZ2lzdHJ5UmVzcG9uc2USDwoHdmVyc2lvbhgBIAEoAxIrCghyZWdpc3RyeRgCIAEoCzIZLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeSIcChpEZWxldGVNb2RlbFJlZ2lzdHJ5UmVxdWVzdCIdChtEZWxldGVNb2RlbFJlZ2lzdHJ5UmVzcG9uc2UiKAoQQWdlbnRBdHRyaWJ1dGlvbhIUCgxhZ2VudF9oYW5kbGUYASABKAkiRQoIRm9yZ2VSZWYSKwoIcHJvdmlkZXIYASABKA4yGS5jb21wYXNzLnYxLkZvcmdlUHJvdmlkZXISDAoEaG9zdBgCIAEoCSLUAwoFSXNzdWUSCgoCaWQYASABKAkSIwoFZm9yZ2UYAiABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAyABKAkSDgoGbnVtYmVyGAQgASgNEg0KBXRpdGxlGAUgASgJEgwKBGJvZHkYBiABKAkSEwoLZm9yZ2Vfc3RhdGUYByABKAkSCwoDdXJsGAggASgJEisKBWFnZW50GAkgASgLMhwuY29tcGFzcy52MS5BZ2VudEF0dHJpYnV0aW9uEhUKDWZvcmdlX2FjY291bnQYCiABKAkSDgoGbGFiZWxzGAsgAygJEi4KCnVwZGF0ZWRfYXQYEyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEiUKBXN0YXRlGAwgASgOMhYuY29tcGFzcy52MS5Jc3N1ZVN0YXRlEhAKCHByaW9yaXR5GA0gASgJEhAKCGFzc2lnbmVlGA4gASgJEg8KB3N1bW1hcnkYDyABKAkSDgoGYnJhbmNoGBAgASgJEiQKA3BycxgRIAMoCzIXLmNvbXBhc3MudjEuUHVsbFJlcXVlc3QSJwoHdHJhY2tlchgSIAEoCzIWLmNvbXBhc3MudjEuVHJhY2tlclJlZiKeAwoLUHVsbFJlcXVlc3QSIwoFZm9yZ2UYASABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAiABKAkSDgoGbnVtYmVyGAMgASgNEg0KBXRpdGxlGAQgASgJEhMKC2ZvcmdlX3N0YXRlGAUgASgJEgsKA3VybBgGIAEoCRIQCghoZWFkX3JlZhgHIAEoCRIQCghiYXNlX3JlZhgIIAEoCRIrCgVhZ2VudBgJIAEoCzIcLmNvbXBhc3MudjEuQWdlbnRBdHRyaWJ1dGlvbhIVCg1mb3JnZV9hY2NvdW50GAogASgJEg0KBWRyYWZ0GAsgASgIEikKB2NoYW5nZWQYDCABKAsyGC5jb21wYXNzLnYxLkNoYW5nZWRTdGF0cxIpCgZjaGVja3MYDSABKAsyGS5jb21wYXNzLnYxLkNoZWNrc1N1bW1hcnkSIwoHcmV2aWV3cxgOIAMoCzISLmNvbXBhc3MudjEuUmV2aWV3EikKB3RocmVhZHMYDyADKAsyGC5jb21wYXNzLnYxLlJldmlld1RocmVhZCJTCg1DaGVja3NTdW1tYXJ5EhAKCGhlYWRfc2hhGAEgASgJEg0KBXN0YXRlGAIgASgJEiEKBmNoZWNrcxgDIAMoCzIRLmNvbXBhc3MudjEuQ2hlY2siQwoFQ2hlY2sSDAoEbmFtZRgBIAEoCRINCgVzdGF0ZRgCIAEoCRILCgN1cmwYAyABKAkSEAoIcmVxdWlyZWQYBCABKAgiQwoMQ2hhbmdlZFN0YXRzEg0KBWZpbGVzGAEgASgNEhEKCWFkZGl0aW9ucxgCIAEoDRIRCglkZWxldGlvbnMYAyABKA0iQwoKVHJhY2tlclJlZhIMCgRraW5kGAEgASgJEgoKAmlkGAIgASgJEg4KBnN0YXR1cxgDIAEoCRILCgN1cmwYBCABKAkiRwoGUmV2aWV3Eg4KBmF1dGhvchgBIAEoCRIOCgZpc19ib3QYAiABKAgSDwoHdmVyZGljdBgDIAEoCRIMCgRib2R5GAQgASgJIlUKDFJldmlld1RocmVhZBIMCgRwYXRoGAEgASgJEhAKCHJlc29sdmVkGAIgASgIEiUKCGNvbW1lbnRzGAMgAygLMhMuY29tcGFzcy52MS5Db21tZW50IjcKB0NvbW1lbnQSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIMCgRib2R5GAMgASgJKmQKDlNlY3JldERlbGl2ZXJ5Eh8KG1NFQ1JFVF9ERUxJVkVSWV9VTlNQRUNJRklFRBAAEhgKFFNFQ1JFVF9ERUxJVkVSWV9GSUxFEAESFwoTU0VDUkVUX0RFTElWRVJZX0VOVhACKnAKClNlY3JldEtpbmQSGwoXU0VDUkVUX0tJTkRfVU5TUEVDSUZJRUQQABIXChNTRUNSRVRfS0lORF9HRU5FUklDEAESGAoUU0VDUkVUX0tJTkRfUFJPVklERVIQAhISCg5TRUNSRVRfS0lORF9HSBADKkMKC1NlcnZlclN0YXRlEhwKGFNFUlZFUl9TVEFURV9VTlNQRUNJRklFRBAAEhYKElNFUlZFUl9TVEFURV9SRUFEWRABKoICChFBZ2VudFNlc3Npb25TdGF0ZRIjCh9BR0VOVF9TRVNTSU9OX1NUQVRFX1VOU1BFQ0lGSUVEEAASIAocQUdFTlRfU0VTU0lPTl9TVEFURV9TVEFSVElORxABEh0KGUFHRU5UX1NFU1NJT05fU1RBVEVfUkVBRFkQAhIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX1dPUktJTkcQAxIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX1NUT1BQRUQQBBIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX0VSUk9SRUQQBRIkCiBBR0VOVF9TRVNTSU9OX1NUQVRFX0RJU0NPTk5FQ1RFRBAGKtIBChNBZ2VudFRvb2xDYWxsU3RhdHVzEiYKIkFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfVU5TUEVDSUZJRUQQABIiCh5BR0VOVF9UT09MX0NBTExfU1RBVFVTX1BFTkRJTkcQARImCiJBR0VOVF9UT09MX0NBTExfU1RBVFVTX0lOX1BST0dSRVNTEAISJAogQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19DT01QTEVURUQQAxIhCh1BR0VOVF9UT09MX0NBTExfU1RBVFVTX0ZBSUxFRBAEKrQBChRBZ2VudFBsYW5FbnRyeVN0YXR1cxInCiNBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19VTlNQRUNJRklFRBAAEiMKH0FHRU5UX1BMQU5fRU5UUllfU1RBVFVTX1BFTkRJTkcQARInCiNBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19JTl9QUk9HUkVTUxACEiUKIUFHRU5UX1BMQU5fRU5UUllfU1RBVFVTX0NPTVBMRVRFRBADKoQBChRTZXNzaW9uSW5qZWN0aW9uS2luZBImCiJTRVNTSU9OX0lOSkVDVElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASIAocU0VTU0lPTl9JTkpFQ1RJT05fS0lORF9TVEVFUhABEiIKHlNFU1NJT05fSU5KRUNUSU9OX0tJTkRfREVMSVZFUhACKvEBCgpJc3N1ZVN0YXRlEhsKF0lTU1VFX1NUQVRFX1VOU1BFQ0lGSUVEEAASFwoTSVNTVUVfU1RBVEVfQkFDS0xPRxABEhQKEElTU1VFX1NUQVRFX1RPRE8QAhIWChJJU1NVRV9TVEFURV9RVUVVRUQQAxIXChNJU1NVRV9TVEFURV9CTE9DS0VEEAQSGwoXSVNTVUVfU1RBVEVfSU5fUFJPR1JFU1MQBRIZChVJU1NVRV9TVEFURV9JTl9SRVZJRVcQBhIUChBJU1NVRV9TVEFURV9ET05FEAcSGAoUSVNTVUVfU1RBVEVfQVJDSElWRUQQCCqcAQoNRm9yZ2VQcm92aWRlchIeChpGT1JHRV9QUk9WSURFUl9VTlNQRUNJRklFRBAAEhkKFUZPUkdFX1BST1ZJREVSX0dJVEhVQhABEhkKFUZPUkdFX1BST1ZJREVSX0dJVExBQhACEhoKFkZPUkdFX1BST1ZJREVSX0ZPUkdFSk8QAxIZChVGT1JHRV9QUk9WSURFUl9MSU5FQVIQBDLTDgoOQ29tcGFzc1NlcnZpY2USVAoNR2V0U2VydmVySW5mbxIgLmNvbXBhc3MudjEuR2V0U2VydmVySW5mb1JlcXVlc3QaIS5jb21wYXNzLnYxLkdldFNlcnZlckluZm9SZXNwb25zZRI/CgZXaG9BbUkSGS5jb21wYXNzLnYxLldob0FtSVJlcXVlc3QaGi5jb21wYXNzLnYxLldob0FtSVJlc3BvbnNlElwKD1N1YnNjcmliZUV2ZW50cxIiLmNvbXBhc3MudjEuU3Vic2NyaWJlRXZlbnRzUmVxdWVzdBojLmNvbXBhc3MudjEuU3Vic2NyaWJlRXZlbnRzUmVzcG9uc2UwARJaCg9MaXN0Qm9hcmRJc3N1ZXMSIi5jb21wYXNzLnYxLkxpc3RCb2FyZElzc3Vlc1JlcXVlc3QaIy5jb21wYXNzLnYxLkxpc3RCb2FyZElzc3Vlc1Jlc3BvbnNlEnIKF1Byb3Zpc2lvbkFnZW50V29ya3NwYWNlEiouY29tcGFzcy52MS5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QaKy5jb21wYXNzLnYxLlByb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVzcG9uc2USYAoRU3RhcnRBZ2VudFNlc3Npb24SJC5jb21wYXNzLnYxLlN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBolLmNvbXBhc3MudjEuU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRJLCgpTcGF3bkFnZW50Eh0uY29tcGFzcy52MS5TcGF3bkFnZW50UmVxdWVzdBoeLmNvbXBhc3MudjEuU3Bhd25BZ2VudFJlc3BvbnNlEl0KEFN0b3BBZ2VudFNlc3Npb24SIy5jb21wYXNzLnYxLlN0b3BBZ2VudFNlc3Npb25SZXF1ZXN0GiQuY29tcGFzcy52MS5TdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2USaQoUUmVtb3ZlQWdlbnRXb3Jrc3BhY2USJy5jb21wYXNzLnYxLlJlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBooLmNvbXBhc3MudjEuUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJjChJSZWxvYWRBZ2VudFNlc3Npb24SJS5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QaJi5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlc3BvbnNlElcKDkdldEFnZW50U3RhdHVzEiEuY29tcGFzcy52MS5HZXRBZ2VudFN0YXR1c1JlcXVlc3QaIi5jb21wYXNzLnYxLkdldEFnZW50U3RhdHVzUmVzcG9uc2USYgoVU3Vic2NyaWJlQWdlbnRTZXNzaW9uEiguY29tcGFzcy52MS5TdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0Gh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25GcmFtZTABEksKCklzc3VlVG9rZW4SHS5jb21wYXNzLnYxLklzc3VlVG9rZW5SZXF1ZXN0Gh4uY29tcGFzcy52MS5Jc3N1ZVRva2VuUmVzcG9uc2USTgoLUmV2b2tlVG9rZW4SHi5jb21wYXNzLnYxLlJldm9rZVRva2VuUmVxdWVzdBofLmNvbXBhc3MudjEuUmV2b2tlVG9rZW5SZXNwb25zZRJXCg5QdXRBZ2VudENvbmZpZxIhLmNvbXBhc3MudjEuUHV0QWdlbnRDb25maWdSZXF1ZXN0GiIuY29tcGFzcy52MS5QdXRBZ2VudENvbmZpZ1Jlc3BvbnNlEmMKEkdldEFnZW50Q29uZmlnSW5mbxIlLmNvbXBhc3MudjEuR2V0QWdlbnRDb25maWdJbmZvUmVxdWVzdBomLmNvbXBhc3MudjEuR2V0QWdlbnRDb25maWdJbmZvUmVzcG9uc2USYAoRRGVsZXRlQWdlbnRDb25maWcSJC5jb21wYXNzLnYxLkRlbGV0ZUFnZW50Q29uZmlnUmVxdWVzdBolLmNvbXBhc3MudjEuRGVsZXRlQWdlbnRDb25maWdSZXNwb25zZRJdChBQdXRNb2RlbFJlZ2lzdHJ5EiMuY29tcGFzcy52MS5QdXRNb2RlbFJlZ2lzdHJ5UmVxdWVzdBokLmNvbXBhc3MudjEuUHV0TW9kZWxSZWdpc3RyeVJlc3BvbnNlEl0KEEdldE1vZGVsUmVnaXN0cnkSIy5jb21wYXNzLnYxLkdldE1vZGVsUmVnaXN0cnlSZXF1ZXN0GiQuY29tcGFzcy52MS5HZXRNb2RlbFJlZ2lzdHJ5UmVzcG9uc2USZgoTRGVsZXRlTW9kZWxSZWdpc3RyeRImLmNvbXBhc3MudjEuRGVsZXRlTW9kZWxSZWdpc3RyeVJlcXVlc3QaJy5jb21wYXNzLnYxLkRlbGV0ZU1vZGVsUmVnaXN0cnlSZXNwb25zZTL9AQoOU2VjcmV0c1NlcnZpY2USSAoJU2V0U2VjcmV0EhwuY29tcGFzcy52MS5TZXRTZWNyZXRSZXF1ZXN0Gh0uY29tcGFzcy52MS5TZXRTZWNyZXRSZXNwb25zZRJOCgtMaXN0U2VjcmV0cxIeLmNvbXBhc3MudjEuTGlzdFNlY3JldHNSZXF1ZXN0Gh8uY29tcGFzcy52MS5MaXN0U2VjcmV0c1Jlc3BvbnNlElEKDERlbGV0ZVNlY3JldBIfLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVxdWVzdBogLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVzcG9uc2ViBnByb3RvMw", [file_google_protobuf_timestamp]); /** * @generated from message compass.v1.SetSecretRequest @@ -1668,6 +1668,242 @@ export type DeleteAgentConfigResponse = Message<"compass.v1.DeleteAgentConfigRes export const DeleteAgentConfigResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_compass_v1_compass, 56); +/** + * One candidate in a stable name's ordered chain: an upstream (provider, + * model_id) the gateway resolver tries in order until one has a usable + * credential for the caller. The pair is opaque here — the SDK owns the + * selector grammar; the store validates only that both are non-empty and + * whitespace-free. + * + * @generated from message compass.v1.ModelCandidate + */ +export type ModelCandidate = Message<"compass.v1.ModelCandidate"> & { + /** + * @generated from field: string provider = 1; + */ + provider: string; + + /** + * @generated from field: string model_id = 2; + */ + modelId: string; +}; + +/** + * Describes the message compass.v1.ModelCandidate. + * Use `create(ModelCandidateSchema)` to create a new message. + */ +export const ModelCandidateSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 57); + +/** + * The listing metadata a stable name carries, taken from its primary candidate + * (OQ-2): the shape the container /v1/models listing and the cold-boot models.yml + * seed render from. All fields optional — an entry may carry none. + * + * @generated from message compass.v1.ModelMetadata + */ +export type ModelMetadata = Message<"compass.v1.ModelMetadata"> & { + /** + * The upstream context window in tokens, 0 when unknown. + * + * @generated from field: int64 context_window = 1; + */ + contextWindow: bigint; + + /** + * Cost per million input/output tokens in integer micro-USD (money is never a + * float), 0 when unknown. + * + * @generated from field: int64 input_cost_micro_usd = 2; + */ + inputCostMicroUsd: bigint; + + /** + * @generated from field: int64 output_cost_micro_usd = 3; + */ + outputCostMicroUsd: bigint; + + /** + * The SDK Api type the primary candidate speaks (e.g. "anthropic-messages", + * "openai-completions"), empty when unset. + * + * @generated from field: string api = 4; + */ + api: string; +}; + +/** + * Describes the message compass.v1.ModelMetadata. + * Use `create(ModelMetadataSchema)` to create a new message. + */ +export const ModelMetadataSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 58); + +/** + * One stable name's registry entry: a human display name, the ordered candidate + * chain, and the listing metadata. + * + * @generated from message compass.v1.ModelRegistryEntry + */ +export type ModelRegistryEntry = Message<"compass.v1.ModelRegistryEntry"> & { + /** + * @generated from field: string display_name = 1; + */ + displayName: string; + + /** + * @generated from field: repeated compass.v1.ModelCandidate candidates = 2; + */ + candidates: ModelCandidate[]; + + /** + * @generated from field: compass.v1.ModelMetadata metadata = 3; + */ + metadata?: ModelMetadata | undefined; +}; + +/** + * Describes the message compass.v1.ModelRegistryEntry. + * Use `create(ModelRegistryEntrySchema)` to create a new message. + */ +export const ModelRegistryEntrySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 59); + +/** + * The fleet model registry payload: the stable-name → entry map. The map key is + * the stable name (the modelId the gateway resolver looks up). + * + * @generated from message compass.v1.ModelRegistry + */ +export type ModelRegistry = Message<"compass.v1.ModelRegistry"> & { + /** + * @generated from field: map entries = 1; + */ + entries: { [key: string]: ModelRegistryEntry }; +}; + +/** + * Describes the message compass.v1.ModelRegistry. + * Use `create(ModelRegistrySchema)` to create a new message. + */ +export const ModelRegistrySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 60); + +/** + * PutModelRegistry: declare the fleet model registry. The caller's identity is + * the authenticated connection, never a field here. + * + * @generated from message compass.v1.PutModelRegistryRequest + */ +export type PutModelRegistryRequest = Message<"compass.v1.PutModelRegistryRequest"> & { + /** + * The registry payload to write, validated fail-closed at the door. + * + * @generated from field: compass.v1.ModelRegistry registry = 1; + */ + registry?: ModelRegistry | undefined; + + /** + * The version the caller read (compare-and-set). 0 seeds the first registry; + * a non-zero value must equal the row's current version or the write is + * ABORTED (a racing operator write landed first — re-read and retry). + * + * @generated from field: int64 expected_version = 2; + */ + expectedVersion: bigint; +}; + +/** + * Describes the message compass.v1.PutModelRegistryRequest. + * Use `create(PutModelRegistryRequestSchema)` to create a new message. + */ +export const PutModelRegistryRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 61); + +/** + * @generated from message compass.v1.PutModelRegistryResponse + */ +export type PutModelRegistryResponse = Message<"compass.v1.PutModelRegistryResponse"> & { + /** + * The new whole-registry version after the write (the prior version + 1). + * + * @generated from field: int64 version = 1; + */ + version: bigint; +}; + +/** + * Describes the message compass.v1.PutModelRegistryResponse. + * Use `create(PutModelRegistryResponseSchema)` to create a new message. + */ +export const PutModelRegistryResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 62); + +/** + * @generated from message compass.v1.GetModelRegistryRequest + */ +export type GetModelRegistryRequest = Message<"compass.v1.GetModelRegistryRequest"> & { +}; + +/** + * Describes the message compass.v1.GetModelRegistryRequest. + * Use `create(GetModelRegistryRequestSchema)` to create a new message. + */ +export const GetModelRegistryRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 63); + +/** + * GetModelRegistry: the current registry version and payload. An unconfigured + * fleet reports version 0 and an empty registry. + * + * @generated from message compass.v1.GetModelRegistryResponse + */ +export type GetModelRegistryResponse = Message<"compass.v1.GetModelRegistryResponse"> & { + /** + * @generated from field: int64 version = 1; + */ + version: bigint; + + /** + * @generated from field: compass.v1.ModelRegistry registry = 2; + */ + registry?: ModelRegistry | undefined; +}; + +/** + * Describes the message compass.v1.GetModelRegistryResponse. + * Use `create(GetModelRegistryResponseSchema)` to create a new message. + */ +export const GetModelRegistryResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 64); + +/** + * @generated from message compass.v1.DeleteModelRegistryRequest + */ +export type DeleteModelRegistryRequest = Message<"compass.v1.DeleteModelRegistryRequest"> & { +}; + +/** + * Describes the message compass.v1.DeleteModelRegistryRequest. + * Use `create(DeleteModelRegistryRequestSchema)` to create a new message. + */ +export const DeleteModelRegistryRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 65); + +/** + * @generated from message compass.v1.DeleteModelRegistryResponse + */ +export type DeleteModelRegistryResponse = Message<"compass.v1.DeleteModelRegistryResponse"> & { +}; + +/** + * Describes the message compass.v1.DeleteModelRegistryResponse. + * Use `create(DeleteModelRegistryResponseSchema)` to create a new message. + */ +export const DeleteModelRegistryResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 66); + /** * The Compass agent attribution parsed from the owner header at ingestion — a * plain display fact (DL-094, compass-attribution-simplification, Active). If a @@ -1696,7 +1932,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, 57); + messageDesc(file_compass_v1_compass, 67); /** * @generated from message compass.v1.ForgeRef @@ -1720,7 +1956,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, 58); + messageDesc(file_compass_v1_compass, 68); /** * The board unit: a Compass Issue — the forge issue's fields PLUS the Compass @@ -1878,7 +2114,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, 59); + messageDesc(file_compass_v1_compass, 69); /** * A Compass pull request: the forge PR's fields plus the Compass agent @@ -1985,7 +2221,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, 60); + messageDesc(file_compass_v1_compass, 70); /** * The rolled-up CI + status-check state on a PR head — Compass-owned, populated @@ -2017,7 +2253,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, 61); + messageDesc(file_compass_v1_compass, 71); /** * @generated from message compass.v1.Check @@ -2051,7 +2287,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, 62); + messageDesc(file_compass_v1_compass, 72); /** * A PR diffstat (files/additions/deletions), carried on PullRequest — a @@ -2081,7 +2317,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, 63); + messageDesc(file_compass_v1_compass, 73); /** * The linked tracker issue — the projection target (DL-032); the @@ -2122,7 +2358,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, 64); + messageDesc(file_compass_v1_compass, 74); /** * The full review state the right-sidebar PR pane shows — every submitted review @@ -2169,7 +2405,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, 65); + messageDesc(file_compass_v1_compass, 75); /** * @generated from message compass.v1.ReviewThread @@ -2198,7 +2434,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, 66); + messageDesc(file_compass_v1_compass, 76); /** * @generated from message compass.v1.Comment @@ -2225,7 +2461,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, 67); + messageDesc(file_compass_v1_compass, 77); /** * How a resolved secret is delivered into the agent container. Mirrors @@ -2857,6 +3093,50 @@ export const CompassService: GenService<{ input: typeof DeleteAgentConfigRequestSchema; output: typeof DeleteAgentConfigResponseSchema; }, + /** + * Declare the fleet model registry: the stable-name → candidate-chain map the + * gateway resolver routes with (compass-stable-name-routing §P2). Compare-and- + * set on the whole-registry version — the caller carries the version it read + * (0 to seed the first registry) and the write lands only if the row still + * holds it, so a racing operator write is never clobbered; a stale version is + * ABORTED. Admin-gated operator write (operator-scoped only: agents never + * author). The payload is validated fail-closed at the RPC boundary (schema + * shape, candidate provider/model_id shape, and no removal that would orphan a + * published profile's models.* reference) before a row is written. + * + * @generated from rpc compass.v1.CompassService.PutModelRegistry + */ + putModelRegistry: { + methodKind: "unary"; + input: typeof PutModelRegistryRequestSchema; + output: typeof PutModelRegistryResponseSchema; + }, + /** + * Report the current model registry: its version and full payload. An + * unconfigured fleet is a valid state — the response is empty (version 0, no + * entries), never an error. Value-free of credentials by construction (the + * registry names providers/models, never holds keys), so it is open to any + * authenticated account like GetAgentConfigInfo. + * + * @generated from rpc compass.v1.CompassService.GetModelRegistry + */ + getModelRegistry: { + methodKind: "unary"; + input: typeof GetModelRegistryRequestSchema; + output: typeof GetModelRegistryResponseSchema; + }, + /** + * Clear the fleet model registry back to the unconfigured state. Admin-gated + * like PutModelRegistry. Fails closed if the registry being cleared holds any + * stable name still referenced by a published profile's models.* map. + * + * @generated from rpc compass.v1.CompassService.DeleteModelRegistry + */ + deleteModelRegistry: { + methodKind: "unary"; + input: typeof DeleteModelRegistryRequestSchema; + output: typeof DeleteModelRegistryResponseSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_compass_v1_compass, 0); 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 e6375d9c7..346e13fdd 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("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEiqAEKEFNldFNlY3JldFJlcXVlc3QSDAoEbmFtZRgBIAEoCRISCgV2YWx1ZRgCIAEoCUIDgAEBEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkiEwoRU2V0U2VjcmV0UmVzcG9uc2UiFAoSTGlzdFNlY3JldHNSZXF1ZXN0IkAKE0xpc3RTZWNyZXRzUmVzcG9uc2USKQoHc2VjcmV0cxgBIAMoCzIYLmNvbXBhc3MudjEuU2VjcmV0U3RhdHVzIqABCgxTZWNyZXRTdGF0dXMSDAoEbmFtZRgBIAEoCRIOCgZpc19zZXQYAiABKAgSLAoIZGVsaXZlcnkYAyABKA4yGi5jb21wYXNzLnYxLlNlY3JldERlbGl2ZXJ5EiQKBGtpbmQYBCABKA4yFi5jb21wYXNzLnYxLlNlY3JldEtpbmQSEAoIcHJvdmlkZXIYBSABKAkSDAoEaG9zdBgGIAEoCSIjChNEZWxldGVTZWNyZXRSZXF1ZXN0EgwKBG5hbWUYASABKAkiFgoURGVsZXRlU2VjcmV0UmVzcG9uc2UiFgoUR2V0U2VydmVySW5mb1JlcXVlc3QiPQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSEwoLYXBpX3ZlcnNpb24YAiABKAkiDwoNV2hvQW1JUmVxdWVzdCIkCg5XaG9BbUlSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJIkMKFlN1YnNjcmliZUV2ZW50c1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEIuIDChdTdWJzY3JpYmVFdmVudHNSZXNwb25zZRILCgNzZXEYASABKAQSEgoKYXRfdW5peF9tcxgCIAEoAxIWCg5pbnN0YW5jZV9lcG9jaBgDIAEoBBIUCgxzbmFwc2hvdF9zZXEYBCABKAQSMQoNc2VydmVyX3N0YXR1cxgKIAEoCzIYLmNvbXBhc3MudjEuU2VydmVyU3RhdHVzSAASNQoPcmVzeW5jX3JlcXVpcmVkGAsgASgLMhouY29tcGFzcy52MS5SZXN5bmNSZXF1aXJlZEgAEj4KFGFnZW50X3Nlc3Npb25fc3RhdHVzGAwgASgLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXNIABI8ChNhZ2VudF9tZXNzYWdlX2NodW5rGA0gASgLMh0uY29tcGFzcy52MS5BZ2VudE1lc3NhZ2VDaHVua0gAEjQKD2FnZW50X3Rvb2xfY2FsbBgOIAEoCzIZLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbEgAEisKCmFnZW50X3BsYW4YDyABKAsyFS5jb21wYXNzLnYxLkFnZW50UGxhbkgAEiIKBWlzc3VlGBAgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAQgkKB3BheWxvYWQiLgoWTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPAoXTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSI2CgxTZXJ2ZXJTdGF0dXMSJgoFc3RhdGUYASABKA4yFy5jb21wYXNzLnYxLlNlcnZlclN0YXRlIhAKDlJlc3luY1JlcXVpcmVkInAKEkFnZW50U2Vzc2lvblN0YXR1cxISCgpzZXNzaW9uX2lkGAEgASgJEiwKBXN0YXRlGAIgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZRIYChBhZ2VudF9hY2NvdW50X2lkGAMgASgJIkkKEUFnZW50TWVzc2FnZUNodW5rEhIKCnNlc3Npb25faWQYASABKAkSDAoEdGV4dBgCIAEoCRISCgppc190aG91Z2h0GAMgASgIInkKDUFnZW50VG9vbENhbGwSEgoKc2Vzc2lvbl9pZBgBIAEoCRIUCgx0b29sX2NhbGxfaWQYAiABKAkSDQoFdGl0bGUYAyABKAkSLwoGc3RhdHVzGAQgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIkwKCUFnZW50UGxhbhISCgpzZXNzaW9uX2lkGAEgASgJEisKB2VudHJpZXMYAiADKAsyGi5jb21wYXNzLnYxLkFnZW50UGxhbkVudHJ5IlMKDkFnZW50UGxhbkVudHJ5Eg8KB2NvbnRlbnQYASABKAkSMAoGc3RhdHVzGAIgASgOMiAuY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeVN0YXR1cyKsAwoMU2Vzc2lvbkV2ZW50EhAKCGV2ZW50X2lkGAEgASgJEhIKCmF0X3VuaXhfbXMYAiABKAMSOgoOYXNzaXN0YW50X3RleHQYAyABKAsyIC5jb21wYXNzLnYxLlNlc3Npb25Bc3Npc3RhbnRUZXh0SAASLwoIdGhpbmtpbmcYBCABKAsyGy5jb21wYXNzLnYxLlNlc3Npb25UaGlua2luZ0gAEjAKCXRvb2xfY2FsbBgFIAEoCzIbLmNvbXBhc3MudjEuU2Vzc2lvblRvb2xDYWxsSAASPQoQdG9vbF9jYWxsX3VwZGF0ZRgGIAEoCzIhLmNvbXBhc3MudjEuU2Vzc2lvblRvb2xDYWxsVXBkYXRlSAASJwoEcGxhbhgHIAEoCzIXLmNvbXBhc3MudjEuU2Vzc2lvblBsYW5IABIrCgZub3RpY2UYCCABKAsyGS5jb21wYXNzLnYxLlNlc3Npb25Ob3RpY2VIABI5ChFzZXNzaW9uX2luamVjdGlvbhgJIAEoCzIcLmNvbXBhc3MudjEuU2Vzc2lvbkluamVjdGlvbkgAQgcKBWV2ZW50IjgKFFNlc3Npb25Bc3Npc3RhbnRUZXh0EgwKBHRleHQYASABKAkSEgoKbWVzc2FnZV9pZBgCIAEoCSIzCg9TZXNzaW9uVGhpbmtpbmcSDAoEdGV4dBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJImcKD1Nlc3Npb25Ub29sQ2FsbBIUCgx0b29sX2NhbGxfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSLwoGc3RhdHVzGAMgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIpoBChVTZXNzaW9uVG9vbENhbGxVcGRhdGUSFAoMdG9vbF9jYWxsX2lkGAEgASgJEi8KBnN0YXR1cxgCIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cxIOCgZvdXRwdXQYAyABKAkSKgoFZGlmZnMYBCADKAsyGy5jb21wYXNzLnYxLlNlc3Npb25GaWxlRGlmZiJVCg9TZXNzaW9uRmlsZURpZmYSDAoEcGF0aBgBIAEoCRIVCghvbGRfdGV4dBgCIAEoCUgAiAEBEhAKCG5ld190ZXh0GAMgASgJQgsKCV9vbGRfdGV4dCI6CgtTZXNzaW9uUGxhbhIrCgdlbnRyaWVzGAEgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSI5Cg1TZXNzaW9uTm90aWNlEgwKBHRleHQYASABKAkSEQoEbGluaxgCIAEoCUgAiAEBQgcKBV9saW5rIoMBChBTZXNzaW9uSW5qZWN0aW9uEjEKB29wX2tpbmQYASABKA4yIC5jb21wYXNzLnYxLlNlc3Npb25JbmplY3Rpb25LaW5kEhIKCm1lc3NhZ2VfaWQYAiABKAkSEwoLZnJvbV9oYW5kbGUYAyABKAkSEwoLdHJhY2VwYXJlbnQYBCABKAkiMgocU3Vic2NyaWJlQWdlbnRTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIn4KEUFnZW50U2Vzc2lvbkZyYW1lEhIKCnNlc3Npb25faWQYASABKAkSJwoFZXZlbnQYAiABKAsyGC5jb21wYXNzLnYxLlNlc3Npb25FdmVudBIsCgVzdGF0ZRgDIAEoDjIdLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdGUicAoeUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgCIAEoCRIPCgdwZXJzb25hGAMgASgJEgwKBHJvbGUYBCABKAkiOQofUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRIWCg5jb250YWluZXJfbmFtZRgBIAEoCSJQChtSZW1vdmVBZ2VudFdvcmtzcGFjZVJlcXVlc3QSFgoOY29udGFpbmVyX25hbWUYASABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAiABKAkiHgocUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXNwb25zZSJjChhTdGFydEFnZW50U2Vzc2lvblJlcXVlc3QSFgoOY29udGFpbmVyX25hbWUYASABKAkSGQoRcmVzdW1lX3Nlc3Npb25faWQYAyABKAlKBAgCEANSDmluaXRpYWxfcHJvbXB0Ii8KGVN0YXJ0QWdlbnRTZXNzaW9uUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCSJaChFTcGF3bkFnZW50UmVxdWVzdBIUCgxhZ2VudF9oYW5kbGUYASABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAyABKAlKBAgCEANSDmluaXRpYWxfcHJvbXB0IkAKElNwYXduQWdlbnRSZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJEhYKDmNvbnRhaW5lcl9uYW1lGAIgASgJIi0KF1N0b3BBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiGgoYU3RvcEFnZW50U2Vzc2lvblJlc3BvbnNlIi8KGVJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIwChpSZWxvYWRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIisKFUdldEFnZW50U3RhdHVzUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIkoKFkdldEFnZW50U3RhdHVzUmVzcG9uc2USMAoIc3RhdHVzZXMYASADKAsyHi5jb21wYXNzLnYxLkFnZW50U2Vzc2lvblN0YXR1cyIrChFJc3N1ZVRva2VuUmVxdWVzdBIWCg5hY2NvdW50X2hhbmRsZRgBIAEoCSIjChJJc3N1ZVRva2VuUmVzcG9uc2USDQoFdG9rZW4YASABKAkiKAoSUmV2b2tlVG9rZW5SZXF1ZXN0EhIKBXRva2VuGAEgASgJQgOAAQEiFQoTUmV2b2tlVG9rZW5SZXNwb25zZSInChVQdXRBZ2VudENvbmZpZ1JlcXVlc3QSDgoGYnVuZGxlGAEgASgMIikKFlB1dEFnZW50Q29uZmlnUmVzcG9uc2USDwoHdmVyc2lvbhgBIAEoCSIbChlHZXRBZ2VudENvbmZpZ0luZm9SZXF1ZXN0ItoBChpHZXRBZ2VudENvbmZpZ0luZm9SZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJEg4KBnNraWxscxgCIAMoCRISCgpleHRlbnNpb25zGAMgAygJEhMKC21jcF9zZXJ2ZXJzGAQgAygJEhQKDGhhc19zZXR0aW5ncxgFIAEoCBIVCg1oYXNfYWdlbnRzX21kGAYgASgIEg0KBXJ1bGVzGAcgAygJEhEKCXN1YmFnZW50cxgIIAMoCRISCgpoYXNfbW9kZWxzGAkgASgIEg8KB3Byb21wdHMYCiADKAkiGgoYRGVsZXRlQWdlbnRDb25maWdSZXF1ZXN0IhsKGURlbGV0ZUFnZW50Q29uZmlnUmVzcG9uc2UiKAoQQWdlbnRBdHRyaWJ1dGlvbhIUCgxhZ2VudF9oYW5kbGUYASABKAkiRQoIRm9yZ2VSZWYSKwoIcHJvdmlkZXIYASABKA4yGS5jb21wYXNzLnYxLkZvcmdlUHJvdmlkZXISDAoEaG9zdBgCIAEoCSLUAwoFSXNzdWUSCgoCaWQYASABKAkSIwoFZm9yZ2UYAiABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAyABKAkSDgoGbnVtYmVyGAQgASgNEg0KBXRpdGxlGAUgASgJEgwKBGJvZHkYBiABKAkSEwoLZm9yZ2Vfc3RhdGUYByABKAkSCwoDdXJsGAggASgJEisKBWFnZW50GAkgASgLMhwuY29tcGFzcy52MS5BZ2VudEF0dHJpYnV0aW9uEhUKDWZvcmdlX2FjY291bnQYCiABKAkSDgoGbGFiZWxzGAsgAygJEi4KCnVwZGF0ZWRfYXQYEyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEiUKBXN0YXRlGAwgASgOMhYuY29tcGFzcy52MS5Jc3N1ZVN0YXRlEhAKCHByaW9yaXR5GA0gASgJEhAKCGFzc2lnbmVlGA4gASgJEg8KB3N1bW1hcnkYDyABKAkSDgoGYnJhbmNoGBAgASgJEiQKA3BycxgRIAMoCzIXLmNvbXBhc3MudjEuUHVsbFJlcXVlc3QSJwoHdHJhY2tlchgSIAEoCzIWLmNvbXBhc3MudjEuVHJhY2tlclJlZiKeAwoLUHVsbFJlcXVlc3QSIwoFZm9yZ2UYASABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAiABKAkSDgoGbnVtYmVyGAMgASgNEg0KBXRpdGxlGAQgASgJEhMKC2ZvcmdlX3N0YXRlGAUgASgJEgsKA3VybBgGIAEoCRIQCghoZWFkX3JlZhgHIAEoCRIQCghiYXNlX3JlZhgIIAEoCRIrCgVhZ2VudBgJIAEoCzIcLmNvbXBhc3MudjEuQWdlbnRBdHRyaWJ1dGlvbhIVCg1mb3JnZV9hY2NvdW50GAogASgJEg0KBWRyYWZ0GAsgASgIEikKB2NoYW5nZWQYDCABKAsyGC5jb21wYXNzLnYxLkNoYW5nZWRTdGF0cxIpCgZjaGVja3MYDSABKAsyGS5jb21wYXNzLnYxLkNoZWNrc1N1bW1hcnkSIwoHcmV2aWV3cxgOIAMoCzISLmNvbXBhc3MudjEuUmV2aWV3EikKB3RocmVhZHMYDyADKAsyGC5jb21wYXNzLnYxLlJldmlld1RocmVhZCJTCg1DaGVja3NTdW1tYXJ5EhAKCGhlYWRfc2hhGAEgASgJEg0KBXN0YXRlGAIgASgJEiEKBmNoZWNrcxgDIAMoCzIRLmNvbXBhc3MudjEuQ2hlY2siQwoFQ2hlY2sSDAoEbmFtZRgBIAEoCRINCgVzdGF0ZRgCIAEoCRILCgN1cmwYAyABKAkSEAoIcmVxdWlyZWQYBCABKAgiQwoMQ2hhbmdlZFN0YXRzEg0KBWZpbGVzGAEgASgNEhEKCWFkZGl0aW9ucxgCIAEoDRIRCglkZWxldGlvbnMYAyABKA0iQwoKVHJhY2tlclJlZhIMCgRraW5kGAEgASgJEgoKAmlkGAIgASgJEg4KBnN0YXR1cxgDIAEoCRILCgN1cmwYBCABKAkiRwoGUmV2aWV3Eg4KBmF1dGhvchgBIAEoCRIOCgZpc19ib3QYAiABKAgSDwoHdmVyZGljdBgDIAEoCRIMCgRib2R5GAQgASgJIlUKDFJldmlld1RocmVhZBIMCgRwYXRoGAEgASgJEhAKCHJlc29sdmVkGAIgASgIEiUKCGNvbW1lbnRzGAMgAygLMhMuY29tcGFzcy52MS5Db21tZW50IjcKB0NvbW1lbnQSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIMCgRib2R5GAMgASgJKmQKDlNlY3JldERlbGl2ZXJ5Eh8KG1NFQ1JFVF9ERUxJVkVSWV9VTlNQRUNJRklFRBAAEhgKFFNFQ1JFVF9ERUxJVkVSWV9GSUxFEAESFwoTU0VDUkVUX0RFTElWRVJZX0VOVhACKnAKClNlY3JldEtpbmQSGwoXU0VDUkVUX0tJTkRfVU5TUEVDSUZJRUQQABIXChNTRUNSRVRfS0lORF9HRU5FUklDEAESGAoUU0VDUkVUX0tJTkRfUFJPVklERVIQAhISCg5TRUNSRVRfS0lORF9HSBADKkMKC1NlcnZlclN0YXRlEhwKGFNFUlZFUl9TVEFURV9VTlNQRUNJRklFRBAAEhYKElNFUlZFUl9TVEFURV9SRUFEWRABKoICChFBZ2VudFNlc3Npb25TdGF0ZRIjCh9BR0VOVF9TRVNTSU9OX1NUQVRFX1VOU1BFQ0lGSUVEEAASIAocQUdFTlRfU0VTU0lPTl9TVEFURV9TVEFSVElORxABEh0KGUFHRU5UX1NFU1NJT05fU1RBVEVfUkVBRFkQAhIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX1dPUktJTkcQAxIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX1NUT1BQRUQQBBIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX0VSUk9SRUQQBRIkCiBBR0VOVF9TRVNTSU9OX1NUQVRFX0RJU0NPTk5FQ1RFRBAGKtIBChNBZ2VudFRvb2xDYWxsU3RhdHVzEiYKIkFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfVU5TUEVDSUZJRUQQABIiCh5BR0VOVF9UT09MX0NBTExfU1RBVFVTX1BFTkRJTkcQARImCiJBR0VOVF9UT09MX0NBTExfU1RBVFVTX0lOX1BST0dSRVNTEAISJAogQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19DT01QTEVURUQQAxIhCh1BR0VOVF9UT09MX0NBTExfU1RBVFVTX0ZBSUxFRBAEKrQBChRBZ2VudFBsYW5FbnRyeVN0YXR1cxInCiNBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19VTlNQRUNJRklFRBAAEiMKH0FHRU5UX1BMQU5fRU5UUllfU1RBVFVTX1BFTkRJTkcQARInCiNBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19JTl9QUk9HUkVTUxACEiUKIUFHRU5UX1BMQU5fRU5UUllfU1RBVFVTX0NPTVBMRVRFRBADKoQBChRTZXNzaW9uSW5qZWN0aW9uS2luZBImCiJTRVNTSU9OX0lOSkVDVElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASIAocU0VTU0lPTl9JTkpFQ1RJT05fS0lORF9TVEVFUhABEiIKHlNFU1NJT05fSU5KRUNUSU9OX0tJTkRfREVMSVZFUhACKvEBCgpJc3N1ZVN0YXRlEhsKF0lTU1VFX1NUQVRFX1VOU1BFQ0lGSUVEEAASFwoTSVNTVUVfU1RBVEVfQkFDS0xPRxABEhQKEElTU1VFX1NUQVRFX1RPRE8QAhIWChJJU1NVRV9TVEFURV9RVUVVRUQQAxIXChNJU1NVRV9TVEFURV9CTE9DS0VEEAQSGwoXSVNTVUVfU1RBVEVfSU5fUFJPR1JFU1MQBRIZChVJU1NVRV9TVEFURV9JTl9SRVZJRVcQBhIUChBJU1NVRV9TVEFURV9ET05FEAcSGAoUSVNTVUVfU1RBVEVfQVJDSElWRUQQCCqcAQoNRm9yZ2VQcm92aWRlchIeChpGT1JHRV9QUk9WSURFUl9VTlNQRUNJRklFRBAAEhkKFUZPUkdFX1BST1ZJREVSX0dJVEhVQhABEhkKFUZPUkdFX1BST1ZJREVSX0dJVExBQhACEhoKFkZPUkdFX1BST1ZJREVSX0ZPUkdFSk8QAxIZChVGT1JHRV9QUk9WSURFUl9MSU5FQVIQBDKtDAoOQ29tcGFzc1NlcnZpY2USVAoNR2V0U2VydmVySW5mbxIgLmNvbXBhc3MudjEuR2V0U2VydmVySW5mb1JlcXVlc3QaIS5jb21wYXNzLnYxLkdldFNlcnZlckluZm9SZXNwb25zZRI/CgZXaG9BbUkSGS5jb21wYXNzLnYxLldob0FtSVJlcXVlc3QaGi5jb21wYXNzLnYxLldob0FtSVJlc3BvbnNlElwKD1N1YnNjcmliZUV2ZW50cxIiLmNvbXBhc3MudjEuU3Vic2NyaWJlRXZlbnRzUmVxdWVzdBojLmNvbXBhc3MudjEuU3Vic2NyaWJlRXZlbnRzUmVzcG9uc2UwARJaCg9MaXN0Qm9hcmRJc3N1ZXMSIi5jb21wYXNzLnYxLkxpc3RCb2FyZElzc3Vlc1JlcXVlc3QaIy5jb21wYXNzLnYxLkxpc3RCb2FyZElzc3Vlc1Jlc3BvbnNlEnIKF1Byb3Zpc2lvbkFnZW50V29ya3NwYWNlEiouY29tcGFzcy52MS5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QaKy5jb21wYXNzLnYxLlByb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVzcG9uc2USYAoRU3RhcnRBZ2VudFNlc3Npb24SJC5jb21wYXNzLnYxLlN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBolLmNvbXBhc3MudjEuU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRJLCgpTcGF3bkFnZW50Eh0uY29tcGFzcy52MS5TcGF3bkFnZW50UmVxdWVzdBoeLmNvbXBhc3MudjEuU3Bhd25BZ2VudFJlc3BvbnNlEl0KEFN0b3BBZ2VudFNlc3Npb24SIy5jb21wYXNzLnYxLlN0b3BBZ2VudFNlc3Npb25SZXF1ZXN0GiQuY29tcGFzcy52MS5TdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2USaQoUUmVtb3ZlQWdlbnRXb3Jrc3BhY2USJy5jb21wYXNzLnYxLlJlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBooLmNvbXBhc3MudjEuUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJjChJSZWxvYWRBZ2VudFNlc3Npb24SJS5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QaJi5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlc3BvbnNlElcKDkdldEFnZW50U3RhdHVzEiEuY29tcGFzcy52MS5HZXRBZ2VudFN0YXR1c1JlcXVlc3QaIi5jb21wYXNzLnYxLkdldEFnZW50U3RhdHVzUmVzcG9uc2USYgoVU3Vic2NyaWJlQWdlbnRTZXNzaW9uEiguY29tcGFzcy52MS5TdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0Gh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25GcmFtZTABEksKCklzc3VlVG9rZW4SHS5jb21wYXNzLnYxLklzc3VlVG9rZW5SZXF1ZXN0Gh4uY29tcGFzcy52MS5Jc3N1ZVRva2VuUmVzcG9uc2USTgoLUmV2b2tlVG9rZW4SHi5jb21wYXNzLnYxLlJldm9rZVRva2VuUmVxdWVzdBofLmNvbXBhc3MudjEuUmV2b2tlVG9rZW5SZXNwb25zZRJXCg5QdXRBZ2VudENvbmZpZxIhLmNvbXBhc3MudjEuUHV0QWdlbnRDb25maWdSZXF1ZXN0GiIuY29tcGFzcy52MS5QdXRBZ2VudENvbmZpZ1Jlc3BvbnNlEmMKEkdldEFnZW50Q29uZmlnSW5mbxIlLmNvbXBhc3MudjEuR2V0QWdlbnRDb25maWdJbmZvUmVxdWVzdBomLmNvbXBhc3MudjEuR2V0QWdlbnRDb25maWdJbmZvUmVzcG9uc2USYAoRRGVsZXRlQWdlbnRDb25maWcSJC5jb21wYXNzLnYxLkRlbGV0ZUFnZW50Q29uZmlnUmVxdWVzdBolLmNvbXBhc3MudjEuRGVsZXRlQWdlbnRDb25maWdSZXNwb25zZTL9AQoOU2VjcmV0c1NlcnZpY2USSAoJU2V0U2VjcmV0EhwuY29tcGFzcy52MS5TZXRTZWNyZXRSZXF1ZXN0Gh0uY29tcGFzcy52MS5TZXRTZWNyZXRSZXNwb25zZRJOCgtMaXN0U2VjcmV0cxIeLmNvbXBhc3MudjEuTGlzdFNlY3JldHNSZXF1ZXN0Gh8uY29tcGFzcy52MS5MaXN0U2VjcmV0c1Jlc3BvbnNlElEKDERlbGV0ZVNlY3JldBIfLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVxdWVzdBogLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVzcG9uc2ViBnByb3RvMw", [file_google_protobuf_timestamp]); + fileDesc("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEiqAEKEFNldFNlY3JldFJlcXVlc3QSDAoEbmFtZRgBIAEoCRISCgV2YWx1ZRgCIAEoCUIDgAEBEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkiEwoRU2V0U2VjcmV0UmVzcG9uc2UiFAoSTGlzdFNlY3JldHNSZXF1ZXN0IkAKE0xpc3RTZWNyZXRzUmVzcG9uc2USKQoHc2VjcmV0cxgBIAMoCzIYLmNvbXBhc3MudjEuU2VjcmV0U3RhdHVzIqABCgxTZWNyZXRTdGF0dXMSDAoEbmFtZRgBIAEoCRIOCgZpc19zZXQYAiABKAgSLAoIZGVsaXZlcnkYAyABKA4yGi5jb21wYXNzLnYxLlNlY3JldERlbGl2ZXJ5EiQKBGtpbmQYBCABKA4yFi5jb21wYXNzLnYxLlNlY3JldEtpbmQSEAoIcHJvdmlkZXIYBSABKAkSDAoEaG9zdBgGIAEoCSIjChNEZWxldGVTZWNyZXRSZXF1ZXN0EgwKBG5hbWUYASABKAkiFgoURGVsZXRlU2VjcmV0UmVzcG9uc2UiFgoUR2V0U2VydmVySW5mb1JlcXVlc3QiPQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSEwoLYXBpX3ZlcnNpb24YAiABKAkiDwoNV2hvQW1JUmVxdWVzdCIkCg5XaG9BbUlSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJIkMKFlN1YnNjcmliZUV2ZW50c1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEIuIDChdTdWJzY3JpYmVFdmVudHNSZXNwb25zZRILCgNzZXEYASABKAQSEgoKYXRfdW5peF9tcxgCIAEoAxIWCg5pbnN0YW5jZV9lcG9jaBgDIAEoBBIUCgxzbmFwc2hvdF9zZXEYBCABKAQSMQoNc2VydmVyX3N0YXR1cxgKIAEoCzIYLmNvbXBhc3MudjEuU2VydmVyU3RhdHVzSAASNQoPcmVzeW5jX3JlcXVpcmVkGAsgASgLMhouY29tcGFzcy52MS5SZXN5bmNSZXF1aXJlZEgAEj4KFGFnZW50X3Nlc3Npb25fc3RhdHVzGAwgASgLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXNIABI8ChNhZ2VudF9tZXNzYWdlX2NodW5rGA0gASgLMh0uY29tcGFzcy52MS5BZ2VudE1lc3NhZ2VDaHVua0gAEjQKD2FnZW50X3Rvb2xfY2FsbBgOIAEoCzIZLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbEgAEisKCmFnZW50X3BsYW4YDyABKAsyFS5jb21wYXNzLnYxLkFnZW50UGxhbkgAEiIKBWlzc3VlGBAgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAQgkKB3BheWxvYWQiLgoWTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPAoXTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSI2CgxTZXJ2ZXJTdGF0dXMSJgoFc3RhdGUYASABKA4yFy5jb21wYXNzLnYxLlNlcnZlclN0YXRlIhAKDlJlc3luY1JlcXVpcmVkInAKEkFnZW50U2Vzc2lvblN0YXR1cxISCgpzZXNzaW9uX2lkGAEgASgJEiwKBXN0YXRlGAIgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZRIYChBhZ2VudF9hY2NvdW50X2lkGAMgASgJIkkKEUFnZW50TWVzc2FnZUNodW5rEhIKCnNlc3Npb25faWQYASABKAkSDAoEdGV4dBgCIAEoCRISCgppc190aG91Z2h0GAMgASgIInkKDUFnZW50VG9vbENhbGwSEgoKc2Vzc2lvbl9pZBgBIAEoCRIUCgx0b29sX2NhbGxfaWQYAiABKAkSDQoFdGl0bGUYAyABKAkSLwoGc3RhdHVzGAQgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIkwKCUFnZW50UGxhbhISCgpzZXNzaW9uX2lkGAEgASgJEisKB2VudHJpZXMYAiADKAsyGi5jb21wYXNzLnYxLkFnZW50UGxhbkVudHJ5IlMKDkFnZW50UGxhbkVudHJ5Eg8KB2NvbnRlbnQYASABKAkSMAoGc3RhdHVzGAIgASgOMiAuY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeVN0YXR1cyKsAwoMU2Vzc2lvbkV2ZW50EhAKCGV2ZW50X2lkGAEgASgJEhIKCmF0X3VuaXhfbXMYAiABKAMSOgoOYXNzaXN0YW50X3RleHQYAyABKAsyIC5jb21wYXNzLnYxLlNlc3Npb25Bc3Npc3RhbnRUZXh0SAASLwoIdGhpbmtpbmcYBCABKAsyGy5jb21wYXNzLnYxLlNlc3Npb25UaGlua2luZ0gAEjAKCXRvb2xfY2FsbBgFIAEoCzIbLmNvbXBhc3MudjEuU2Vzc2lvblRvb2xDYWxsSAASPQoQdG9vbF9jYWxsX3VwZGF0ZRgGIAEoCzIhLmNvbXBhc3MudjEuU2Vzc2lvblRvb2xDYWxsVXBkYXRlSAASJwoEcGxhbhgHIAEoCzIXLmNvbXBhc3MudjEuU2Vzc2lvblBsYW5IABIrCgZub3RpY2UYCCABKAsyGS5jb21wYXNzLnYxLlNlc3Npb25Ob3RpY2VIABI5ChFzZXNzaW9uX2luamVjdGlvbhgJIAEoCzIcLmNvbXBhc3MudjEuU2Vzc2lvbkluamVjdGlvbkgAQgcKBWV2ZW50IjgKFFNlc3Npb25Bc3Npc3RhbnRUZXh0EgwKBHRleHQYASABKAkSEgoKbWVzc2FnZV9pZBgCIAEoCSIzCg9TZXNzaW9uVGhpbmtpbmcSDAoEdGV4dBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJImcKD1Nlc3Npb25Ub29sQ2FsbBIUCgx0b29sX2NhbGxfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSLwoGc3RhdHVzGAMgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIpoBChVTZXNzaW9uVG9vbENhbGxVcGRhdGUSFAoMdG9vbF9jYWxsX2lkGAEgASgJEi8KBnN0YXR1cxgCIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cxIOCgZvdXRwdXQYAyABKAkSKgoFZGlmZnMYBCADKAsyGy5jb21wYXNzLnYxLlNlc3Npb25GaWxlRGlmZiJVCg9TZXNzaW9uRmlsZURpZmYSDAoEcGF0aBgBIAEoCRIVCghvbGRfdGV4dBgCIAEoCUgAiAEBEhAKCG5ld190ZXh0GAMgASgJQgsKCV9vbGRfdGV4dCI6CgtTZXNzaW9uUGxhbhIrCgdlbnRyaWVzGAEgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSI5Cg1TZXNzaW9uTm90aWNlEgwKBHRleHQYASABKAkSEQoEbGluaxgCIAEoCUgAiAEBQgcKBV9saW5rIoMBChBTZXNzaW9uSW5qZWN0aW9uEjEKB29wX2tpbmQYASABKA4yIC5jb21wYXNzLnYxLlNlc3Npb25JbmplY3Rpb25LaW5kEhIKCm1lc3NhZ2VfaWQYAiABKAkSEwoLZnJvbV9oYW5kbGUYAyABKAkSEwoLdHJhY2VwYXJlbnQYBCABKAkiMgocU3Vic2NyaWJlQWdlbnRTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIn4KEUFnZW50U2Vzc2lvbkZyYW1lEhIKCnNlc3Npb25faWQYASABKAkSJwoFZXZlbnQYAiABKAsyGC5jb21wYXNzLnYxLlNlc3Npb25FdmVudBIsCgVzdGF0ZRgDIAEoDjIdLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdGUicAoeUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgCIAEoCRIPCgdwZXJzb25hGAMgASgJEgwKBHJvbGUYBCABKAkiOQofUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRIWCg5jb250YWluZXJfbmFtZRgBIAEoCSJQChtSZW1vdmVBZ2VudFdvcmtzcGFjZVJlcXVlc3QSFgoOY29udGFpbmVyX25hbWUYASABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAiABKAkiHgocUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXNwb25zZSJjChhTdGFydEFnZW50U2Vzc2lvblJlcXVlc3QSFgoOY29udGFpbmVyX25hbWUYASABKAkSGQoRcmVzdW1lX3Nlc3Npb25faWQYAyABKAlKBAgCEANSDmluaXRpYWxfcHJvbXB0Ii8KGVN0YXJ0QWdlbnRTZXNzaW9uUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCSJaChFTcGF3bkFnZW50UmVxdWVzdBIUCgxhZ2VudF9oYW5kbGUYASABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAyABKAlKBAgCEANSDmluaXRpYWxfcHJvbXB0IkAKElNwYXduQWdlbnRSZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJEhYKDmNvbnRhaW5lcl9uYW1lGAIgASgJIi0KF1N0b3BBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiGgoYU3RvcEFnZW50U2Vzc2lvblJlc3BvbnNlIi8KGVJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIwChpSZWxvYWRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIisKFUdldEFnZW50U3RhdHVzUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIkoKFkdldEFnZW50U3RhdHVzUmVzcG9uc2USMAoIc3RhdHVzZXMYASADKAsyHi5jb21wYXNzLnYxLkFnZW50U2Vzc2lvblN0YXR1cyIrChFJc3N1ZVRva2VuUmVxdWVzdBIWCg5hY2NvdW50X2hhbmRsZRgBIAEoCSIjChJJc3N1ZVRva2VuUmVzcG9uc2USDQoFdG9rZW4YASABKAkiKAoSUmV2b2tlVG9rZW5SZXF1ZXN0EhIKBXRva2VuGAEgASgJQgOAAQEiFQoTUmV2b2tlVG9rZW5SZXNwb25zZSInChVQdXRBZ2VudENvbmZpZ1JlcXVlc3QSDgoGYnVuZGxlGAEgASgMIikKFlB1dEFnZW50Q29uZmlnUmVzcG9uc2USDwoHdmVyc2lvbhgBIAEoCSIbChlHZXRBZ2VudENvbmZpZ0luZm9SZXF1ZXN0ItoBChpHZXRBZ2VudENvbmZpZ0luZm9SZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJEg4KBnNraWxscxgCIAMoCRISCgpleHRlbnNpb25zGAMgAygJEhMKC21jcF9zZXJ2ZXJzGAQgAygJEhQKDGhhc19zZXR0aW5ncxgFIAEoCBIVCg1oYXNfYWdlbnRzX21kGAYgASgIEg0KBXJ1bGVzGAcgAygJEhEKCXN1YmFnZW50cxgIIAMoCRISCgpoYXNfbW9kZWxzGAkgASgIEg8KB3Byb21wdHMYCiADKAkiGgoYRGVsZXRlQWdlbnRDb25maWdSZXF1ZXN0IhsKGURlbGV0ZUFnZW50Q29uZmlnUmVzcG9uc2UiNAoOTW9kZWxDYW5kaWRhdGUSEAoIcHJvdmlkZXIYASABKAkSEAoIbW9kZWxfaWQYAiABKAkicQoNTW9kZWxNZXRhZGF0YRIWCg5jb250ZXh0X3dpbmRvdxgBIAEoAxIcChRpbnB1dF9jb3N0X21pY3JvX3VzZBgCIAEoAxIdChVvdXRwdXRfY29zdF9taWNyb191c2QYAyABKAMSCwoDYXBpGAQgASgJIocBChJNb2RlbFJlZ2lzdHJ5RW50cnkSFAoMZGlzcGxheV9uYW1lGAEgASgJEi4KCmNhbmRpZGF0ZXMYAiADKAsyGi5jb21wYXNzLnYxLk1vZGVsQ2FuZGlkYXRlEisKCG1ldGFkYXRhGAMgASgLMhkuY29tcGFzcy52MS5Nb2RlbE1ldGFkYXRhIpgBCg1Nb2RlbFJlZ2lzdHJ5EjcKB2VudHJpZXMYASADKAsyJi5jb21wYXNzLnYxLk1vZGVsUmVnaXN0cnkuRW50cmllc0VudHJ5Gk4KDEVudHJpZXNFbnRyeRILCgNrZXkYASABKAkSLQoFdmFsdWUYAiABKAsyHi5jb21wYXNzLnYxLk1vZGVsUmVnaXN0cnlFbnRyeToCOAEiYAoXUHV0TW9kZWxSZWdpc3RyeVJlcXVlc3QSKwoIcmVnaXN0cnkYASABKAsyGS5jb21wYXNzLnYxLk1vZGVsUmVnaXN0cnkSGAoQZXhwZWN0ZWRfdmVyc2lvbhgCIAEoAyIrChhQdXRNb2RlbFJlZ2lzdHJ5UmVzcG9uc2USDwoHdmVyc2lvbhgBIAEoAyIZChdHZXRNb2RlbFJlZ2lzdHJ5UmVxdWVzdCJYChhHZXRNb2RlbFJlZ2lzdHJ5UmVzcG9uc2USDwoHdmVyc2lvbhgBIAEoAxIrCghyZWdpc3RyeRgCIAEoCzIZLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeSIcChpEZWxldGVNb2RlbFJlZ2lzdHJ5UmVxdWVzdCIdChtEZWxldGVNb2RlbFJlZ2lzdHJ5UmVzcG9uc2UiKAoQQWdlbnRBdHRyaWJ1dGlvbhIUCgxhZ2VudF9oYW5kbGUYASABKAkiRQoIRm9yZ2VSZWYSKwoIcHJvdmlkZXIYASABKA4yGS5jb21wYXNzLnYxLkZvcmdlUHJvdmlkZXISDAoEaG9zdBgCIAEoCSLUAwoFSXNzdWUSCgoCaWQYASABKAkSIwoFZm9yZ2UYAiABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAyABKAkSDgoGbnVtYmVyGAQgASgNEg0KBXRpdGxlGAUgASgJEgwKBGJvZHkYBiABKAkSEwoLZm9yZ2Vfc3RhdGUYByABKAkSCwoDdXJsGAggASgJEisKBWFnZW50GAkgASgLMhwuY29tcGFzcy52MS5BZ2VudEF0dHJpYnV0aW9uEhUKDWZvcmdlX2FjY291bnQYCiABKAkSDgoGbGFiZWxzGAsgAygJEi4KCnVwZGF0ZWRfYXQYEyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEiUKBXN0YXRlGAwgASgOMhYuY29tcGFzcy52MS5Jc3N1ZVN0YXRlEhAKCHByaW9yaXR5GA0gASgJEhAKCGFzc2lnbmVlGA4gASgJEg8KB3N1bW1hcnkYDyABKAkSDgoGYnJhbmNoGBAgASgJEiQKA3BycxgRIAMoCzIXLmNvbXBhc3MudjEuUHVsbFJlcXVlc3QSJwoHdHJhY2tlchgSIAEoCzIWLmNvbXBhc3MudjEuVHJhY2tlclJlZiKeAwoLUHVsbFJlcXVlc3QSIwoFZm9yZ2UYASABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAiABKAkSDgoGbnVtYmVyGAMgASgNEg0KBXRpdGxlGAQgASgJEhMKC2ZvcmdlX3N0YXRlGAUgASgJEgsKA3VybBgGIAEoCRIQCghoZWFkX3JlZhgHIAEoCRIQCghiYXNlX3JlZhgIIAEoCRIrCgVhZ2VudBgJIAEoCzIcLmNvbXBhc3MudjEuQWdlbnRBdHRyaWJ1dGlvbhIVCg1mb3JnZV9hY2NvdW50GAogASgJEg0KBWRyYWZ0GAsgASgIEikKB2NoYW5nZWQYDCABKAsyGC5jb21wYXNzLnYxLkNoYW5nZWRTdGF0cxIpCgZjaGVja3MYDSABKAsyGS5jb21wYXNzLnYxLkNoZWNrc1N1bW1hcnkSIwoHcmV2aWV3cxgOIAMoCzISLmNvbXBhc3MudjEuUmV2aWV3EikKB3RocmVhZHMYDyADKAsyGC5jb21wYXNzLnYxLlJldmlld1RocmVhZCJTCg1DaGVja3NTdW1tYXJ5EhAKCGhlYWRfc2hhGAEgASgJEg0KBXN0YXRlGAIgASgJEiEKBmNoZWNrcxgDIAMoCzIRLmNvbXBhc3MudjEuQ2hlY2siQwoFQ2hlY2sSDAoEbmFtZRgBIAEoCRINCgVzdGF0ZRgCIAEoCRILCgN1cmwYAyABKAkSEAoIcmVxdWlyZWQYBCABKAgiQwoMQ2hhbmdlZFN0YXRzEg0KBWZpbGVzGAEgASgNEhEKCWFkZGl0aW9ucxgCIAEoDRIRCglkZWxldGlvbnMYAyABKA0iQwoKVHJhY2tlclJlZhIMCgRraW5kGAEgASgJEgoKAmlkGAIgASgJEg4KBnN0YXR1cxgDIAEoCRILCgN1cmwYBCABKAkiRwoGUmV2aWV3Eg4KBmF1dGhvchgBIAEoCRIOCgZpc19ib3QYAiABKAgSDwoHdmVyZGljdBgDIAEoCRIMCgRib2R5GAQgASgJIlUKDFJldmlld1RocmVhZBIMCgRwYXRoGAEgASgJEhAKCHJlc29sdmVkGAIgASgIEiUKCGNvbW1lbnRzGAMgAygLMhMuY29tcGFzcy52MS5Db21tZW50IjcKB0NvbW1lbnQSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIMCgRib2R5GAMgASgJKmQKDlNlY3JldERlbGl2ZXJ5Eh8KG1NFQ1JFVF9ERUxJVkVSWV9VTlNQRUNJRklFRBAAEhgKFFNFQ1JFVF9ERUxJVkVSWV9GSUxFEAESFwoTU0VDUkVUX0RFTElWRVJZX0VOVhACKnAKClNlY3JldEtpbmQSGwoXU0VDUkVUX0tJTkRfVU5TUEVDSUZJRUQQABIXChNTRUNSRVRfS0lORF9HRU5FUklDEAESGAoUU0VDUkVUX0tJTkRfUFJPVklERVIQAhISCg5TRUNSRVRfS0lORF9HSBADKkMKC1NlcnZlclN0YXRlEhwKGFNFUlZFUl9TVEFURV9VTlNQRUNJRklFRBAAEhYKElNFUlZFUl9TVEFURV9SRUFEWRABKoICChFBZ2VudFNlc3Npb25TdGF0ZRIjCh9BR0VOVF9TRVNTSU9OX1NUQVRFX1VOU1BFQ0lGSUVEEAASIAocQUdFTlRfU0VTU0lPTl9TVEFURV9TVEFSVElORxABEh0KGUFHRU5UX1NFU1NJT05fU1RBVEVfUkVBRFkQAhIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX1dPUktJTkcQAxIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX1NUT1BQRUQQBBIfChtBR0VOVF9TRVNTSU9OX1NUQVRFX0VSUk9SRUQQBRIkCiBBR0VOVF9TRVNTSU9OX1NUQVRFX0RJU0NPTk5FQ1RFRBAGKtIBChNBZ2VudFRvb2xDYWxsU3RhdHVzEiYKIkFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfVU5TUEVDSUZJRUQQABIiCh5BR0VOVF9UT09MX0NBTExfU1RBVFVTX1BFTkRJTkcQARImCiJBR0VOVF9UT09MX0NBTExfU1RBVFVTX0lOX1BST0dSRVNTEAISJAogQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19DT01QTEVURUQQAxIhCh1BR0VOVF9UT09MX0NBTExfU1RBVFVTX0ZBSUxFRBAEKrQBChRBZ2VudFBsYW5FbnRyeVN0YXR1cxInCiNBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19VTlNQRUNJRklFRBAAEiMKH0FHRU5UX1BMQU5fRU5UUllfU1RBVFVTX1BFTkRJTkcQARInCiNBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19JTl9QUk9HUkVTUxACEiUKIUFHRU5UX1BMQU5fRU5UUllfU1RBVFVTX0NPTVBMRVRFRBADKoQBChRTZXNzaW9uSW5qZWN0aW9uS2luZBImCiJTRVNTSU9OX0lOSkVDVElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASIAocU0VTU0lPTl9JTkpFQ1RJT05fS0lORF9TVEVFUhABEiIKHlNFU1NJT05fSU5KRUNUSU9OX0tJTkRfREVMSVZFUhACKvEBCgpJc3N1ZVN0YXRlEhsKF0lTU1VFX1NUQVRFX1VOU1BFQ0lGSUVEEAASFwoTSVNTVUVfU1RBVEVfQkFDS0xPRxABEhQKEElTU1VFX1NUQVRFX1RPRE8QAhIWChJJU1NVRV9TVEFURV9RVUVVRUQQAxIXChNJU1NVRV9TVEFURV9CTE9DS0VEEAQSGwoXSVNTVUVfU1RBVEVfSU5fUFJPR1JFU1MQBRIZChVJU1NVRV9TVEFURV9JTl9SRVZJRVcQBhIUChBJU1NVRV9TVEFURV9ET05FEAcSGAoUSVNTVUVfU1RBVEVfQVJDSElWRUQQCCqcAQoNRm9yZ2VQcm92aWRlchIeChpGT1JHRV9QUk9WSURFUl9VTlNQRUNJRklFRBAAEhkKFUZPUkdFX1BST1ZJREVSX0dJVEhVQhABEhkKFUZPUkdFX1BST1ZJREVSX0dJVExBQhACEhoKFkZPUkdFX1BST1ZJREVSX0ZPUkdFSk8QAxIZChVGT1JHRV9QUk9WSURFUl9MSU5FQVIQBDLTDgoOQ29tcGFzc1NlcnZpY2USVAoNR2V0U2VydmVySW5mbxIgLmNvbXBhc3MudjEuR2V0U2VydmVySW5mb1JlcXVlc3QaIS5jb21wYXNzLnYxLkdldFNlcnZlckluZm9SZXNwb25zZRI/CgZXaG9BbUkSGS5jb21wYXNzLnYxLldob0FtSVJlcXVlc3QaGi5jb21wYXNzLnYxLldob0FtSVJlc3BvbnNlElwKD1N1YnNjcmliZUV2ZW50cxIiLmNvbXBhc3MudjEuU3Vic2NyaWJlRXZlbnRzUmVxdWVzdBojLmNvbXBhc3MudjEuU3Vic2NyaWJlRXZlbnRzUmVzcG9uc2UwARJaCg9MaXN0Qm9hcmRJc3N1ZXMSIi5jb21wYXNzLnYxLkxpc3RCb2FyZElzc3Vlc1JlcXVlc3QaIy5jb21wYXNzLnYxLkxpc3RCb2FyZElzc3Vlc1Jlc3BvbnNlEnIKF1Byb3Zpc2lvbkFnZW50V29ya3NwYWNlEiouY29tcGFzcy52MS5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QaKy5jb21wYXNzLnYxLlByb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVzcG9uc2USYAoRU3RhcnRBZ2VudFNlc3Npb24SJC5jb21wYXNzLnYxLlN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBolLmNvbXBhc3MudjEuU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRJLCgpTcGF3bkFnZW50Eh0uY29tcGFzcy52MS5TcGF3bkFnZW50UmVxdWVzdBoeLmNvbXBhc3MudjEuU3Bhd25BZ2VudFJlc3BvbnNlEl0KEFN0b3BBZ2VudFNlc3Npb24SIy5jb21wYXNzLnYxLlN0b3BBZ2VudFNlc3Npb25SZXF1ZXN0GiQuY29tcGFzcy52MS5TdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2USaQoUUmVtb3ZlQWdlbnRXb3Jrc3BhY2USJy5jb21wYXNzLnYxLlJlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBooLmNvbXBhc3MudjEuUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJjChJSZWxvYWRBZ2VudFNlc3Npb24SJS5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QaJi5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlc3BvbnNlElcKDkdldEFnZW50U3RhdHVzEiEuY29tcGFzcy52MS5HZXRBZ2VudFN0YXR1c1JlcXVlc3QaIi5jb21wYXNzLnYxLkdldEFnZW50U3RhdHVzUmVzcG9uc2USYgoVU3Vic2NyaWJlQWdlbnRTZXNzaW9uEiguY29tcGFzcy52MS5TdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0Gh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25GcmFtZTABEksKCklzc3VlVG9rZW4SHS5jb21wYXNzLnYxLklzc3VlVG9rZW5SZXF1ZXN0Gh4uY29tcGFzcy52MS5Jc3N1ZVRva2VuUmVzcG9uc2USTgoLUmV2b2tlVG9rZW4SHi5jb21wYXNzLnYxLlJldm9rZVRva2VuUmVxdWVzdBofLmNvbXBhc3MudjEuUmV2b2tlVG9rZW5SZXNwb25zZRJXCg5QdXRBZ2VudENvbmZpZxIhLmNvbXBhc3MudjEuUHV0QWdlbnRDb25maWdSZXF1ZXN0GiIuY29tcGFzcy52MS5QdXRBZ2VudENvbmZpZ1Jlc3BvbnNlEmMKEkdldEFnZW50Q29uZmlnSW5mbxIlLmNvbXBhc3MudjEuR2V0QWdlbnRDb25maWdJbmZvUmVxdWVzdBomLmNvbXBhc3MudjEuR2V0QWdlbnRDb25maWdJbmZvUmVzcG9uc2USYAoRRGVsZXRlQWdlbnRDb25maWcSJC5jb21wYXNzLnYxLkRlbGV0ZUFnZW50Q29uZmlnUmVxdWVzdBolLmNvbXBhc3MudjEuRGVsZXRlQWdlbnRDb25maWdSZXNwb25zZRJdChBQdXRNb2RlbFJlZ2lzdHJ5EiMuY29tcGFzcy52MS5QdXRNb2RlbFJlZ2lzdHJ5UmVxdWVzdBokLmNvbXBhc3MudjEuUHV0TW9kZWxSZWdpc3RyeVJlc3BvbnNlEl0KEEdldE1vZGVsUmVnaXN0cnkSIy5jb21wYXNzLnYxLkdldE1vZGVsUmVnaXN0cnlSZXF1ZXN0GiQuY29tcGFzcy52MS5HZXRNb2RlbFJlZ2lzdHJ5UmVzcG9uc2USZgoTRGVsZXRlTW9kZWxSZWdpc3RyeRImLmNvbXBhc3MudjEuRGVsZXRlTW9kZWxSZWdpc3RyeVJlcXVlc3QaJy5jb21wYXNzLnYxLkRlbGV0ZU1vZGVsUmVnaXN0cnlSZXNwb25zZTL9AQoOU2VjcmV0c1NlcnZpY2USSAoJU2V0U2VjcmV0EhwuY29tcGFzcy52MS5TZXRTZWNyZXRSZXF1ZXN0Gh0uY29tcGFzcy52MS5TZXRTZWNyZXRSZXNwb25zZRJOCgtMaXN0U2VjcmV0cxIeLmNvbXBhc3MudjEuTGlzdFNlY3JldHNSZXF1ZXN0Gh8uY29tcGFzcy52MS5MaXN0U2VjcmV0c1Jlc3BvbnNlElEKDERlbGV0ZVNlY3JldBIfLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVxdWVzdBogLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVzcG9uc2ViBnByb3RvMw", [file_google_protobuf_timestamp]); /** * @generated from message compass.v1.SetSecretRequest @@ -1668,6 +1668,242 @@ export type DeleteAgentConfigResponse = Message<"compass.v1.DeleteAgentConfigRes export const DeleteAgentConfigResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_compass_v1_compass, 56); +/** + * One candidate in a stable name's ordered chain: an upstream (provider, + * model_id) the gateway resolver tries in order until one has a usable + * credential for the caller. The pair is opaque here — the SDK owns the + * selector grammar; the store validates only that both are non-empty and + * whitespace-free. + * + * @generated from message compass.v1.ModelCandidate + */ +export type ModelCandidate = Message<"compass.v1.ModelCandidate"> & { + /** + * @generated from field: string provider = 1; + */ + provider: string; + + /** + * @generated from field: string model_id = 2; + */ + modelId: string; +}; + +/** + * Describes the message compass.v1.ModelCandidate. + * Use `create(ModelCandidateSchema)` to create a new message. + */ +export const ModelCandidateSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 57); + +/** + * The listing metadata a stable name carries, taken from its primary candidate + * (OQ-2): the shape the container /v1/models listing and the cold-boot models.yml + * seed render from. All fields optional — an entry may carry none. + * + * @generated from message compass.v1.ModelMetadata + */ +export type ModelMetadata = Message<"compass.v1.ModelMetadata"> & { + /** + * The upstream context window in tokens, 0 when unknown. + * + * @generated from field: int64 context_window = 1; + */ + contextWindow: bigint; + + /** + * Cost per million input/output tokens in integer micro-USD (money is never a + * float), 0 when unknown. + * + * @generated from field: int64 input_cost_micro_usd = 2; + */ + inputCostMicroUsd: bigint; + + /** + * @generated from field: int64 output_cost_micro_usd = 3; + */ + outputCostMicroUsd: bigint; + + /** + * The SDK Api type the primary candidate speaks (e.g. "anthropic-messages", + * "openai-completions"), empty when unset. + * + * @generated from field: string api = 4; + */ + api: string; +}; + +/** + * Describes the message compass.v1.ModelMetadata. + * Use `create(ModelMetadataSchema)` to create a new message. + */ +export const ModelMetadataSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 58); + +/** + * One stable name's registry entry: a human display name, the ordered candidate + * chain, and the listing metadata. + * + * @generated from message compass.v1.ModelRegistryEntry + */ +export type ModelRegistryEntry = Message<"compass.v1.ModelRegistryEntry"> & { + /** + * @generated from field: string display_name = 1; + */ + displayName: string; + + /** + * @generated from field: repeated compass.v1.ModelCandidate candidates = 2; + */ + candidates: ModelCandidate[]; + + /** + * @generated from field: compass.v1.ModelMetadata metadata = 3; + */ + metadata?: ModelMetadata | undefined; +}; + +/** + * Describes the message compass.v1.ModelRegistryEntry. + * Use `create(ModelRegistryEntrySchema)` to create a new message. + */ +export const ModelRegistryEntrySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 59); + +/** + * The fleet model registry payload: the stable-name → entry map. The map key is + * the stable name (the modelId the gateway resolver looks up). + * + * @generated from message compass.v1.ModelRegistry + */ +export type ModelRegistry = Message<"compass.v1.ModelRegistry"> & { + /** + * @generated from field: map entries = 1; + */ + entries: { [key: string]: ModelRegistryEntry }; +}; + +/** + * Describes the message compass.v1.ModelRegistry. + * Use `create(ModelRegistrySchema)` to create a new message. + */ +export const ModelRegistrySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 60); + +/** + * PutModelRegistry: declare the fleet model registry. The caller's identity is + * the authenticated connection, never a field here. + * + * @generated from message compass.v1.PutModelRegistryRequest + */ +export type PutModelRegistryRequest = Message<"compass.v1.PutModelRegistryRequest"> & { + /** + * The registry payload to write, validated fail-closed at the door. + * + * @generated from field: compass.v1.ModelRegistry registry = 1; + */ + registry?: ModelRegistry | undefined; + + /** + * The version the caller read (compare-and-set). 0 seeds the first registry; + * a non-zero value must equal the row's current version or the write is + * ABORTED (a racing operator write landed first — re-read and retry). + * + * @generated from field: int64 expected_version = 2; + */ + expectedVersion: bigint; +}; + +/** + * Describes the message compass.v1.PutModelRegistryRequest. + * Use `create(PutModelRegistryRequestSchema)` to create a new message. + */ +export const PutModelRegistryRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 61); + +/** + * @generated from message compass.v1.PutModelRegistryResponse + */ +export type PutModelRegistryResponse = Message<"compass.v1.PutModelRegistryResponse"> & { + /** + * The new whole-registry version after the write (the prior version + 1). + * + * @generated from field: int64 version = 1; + */ + version: bigint; +}; + +/** + * Describes the message compass.v1.PutModelRegistryResponse. + * Use `create(PutModelRegistryResponseSchema)` to create a new message. + */ +export const PutModelRegistryResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 62); + +/** + * @generated from message compass.v1.GetModelRegistryRequest + */ +export type GetModelRegistryRequest = Message<"compass.v1.GetModelRegistryRequest"> & { +}; + +/** + * Describes the message compass.v1.GetModelRegistryRequest. + * Use `create(GetModelRegistryRequestSchema)` to create a new message. + */ +export const GetModelRegistryRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 63); + +/** + * GetModelRegistry: the current registry version and payload. An unconfigured + * fleet reports version 0 and an empty registry. + * + * @generated from message compass.v1.GetModelRegistryResponse + */ +export type GetModelRegistryResponse = Message<"compass.v1.GetModelRegistryResponse"> & { + /** + * @generated from field: int64 version = 1; + */ + version: bigint; + + /** + * @generated from field: compass.v1.ModelRegistry registry = 2; + */ + registry?: ModelRegistry | undefined; +}; + +/** + * Describes the message compass.v1.GetModelRegistryResponse. + * Use `create(GetModelRegistryResponseSchema)` to create a new message. + */ +export const GetModelRegistryResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 64); + +/** + * @generated from message compass.v1.DeleteModelRegistryRequest + */ +export type DeleteModelRegistryRequest = Message<"compass.v1.DeleteModelRegistryRequest"> & { +}; + +/** + * Describes the message compass.v1.DeleteModelRegistryRequest. + * Use `create(DeleteModelRegistryRequestSchema)` to create a new message. + */ +export const DeleteModelRegistryRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 65); + +/** + * @generated from message compass.v1.DeleteModelRegistryResponse + */ +export type DeleteModelRegistryResponse = Message<"compass.v1.DeleteModelRegistryResponse"> & { +}; + +/** + * Describes the message compass.v1.DeleteModelRegistryResponse. + * Use `create(DeleteModelRegistryResponseSchema)` to create a new message. + */ +export const DeleteModelRegistryResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_compass_v1_compass, 66); + /** * The Compass agent attribution parsed from the owner header at ingestion — a * plain display fact (DL-094, compass-attribution-simplification, Active). If a @@ -1696,7 +1932,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, 57); + messageDesc(file_compass_v1_compass, 67); /** * @generated from message compass.v1.ForgeRef @@ -1720,7 +1956,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, 58); + messageDesc(file_compass_v1_compass, 68); /** * The board unit: a Compass Issue — the forge issue's fields PLUS the Compass @@ -1878,7 +2114,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, 59); + messageDesc(file_compass_v1_compass, 69); /** * A Compass pull request: the forge PR's fields plus the Compass agent @@ -1985,7 +2221,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, 60); + messageDesc(file_compass_v1_compass, 70); /** * The rolled-up CI + status-check state on a PR head — Compass-owned, populated @@ -2017,7 +2253,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, 61); + messageDesc(file_compass_v1_compass, 71); /** * @generated from message compass.v1.Check @@ -2051,7 +2287,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, 62); + messageDesc(file_compass_v1_compass, 72); /** * A PR diffstat (files/additions/deletions), carried on PullRequest — a @@ -2081,7 +2317,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, 63); + messageDesc(file_compass_v1_compass, 73); /** * The linked tracker issue — the projection target (DL-032); the @@ -2122,7 +2358,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, 64); + messageDesc(file_compass_v1_compass, 74); /** * The full review state the right-sidebar PR pane shows — every submitted review @@ -2169,7 +2405,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, 65); + messageDesc(file_compass_v1_compass, 75); /** * @generated from message compass.v1.ReviewThread @@ -2198,7 +2434,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, 66); + messageDesc(file_compass_v1_compass, 76); /** * @generated from message compass.v1.Comment @@ -2225,7 +2461,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, 67); + messageDesc(file_compass_v1_compass, 77); /** * How a resolved secret is delivered into the agent container. Mirrors @@ -2857,6 +3093,50 @@ export const CompassService: GenService<{ input: typeof DeleteAgentConfigRequestSchema; output: typeof DeleteAgentConfigResponseSchema; }, + /** + * Declare the fleet model registry: the stable-name → candidate-chain map the + * gateway resolver routes with (compass-stable-name-routing §P2). Compare-and- + * set on the whole-registry version — the caller carries the version it read + * (0 to seed the first registry) and the write lands only if the row still + * holds it, so a racing operator write is never clobbered; a stale version is + * ABORTED. Admin-gated operator write (operator-scoped only: agents never + * author). The payload is validated fail-closed at the RPC boundary (schema + * shape, candidate provider/model_id shape, and no removal that would orphan a + * published profile's models.* reference) before a row is written. + * + * @generated from rpc compass.v1.CompassService.PutModelRegistry + */ + putModelRegistry: { + methodKind: "unary"; + input: typeof PutModelRegistryRequestSchema; + output: typeof PutModelRegistryResponseSchema; + }, + /** + * Report the current model registry: its version and full payload. An + * unconfigured fleet is a valid state — the response is empty (version 0, no + * entries), never an error. Value-free of credentials by construction (the + * registry names providers/models, never holds keys), so it is open to any + * authenticated account like GetAgentConfigInfo. + * + * @generated from rpc compass.v1.CompassService.GetModelRegistry + */ + getModelRegistry: { + methodKind: "unary"; + input: typeof GetModelRegistryRequestSchema; + output: typeof GetModelRegistryResponseSchema; + }, + /** + * Clear the fleet model registry back to the unconfigured state. Admin-gated + * like PutModelRegistry. Fails closed if the registry being cleared holds any + * stable name still referenced by a published profile's models.* map. + * + * @generated from rpc compass.v1.CompassService.DeleteModelRegistry + */ + deleteModelRegistry: { + methodKind: "unary"; + input: typeof DeleteModelRegistryRequestSchema; + output: typeof DeleteModelRegistryResponseSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_compass_v1_compass, 0); diff --git a/proto/compass/v1/compass.proto b/proto/compass/v1/compass.proto index 96ae4f1ca..ead518a90 100644 --- a/proto/compass/v1/compass.proto +++ b/proto/compass/v1/compass.proto @@ -152,6 +152,29 @@ service CompassService { // when already empty succeeds. On success every live Runner is signalled with // an empty config version so it re-materializes the empty config dir. rpc DeleteAgentConfig(DeleteAgentConfigRequest) returns (DeleteAgentConfigResponse); + + // Declare the fleet model registry: the stable-name → candidate-chain map the + // gateway resolver routes with (compass-stable-name-routing §P2). Compare-and- + // set on the whole-registry version — the caller carries the version it read + // (0 to seed the first registry) and the write lands only if the row still + // holds it, so a racing operator write is never clobbered; a stale version is + // ABORTED. Admin-gated operator write (operator-scoped only: agents never + // author). The payload is validated fail-closed at the RPC boundary (schema + // shape, candidate provider/model_id shape, and no removal that would orphan a + // published profile's models.* reference) before a row is written. + rpc PutModelRegistry(PutModelRegistryRequest) returns (PutModelRegistryResponse); + + // Report the current model registry: its version and full payload. An + // unconfigured fleet is a valid state — the response is empty (version 0, no + // entries), never an error. Value-free of credentials by construction (the + // registry names providers/models, never holds keys), so it is open to any + // authenticated account like GetAgentConfigInfo. + rpc GetModelRegistry(GetModelRegistryRequest) returns (GetModelRegistryResponse); + + // Clear the fleet model registry back to the unconfigured state. Admin-gated + // like PutModelRegistry. Fails closed if the registry being cleared holds any + // stable name still referenced by a published profile's models.* map. + rpc DeleteModelRegistry(DeleteModelRegistryRequest) returns (DeleteModelRegistryResponse); } // How a resolved secret is delivered into the agent container. Mirrors @@ -783,6 +806,73 @@ message GetAgentConfigInfoResponse { message DeleteAgentConfigRequest {} message DeleteAgentConfigResponse {} +// One candidate in a stable name's ordered chain: an upstream (provider, +// model_id) the gateway resolver tries in order until one has a usable +// credential for the caller. The pair is opaque here — the SDK owns the +// selector grammar; the store validates only that both are non-empty and +// whitespace-free. +message ModelCandidate { + string provider = 1; + string model_id = 2; +} + +// The listing metadata a stable name carries, taken from its primary candidate +// (OQ-2): the shape the container /v1/models listing and the cold-boot models.yml +// seed render from. All fields optional — an entry may carry none. +message ModelMetadata { + // The upstream context window in tokens, 0 when unknown. + int64 context_window = 1; + // Cost per million input/output tokens in integer micro-USD (money is never a + // float), 0 when unknown. + int64 input_cost_micro_usd = 2; + int64 output_cost_micro_usd = 3; + // The SDK Api type the primary candidate speaks (e.g. "anthropic-messages", + // "openai-completions"), empty when unset. + string api = 4; +} + +// One stable name's registry entry: a human display name, the ordered candidate +// chain, and the listing metadata. +message ModelRegistryEntry { + string display_name = 1; + repeated ModelCandidate candidates = 2; + ModelMetadata metadata = 3; +} + +// The fleet model registry payload: the stable-name → entry map. The map key is +// the stable name (the modelId the gateway resolver looks up). +message ModelRegistry { + map entries = 1; +} + +// PutModelRegistry: declare the fleet model registry. The caller's identity is +// the authenticated connection, never a field here. +message PutModelRegistryRequest { + // The registry payload to write, validated fail-closed at the door. + ModelRegistry registry = 1; + // The version the caller read (compare-and-set). 0 seeds the first registry; + // a non-zero value must equal the row's current version or the write is + // ABORTED (a racing operator write landed first — re-read and retry). + int64 expected_version = 2; +} + +message PutModelRegistryResponse { + // The new whole-registry version after the write (the prior version + 1). + int64 version = 1; +} + +message GetModelRegistryRequest {} + +// GetModelRegistry: the current registry version and payload. An unconfigured +// fleet reports version 0 and an empty registry. +message GetModelRegistryResponse { + int64 version = 1; + ModelRegistry registry = 2; +} + +message DeleteModelRegistryRequest {} +message DeleteModelRegistryResponse {} + // ── The canonical Compass issue/PR type family (compass-issue-model, #1018, // Active) ── One owned wire family: the server translates raw forge data into // these types at ingestion; the raw forge shape is NEVER a proto/wire type