From 45c86b2a93a925b371f05e2b14105093b4faba59 Mon Sep 17 00:00:00 2001 From: Peter Jausovec Date: Tue, 19 May 2026 13:45:11 -0700 Subject: [PATCH 1/8] refresh openshell protos Signed-off-by: Peter Jausovec --- .../gen/computev1/compute_driver.pb.go | 49 +- .../gen/computev1/compute_driver_grpc.pb.go | 2 +- .../openshell/gen/datamodelv1/datamodel.pb.go | 28 +- .../openshell/gen/inferencev1/inference.pb.go | 5 +- .../gen/inferencev1/inference_grpc.pb.go | 2 +- .../openshell/gen/openshelltestv1/test.pb.go | 5 +- .../openshell/gen/openshellv1/openshell.pb.go | 5303 +++++++++++++---- .../gen/openshellv1/openshell_grpc.pb.go | 580 +- go/api/openshell/gen/sandboxv1/sandbox.pb.go | 397 +- go/api/openshell/proto/compute_driver.proto | 6 + go/api/openshell/proto/datamodel.proto | 6 +- go/api/openshell/proto/openshell.proto | 417 +- go/api/openshell/proto/sandbox.proto | 51 +- 13 files changed, 5389 insertions(+), 1462 deletions(-) diff --git a/go/api/openshell/gen/computev1/compute_driver.pb.go b/go/api/openshell/gen/computev1/compute_driver.pb.go index 9912021b73..1d635b64ad 100644 --- a/go/api/openshell/gen/computev1/compute_driver.pb.go +++ b/go/api/openshell/gen/computev1/compute_driver.pb.go @@ -4,15 +4,15 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v3.15.8 +// protoc (unknown) // source: compute_driver.proto package computev1 import ( - _struct "github.com/golang/protobuf/ptypes/struct" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + structpb "google.golang.org/protobuf/types/known/structpb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -70,7 +70,9 @@ type GetCapabilitiesResponse struct { // Default sandbox image recommended by the driver. DefaultImage string `protobuf:"bytes,3,opt,name=default_image,json=defaultImage,proto3" json:"default_image,omitempty"` // True when the driver can provision GPU-backed sandboxes. - SupportsGpu bool `protobuf:"varint,4,opt,name=supports_gpu,json=supportsGpu,proto3" json:"supports_gpu,omitempty"` + SupportsGpu bool `protobuf:"varint,4,opt,name=supports_gpu,json=supportsGpu,proto3" json:"supports_gpu,omitempty"` + // Number of GPUs available for sandbox assignment. + GpuCount uint32 `protobuf:"varint,5,opt,name=gpu_count,json=gpuCount,proto3" json:"gpu_count,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -133,6 +135,13 @@ func (x *GetCapabilitiesResponse) GetSupportsGpu() bool { return false } +func (x *GetCapabilitiesResponse) GetGpuCount() uint32 { + if x != nil { + return x.GpuCount + } + return 0 +} + // Driver-owned sandbox model used for create requests and platform observations. // // This intentionally omits gateway-owned lifecycle fields such as the public @@ -230,7 +239,11 @@ type DriverSandboxSpec struct { // Runtime template consumed by the driver during provisioning. Template *DriverSandboxTemplate `protobuf:"bytes,6,opt,name=template,proto3" json:"template,omitempty"` // Request NVIDIA GPU resources for this sandbox. - Gpu bool `protobuf:"varint,9,opt,name=gpu,proto3" json:"gpu,omitempty"` + Gpu bool `protobuf:"varint,9,opt,name=gpu,proto3" json:"gpu,omitempty"` + // Optional PCI BDF address (e.g. "0000:2d:00.0") or device index + // (e.g. "0", "1"). When empty with gpu=true, the driver assigns the + // first available GPU. + GpuDevice string `protobuf:"bytes,10,opt,name=gpu_device,json=gpuDevice,proto3" json:"gpu_device,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -293,6 +306,13 @@ func (x *DriverSandboxSpec) GetGpu() bool { return false } +func (x *DriverSandboxSpec) GetGpuDevice() string { + if x != nil { + return x.GpuDevice + } + return "" +} + // Driver-owned runtime template consumed by the compute platform. // // This message describes the sandbox workload in backend-neutral terms. @@ -316,7 +336,7 @@ type DriverSandboxTemplate struct { // The gateway does not inspect this; each driver defines its own schema. // For the Kubernetes driver this carries fields such as runtimeClassName, // annotations, and volumeClaimTemplates. - PlatformConfig *_struct.Struct `protobuf:"bytes,11,opt,name=platform_config,json=platformConfig,proto3" json:"platform_config,omitempty"` + PlatformConfig *structpb.Struct `protobuf:"bytes,11,opt,name=platform_config,json=platformConfig,proto3" json:"platform_config,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -386,7 +406,7 @@ func (x *DriverSandboxTemplate) GetResources() *DriverResourceRequirements { return nil } -func (x *DriverSandboxTemplate) GetPlatformConfig() *_struct.Struct { +func (x *DriverSandboxTemplate) GetPlatformConfig() *structpb.Struct { if x != nil { return x.PlatformConfig } @@ -1558,24 +1578,28 @@ var File_compute_driver_proto protoreflect.FileDescriptor const file_compute_driver_proto_rawDesc = "" + "\n" + "\x14compute_driver.proto\x12\x14openshell.compute.v1\x1a\x1cgoogle/protobuf/struct.proto\"\x18\n" + - "\x16GetCapabilitiesRequest\"\xa9\x01\n" + + "\x16GetCapabilitiesRequest\"\xc6\x01\n" + "\x17GetCapabilitiesResponse\x12\x1f\n" + "\vdriver_name\x18\x01 \x01(\tR\n" + "driverName\x12%\n" + "\x0edriver_version\x18\x02 \x01(\tR\rdriverVersion\x12#\n" + "\rdefault_image\x18\x03 \x01(\tR\fdefaultImage\x12!\n" + - "\fsupports_gpu\x18\x04 \x01(\bR\vsupportsGpu\"\xd1\x01\n" + + "\fsupports_gpu\x18\x04 \x01(\bR\vsupportsGpu\x12\x1b\n" + + "\tgpu_count\x18\x05 \x01(\rR\bgpuCount\"\xd1\x01\n" + "\rDriverSandbox\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x1c\n" + "\tnamespace\x18\x03 \x01(\tR\tnamespace\x12;\n" + "\x04spec\x18\x04 \x01(\v2'.openshell.compute.v1.DriverSandboxSpecR\x04spec\x12A\n" + - "\x06status\x18\x05 \x01(\v2).openshell.compute.v1.DriverSandboxStatusR\x06status\"\xa7\x02\n" + + "\x06status\x18\x05 \x01(\v2).openshell.compute.v1.DriverSandboxStatusR\x06status\"\xc6\x02\n" + "\x11DriverSandboxSpec\x12\x1b\n" + "\tlog_level\x18\x01 \x01(\tR\blogLevel\x12Z\n" + "\venvironment\x18\x05 \x03(\v28.openshell.compute.v1.DriverSandboxSpec.EnvironmentEntryR\venvironment\x12G\n" + "\btemplate\x18\x06 \x01(\v2+.openshell.compute.v1.DriverSandboxTemplateR\btemplate\x12\x10\n" + - "\x03gpu\x18\t \x01(\bR\x03gpu\x1a>\n" + + "\x03gpu\x18\t \x01(\bR\x03gpu\x12\x1d\n" + + "\n" + + "gpu_device\x18\n" + + " \x01(\tR\tgpuDevice\x1a>\n" + "\x10EnvironmentEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x97\x04\n" + @@ -1676,7 +1700,8 @@ const file_compute_driver_proto_rawDesc = "" + "\rCreateSandbox\x12*.openshell.compute.v1.CreateSandboxRequest\x1a+.openshell.compute.v1.CreateSandboxResponse\x12b\n" + "\vStopSandbox\x12(.openshell.compute.v1.StopSandboxRequest\x1a).openshell.compute.v1.StopSandboxResponse\x12h\n" + "\rDeleteSandbox\x12*.openshell.compute.v1.DeleteSandboxRequest\x1a+.openshell.compute.v1.DeleteSandboxResponse\x12j\n" + - "\x0eWatchSandboxes\x12+.openshell.compute.v1.WatchSandboxesRequest\x1a).openshell.compute.v1.WatchSandboxesEvent0\x01b\x06proto3" + "\x0eWatchSandboxes\x12+.openshell.compute.v1.WatchSandboxesRequest\x1a).openshell.compute.v1.WatchSandboxesEvent0\x01B\xdd\x01\n" + + "\x18com.openshell.compute.v1B\x12ComputeDriverProtoP\x01Z;github.com/kagent-dev/kagent/go/api/openshell/gen/computev1\xa2\x02\x03OCX\xaa\x02\x14Openshell.Compute.V1\xca\x02\x14Openshell\\Compute\\V1\xe2\x02 Openshell\\Compute\\V1\\GPBMetadata\xea\x02\x16Openshell::Compute::V1b\x06proto3" var ( file_compute_driver_proto_rawDescOnce sync.Once @@ -1722,7 +1747,7 @@ var file_compute_driver_proto_goTypes = []any{ nil, // 27: openshell.compute.v1.DriverSandboxTemplate.LabelsEntry nil, // 28: openshell.compute.v1.DriverSandboxTemplate.EnvironmentEntry nil, // 29: openshell.compute.v1.DriverPlatformEvent.MetadataEntry - (*_struct.Struct)(nil), // 30: google.protobuf.Struct + (*structpb.Struct)(nil), // 30: google.protobuf.Struct } var file_compute_driver_proto_depIdxs = []int32{ 3, // 0: openshell.compute.v1.DriverSandbox.spec:type_name -> openshell.compute.v1.DriverSandboxSpec diff --git a/go/api/openshell/gen/computev1/compute_driver_grpc.pb.go b/go/api/openshell/gen/computev1/compute_driver_grpc.pb.go index 72749162f6..5bbb329873 100644 --- a/go/api/openshell/gen/computev1/compute_driver_grpc.pb.go +++ b/go/api/openshell/gen/computev1/compute_driver_grpc.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v3.15.8 +// - protoc (unknown) // source: compute_driver.proto package computev1 diff --git a/go/api/openshell/gen/datamodelv1/datamodel.pb.go b/go/api/openshell/gen/datamodelv1/datamodel.pb.go index a99665ecdd..1d55e8ea91 100644 --- a/go/api/openshell/gen/datamodelv1/datamodel.pb.go +++ b/go/api/openshell/gen/datamodelv1/datamodel.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v3.15.8 +// protoc (unknown) // source: datamodel.proto package datamodelv1 @@ -27,7 +27,7 @@ const ( // Kubernetes-style metadata shared by all top-level OpenShell domain objects. // // This structure provides consistent metadata (identity, labels, timestamps, -// versioning) across Sandbox, Provider, SshSession, and other resources. +// resource versioning) across Sandbox, Provider, SshSession, and other resources. type ObjectMeta struct { state protoimpl.MessageState `protogen:"open.v1"` // Stable object ID generated by the gateway. @@ -38,9 +38,12 @@ type ObjectMeta struct { CreatedAtMs int64 `protobuf:"varint,3,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"` // Key-value labels for filtering and organization. // Labels must follow Kubernetes conventions: alphanumeric + `-._/`, max 63 chars per segment. - Labels map[string]string `protobuf:"bytes,4,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Labels map[string]string `protobuf:"bytes,4,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Optimistic concurrency control version. + // Incremented by the gateway on each update. Clients can use this for compare-and-swap operations. + ResourceVersion uint64 `protobuf:"varint,5,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ObjectMeta) Reset() { @@ -101,6 +104,13 @@ func (x *ObjectMeta) GetLabels() map[string]string { return nil } +func (x *ObjectMeta) GetResourceVersion() uint64 { + if x != nil { + return x.ResourceVersion + } + return 0 +} + // Provider model stored by OpenShell. type Provider struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -178,13 +188,14 @@ var File_datamodel_proto protoreflect.FileDescriptor const file_datamodel_proto_rawDesc = "" + "\n" + - "\x0fdatamodel.proto\x12\x16openshell.datamodel.v1\"\xd7\x01\n" + + "\x0fdatamodel.proto\x12\x16openshell.datamodel.v1\"\x82\x02\n" + "\n" + "ObjectMeta\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\"\n" + "\rcreated_at_ms\x18\x03 \x01(\x03R\vcreatedAtMs\x12F\n" + - "\x06labels\x18\x04 \x03(\v2..openshell.datamodel.v1.ObjectMeta.LabelsEntryR\x06labels\x1a9\n" + + "\x06labels\x18\x04 \x03(\v2..openshell.datamodel.v1.ObjectMeta.LabelsEntryR\x06labels\x12)\n" + + "\x10resource_version\x18\x05 \x01(\x04R\x0fresourceVersion\x1a9\n" + "\vLabelsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xf4\x02\n" + @@ -198,7 +209,8 @@ const file_datamodel_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a9\n" + "\vConfigEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01b\x06proto3" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\xe5\x01\n" + + "\x1acom.openshell.datamodel.v1B\x0eDatamodelProtoP\x01Z=github.com/kagent-dev/kagent/go/api/openshell/gen/datamodelv1\xa2\x02\x03ODX\xaa\x02\x16Openshell.Datamodel.V1\xca\x02\x16Openshell\\Datamodel\\V1\xe2\x02\"Openshell\\Datamodel\\V1\\GPBMetadata\xea\x02\x18Openshell::Datamodel::V1b\x06proto3" var ( file_datamodel_proto_rawDescOnce sync.Once diff --git a/go/api/openshell/gen/inferencev1/inference.pb.go b/go/api/openshell/gen/inferencev1/inference.pb.go index 30119a0585..c184850547 100644 --- a/go/api/openshell/gen/inferencev1/inference.pb.go +++ b/go/api/openshell/gen/inferencev1/inference.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v3.15.8 +// protoc (unknown) // source: inference.proto package inferencev1 @@ -768,7 +768,8 @@ const file_inference_proto_rawDesc = "" + "\tInference\x12{\n" + "\x12GetInferenceBundle\x121.openshell.inference.v1.GetInferenceBundleRequest\x1a2.openshell.inference.v1.GetInferenceBundleResponse\x12~\n" + "\x13SetClusterInference\x122.openshell.inference.v1.SetClusterInferenceRequest\x1a3.openshell.inference.v1.SetClusterInferenceResponse\x12~\n" + - "\x13GetClusterInference\x122.openshell.inference.v1.GetClusterInferenceRequest\x1a3.openshell.inference.v1.GetClusterInferenceResponseb\x06proto3" + "\x13GetClusterInference\x122.openshell.inference.v1.GetClusterInferenceRequest\x1a3.openshell.inference.v1.GetClusterInferenceResponseB\xe5\x01\n" + + "\x1acom.openshell.inference.v1B\x0eInferenceProtoP\x01Z=github.com/kagent-dev/kagent/go/api/openshell/gen/inferencev1\xa2\x02\x03OIX\xaa\x02\x16Openshell.Inference.V1\xca\x02\x16Openshell\\Inference\\V1\xe2\x02\"Openshell\\Inference\\V1\\GPBMetadata\xea\x02\x18Openshell::Inference::V1b\x06proto3" var ( file_inference_proto_rawDescOnce sync.Once diff --git a/go/api/openshell/gen/inferencev1/inference_grpc.pb.go b/go/api/openshell/gen/inferencev1/inference_grpc.pb.go index cdbd63ee57..ff9eb20f47 100644 --- a/go/api/openshell/gen/inferencev1/inference_grpc.pb.go +++ b/go/api/openshell/gen/inferencev1/inference_grpc.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v3.15.8 +// - protoc (unknown) // source: inference.proto package inferencev1 diff --git a/go/api/openshell/gen/openshelltestv1/test.pb.go b/go/api/openshell/gen/openshelltestv1/test.pb.go index 7551c9fc5e..685d3e88ab 100644 --- a/go/api/openshell/gen/openshelltestv1/test.pb.go +++ b/go/api/openshell/gen/openshelltestv1/test.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v3.15.8 +// protoc (unknown) // source: test.proto package openshelltestv1 @@ -94,7 +94,8 @@ const file_test_proto_rawDesc = "" + "\rObjectForTest\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x14\n" + - "\x05count\x18\x03 \x01(\rR\x05countb\x06proto3" + "\x05count\x18\x03 \x01(\rR\x05countB\xcb\x01\n" + + "\x15com.openshell.test.v1B\tTestProtoP\x01ZAgithub.com/kagent-dev/kagent/go/api/openshell/gen/openshelltestv1\xa2\x02\x03OTX\xaa\x02\x11Openshell.Test.V1\xca\x02\x11Openshell\\Test\\V1\xe2\x02\x1dOpenshell\\Test\\V1\\GPBMetadata\xea\x02\x13Openshell::Test::V1b\x06proto3" var ( file_test_proto_rawDescOnce sync.Once diff --git a/go/api/openshell/gen/openshellv1/openshell.pb.go b/go/api/openshell/gen/openshellv1/openshell.pb.go index f0a7f70367..67abdf8e3f 100644 --- a/go/api/openshell/gen/openshellv1/openshell.pb.go +++ b/go/api/openshell/gen/openshellv1/openshell.pb.go @@ -4,17 +4,17 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v3.15.8 +// protoc (unknown) // source: openshell.proto package openshellv1 import ( - _struct "github.com/golang/protobuf/ptypes/struct" datamodelv1 "github.com/kagent-dev/kagent/go/api/openshell/gen/datamodelv1" sandboxv1 "github.com/kagent-dev/kagent/go/api/openshell/gen/sandboxv1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + structpb "google.golang.org/protobuf/types/known/structpb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -89,6 +89,71 @@ func (SandboxPhase) EnumDescriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{0} } +// Stable provider profile categories used by clients for grouping and filtering. +type ProviderProfileCategory int32 + +const ( + ProviderProfileCategory_PROVIDER_PROFILE_CATEGORY_UNSPECIFIED ProviderProfileCategory = 0 + ProviderProfileCategory_PROVIDER_PROFILE_CATEGORY_OTHER ProviderProfileCategory = 1 + ProviderProfileCategory_PROVIDER_PROFILE_CATEGORY_INFERENCE ProviderProfileCategory = 2 + ProviderProfileCategory_PROVIDER_PROFILE_CATEGORY_AGENT ProviderProfileCategory = 3 + ProviderProfileCategory_PROVIDER_PROFILE_CATEGORY_SOURCE_CONTROL ProviderProfileCategory = 4 + ProviderProfileCategory_PROVIDER_PROFILE_CATEGORY_MESSAGING ProviderProfileCategory = 5 + ProviderProfileCategory_PROVIDER_PROFILE_CATEGORY_DATA ProviderProfileCategory = 6 + ProviderProfileCategory_PROVIDER_PROFILE_CATEGORY_KNOWLEDGE ProviderProfileCategory = 7 +) + +// Enum value maps for ProviderProfileCategory. +var ( + ProviderProfileCategory_name = map[int32]string{ + 0: "PROVIDER_PROFILE_CATEGORY_UNSPECIFIED", + 1: "PROVIDER_PROFILE_CATEGORY_OTHER", + 2: "PROVIDER_PROFILE_CATEGORY_INFERENCE", + 3: "PROVIDER_PROFILE_CATEGORY_AGENT", + 4: "PROVIDER_PROFILE_CATEGORY_SOURCE_CONTROL", + 5: "PROVIDER_PROFILE_CATEGORY_MESSAGING", + 6: "PROVIDER_PROFILE_CATEGORY_DATA", + 7: "PROVIDER_PROFILE_CATEGORY_KNOWLEDGE", + } + ProviderProfileCategory_value = map[string]int32{ + "PROVIDER_PROFILE_CATEGORY_UNSPECIFIED": 0, + "PROVIDER_PROFILE_CATEGORY_OTHER": 1, + "PROVIDER_PROFILE_CATEGORY_INFERENCE": 2, + "PROVIDER_PROFILE_CATEGORY_AGENT": 3, + "PROVIDER_PROFILE_CATEGORY_SOURCE_CONTROL": 4, + "PROVIDER_PROFILE_CATEGORY_MESSAGING": 5, + "PROVIDER_PROFILE_CATEGORY_DATA": 6, + "PROVIDER_PROFILE_CATEGORY_KNOWLEDGE": 7, + } +) + +func (x ProviderProfileCategory) Enum() *ProviderProfileCategory { + p := new(ProviderProfileCategory) + *p = x + return p +} + +func (x ProviderProfileCategory) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ProviderProfileCategory) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[1].Descriptor() +} + +func (ProviderProfileCategory) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[1] +} + +func (x ProviderProfileCategory) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ProviderProfileCategory.Descriptor instead. +func (ProviderProfileCategory) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{1} +} + // Policy load status. type PolicyStatus int32 @@ -133,11 +198,11 @@ func (x PolicyStatus) String() string { } func (PolicyStatus) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[1].Descriptor() + return file_openshell_proto_enumTypes[2].Descriptor() } func (PolicyStatus) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[1] + return &file_openshell_proto_enumTypes[2] } func (x PolicyStatus) Number() protoreflect.EnumNumber { @@ -146,7 +211,7 @@ func (x PolicyStatus) Number() protoreflect.EnumNumber { // Deprecated: Use PolicyStatus.Descriptor instead. func (PolicyStatus) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{1} + return file_openshell_proto_rawDescGZIP(), []int{2} } // Service status enum. @@ -186,11 +251,11 @@ func (x ServiceStatus) String() string { } func (ServiceStatus) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[2].Descriptor() + return file_openshell_proto_enumTypes[3].Descriptor() } func (ServiceStatus) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[2] + return &file_openshell_proto_enumTypes[3] } func (x ServiceStatus) Number() protoreflect.EnumNumber { @@ -199,7 +264,7 @@ func (x ServiceStatus) Number() protoreflect.EnumNumber { // Deprecated: Use ServiceStatus.Descriptor instead. func (ServiceStatus) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{2} + return file_openshell_proto_rawDescGZIP(), []int{3} } // Health check request. @@ -397,7 +462,11 @@ type SandboxSpec struct { // Provider names to attach to this sandbox. Providers []string `protobuf:"bytes,8,rep,name=providers,proto3" json:"providers,omitempty"` // Request NVIDIA GPU resources for this sandbox. - Gpu bool `protobuf:"varint,9,opt,name=gpu,proto3" json:"gpu,omitempty"` + Gpu bool `protobuf:"varint,9,opt,name=gpu,proto3" json:"gpu,omitempty"` + // Optional PCI BDF address (e.g. "0000:2d:00.0") or device index + // (e.g. "0", "1"). When empty with gpu=true, the driver assigns the + // first available GPU. + GpuDevice string `protobuf:"bytes,10,opt,name=gpu_device,json=gpuDevice,proto3" json:"gpu_device,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -474,6 +543,13 @@ func (x *SandboxSpec) GetGpu() bool { return false } +func (x *SandboxSpec) GetGpuDevice() string { + if x != nil { + return x.GpuDevice + } + return "" +} + // Public sandbox template mapped onto compute-driver template inputs. type SandboxTemplate struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -490,11 +566,17 @@ type SandboxTemplate struct { // Additional environment variables injected by the template. Environment map[string]string `protobuf:"bytes,6,rep,name=environment,proto3" json:"environment,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // Platform-specific compute resource requirements and limits. - Resources *_struct.Struct `protobuf:"bytes,7,opt,name=resources,proto3" json:"resources,omitempty"` + Resources *structpb.Struct `protobuf:"bytes,7,opt,name=resources,proto3" json:"resources,omitempty"` // Optional platform-specific volume claim templates. - VolumeClaimTemplates *_struct.Struct `protobuf:"bytes,9,opt,name=volume_claim_templates,json=volumeClaimTemplates,proto3" json:"volume_claim_templates,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + VolumeClaimTemplates *structpb.Struct `protobuf:"bytes,9,opt,name=volume_claim_templates,json=volumeClaimTemplates,proto3" json:"volume_claim_templates,omitempty"` + // Enable Kubernetes user namespace isolation (hostUsers: false). + // When true, container UID 0 maps to a non-root host UID and capabilities + // become namespaced. Requires Kubernetes 1.33+ with user namespace support + // available (beta through 1.35, GA in 1.36+) and a supporting runtime. + // When unset, the cluster-wide default is used. + UserNamespaces *bool `protobuf:"varint,10,opt,name=user_namespaces,json=userNamespaces,proto3,oneof" json:"user_namespaces,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SandboxTemplate) Reset() { @@ -569,20 +651,27 @@ func (x *SandboxTemplate) GetEnvironment() map[string]string { return nil } -func (x *SandboxTemplate) GetResources() *_struct.Struct { +func (x *SandboxTemplate) GetResources() *structpb.Struct { if x != nil { return x.Resources } return nil } -func (x *SandboxTemplate) GetVolumeClaimTemplates() *_struct.Struct { +func (x *SandboxTemplate) GetVolumeClaimTemplates() *structpb.Struct { if x != nil { return x.VolumeClaimTemplates } return nil } +func (x *SandboxTemplate) GetUserNamespaces() bool { + if x != nil && x.UserNamespaces != nil { + return *x.UserNamespaces + } + return false +} + // User-facing sandbox status derived by the gateway from compute-driver observations. // // Lifecycle summary is exposed separately as `Sandbox.phase`. Public status does @@ -1012,29 +1101,29 @@ func (x *ListSandboxesRequest) GetLabelSelector() string { return "" } -// Delete sandbox request. -type DeleteSandboxRequest struct { +// List providers attached to a sandbox request. +type ListSandboxProvidersRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Sandbox name (canonical lookup key). - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + SandboxName string `protobuf:"bytes,1,opt,name=sandbox_name,json=sandboxName,proto3" json:"sandbox_name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *DeleteSandboxRequest) Reset() { - *x = DeleteSandboxRequest{} +func (x *ListSandboxProvidersRequest) Reset() { + *x = ListSandboxProvidersRequest{} mi := &file_openshell_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *DeleteSandboxRequest) String() string { +func (x *ListSandboxProvidersRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*DeleteSandboxRequest) ProtoMessage() {} +func (*ListSandboxProvidersRequest) ProtoMessage() {} -func (x *DeleteSandboxRequest) ProtoReflect() protoreflect.Message { +func (x *ListSandboxProvidersRequest) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1046,40 +1135,48 @@ func (x *DeleteSandboxRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use DeleteSandboxRequest.ProtoReflect.Descriptor instead. -func (*DeleteSandboxRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use ListSandboxProvidersRequest.ProtoReflect.Descriptor instead. +func (*ListSandboxProvidersRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{11} } -func (x *DeleteSandboxRequest) GetName() string { +func (x *ListSandboxProvidersRequest) GetSandboxName() string { if x != nil { - return x.Name + return x.SandboxName } return "" } -// Sandbox response. -type SandboxResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SandboxResponse) Reset() { - *x = SandboxResponse{} +// Attach provider to sandbox request. +type AttachSandboxProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox name (canonical lookup key). + SandboxName string `protobuf:"bytes,1,opt,name=sandbox_name,json=sandboxName,proto3" json:"sandbox_name,omitempty"` + // Provider name to attach. + ProviderName string `protobuf:"bytes,2,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` + // Expected resource version for optimistic concurrency control. + // If 0, the server uses the current version (backward compatibility). + // If non-zero, the server validates that the sandbox's current resource_version + // matches this value before applying the mutation, returning ABORTED on mismatch. + ExpectedResourceVersion uint64 `protobuf:"varint,3,opt,name=expected_resource_version,json=expectedResourceVersion,proto3" json:"expected_resource_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AttachSandboxProviderRequest) Reset() { + *x = AttachSandboxProviderRequest{} mi := &file_openshell_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *SandboxResponse) String() string { +func (x *AttachSandboxProviderRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SandboxResponse) ProtoMessage() {} +func (*AttachSandboxProviderRequest) ProtoMessage() {} -func (x *SandboxResponse) ProtoReflect() protoreflect.Message { +func (x *AttachSandboxProviderRequest) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1091,40 +1188,62 @@ func (x *SandboxResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SandboxResponse.ProtoReflect.Descriptor instead. -func (*SandboxResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use AttachSandboxProviderRequest.ProtoReflect.Descriptor instead. +func (*AttachSandboxProviderRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{12} } -func (x *SandboxResponse) GetSandbox() *Sandbox { +func (x *AttachSandboxProviderRequest) GetSandboxName() string { if x != nil { - return x.Sandbox + return x.SandboxName } - return nil + return "" } -// List sandboxes response. -type ListSandboxesResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Sandboxes []*Sandbox `protobuf:"bytes,1,rep,name=sandboxes,proto3" json:"sandboxes,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *AttachSandboxProviderRequest) GetProviderName() string { + if x != nil { + return x.ProviderName + } + return "" } -func (x *ListSandboxesResponse) Reset() { - *x = ListSandboxesResponse{} +func (x *AttachSandboxProviderRequest) GetExpectedResourceVersion() uint64 { + if x != nil { + return x.ExpectedResourceVersion + } + return 0 +} + +// Detach provider from sandbox request. +type DetachSandboxProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox name (canonical lookup key). + SandboxName string `protobuf:"bytes,1,opt,name=sandbox_name,json=sandboxName,proto3" json:"sandbox_name,omitempty"` + // Provider name to detach. + ProviderName string `protobuf:"bytes,2,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` + // Expected resource version for optimistic concurrency control. + // If 0, the server uses the current version (backward compatibility). + // If non-zero, the server validates that the sandbox's current resource_version + // matches this value before applying the mutation, returning ABORTED on mismatch. + ExpectedResourceVersion uint64 `protobuf:"varint,3,opt,name=expected_resource_version,json=expectedResourceVersion,proto3" json:"expected_resource_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DetachSandboxProviderRequest) Reset() { + *x = DetachSandboxProviderRequest{} mi := &file_openshell_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ListSandboxesResponse) String() string { +func (x *DetachSandboxProviderRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ListSandboxesResponse) ProtoMessage() {} +func (*DetachSandboxProviderRequest) ProtoMessage() {} -func (x *ListSandboxesResponse) ProtoReflect() protoreflect.Message { +func (x *DetachSandboxProviderRequest) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1136,40 +1255,55 @@ func (x *ListSandboxesResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ListSandboxesResponse.ProtoReflect.Descriptor instead. -func (*ListSandboxesResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use DetachSandboxProviderRequest.ProtoReflect.Descriptor instead. +func (*DetachSandboxProviderRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{13} } -func (x *ListSandboxesResponse) GetSandboxes() []*Sandbox { +func (x *DetachSandboxProviderRequest) GetSandboxName() string { if x != nil { - return x.Sandboxes + return x.SandboxName } - return nil + return "" } -// Delete sandbox response. -type DeleteSandboxResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"` +func (x *DetachSandboxProviderRequest) GetProviderName() string { + if x != nil { + return x.ProviderName + } + return "" +} + +func (x *DetachSandboxProviderRequest) GetExpectedResourceVersion() uint64 { + if x != nil { + return x.ExpectedResourceVersion + } + return 0 +} + +// Delete sandbox request. +type DeleteSandboxRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox name (canonical lookup key). + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *DeleteSandboxResponse) Reset() { - *x = DeleteSandboxResponse{} +func (x *DeleteSandboxRequest) Reset() { + *x = DeleteSandboxRequest{} mi := &file_openshell_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *DeleteSandboxResponse) String() string { +func (x *DeleteSandboxRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*DeleteSandboxResponse) ProtoMessage() {} +func (*DeleteSandboxRequest) ProtoMessage() {} -func (x *DeleteSandboxResponse) ProtoReflect() protoreflect.Message { +func (x *DeleteSandboxRequest) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1181,41 +1315,40 @@ func (x *DeleteSandboxResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use DeleteSandboxResponse.ProtoReflect.Descriptor instead. -func (*DeleteSandboxResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use DeleteSandboxRequest.ProtoReflect.Descriptor instead. +func (*DeleteSandboxRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{14} } -func (x *DeleteSandboxResponse) GetDeleted() bool { +func (x *DeleteSandboxRequest) GetName() string { if x != nil { - return x.Deleted + return x.Name } - return false + return "" } -// Create SSH session request. -type CreateSshSessionRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox id. - SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` +// Sandbox response. +type SandboxResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *CreateSshSessionRequest) Reset() { - *x = CreateSshSessionRequest{} +func (x *SandboxResponse) Reset() { + *x = SandboxResponse{} mi := &file_openshell_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *CreateSshSessionRequest) String() string { +func (x *SandboxResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*CreateSshSessionRequest) ProtoMessage() {} +func (*SandboxResponse) ProtoMessage() {} -func (x *CreateSshSessionRequest) ProtoReflect() protoreflect.Message { +func (x *SandboxResponse) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1227,67 +1360,40 @@ func (x *CreateSshSessionRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use CreateSshSessionRequest.ProtoReflect.Descriptor instead. -func (*CreateSshSessionRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use SandboxResponse.ProtoReflect.Descriptor instead. +func (*SandboxResponse) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{15} } -func (x *CreateSshSessionRequest) GetSandboxId() string { +func (x *SandboxResponse) GetSandbox() *Sandbox { if x != nil { - return x.SandboxId + return x.Sandbox } - return "" + return nil } -// Create SSH session response. -// -// Fields are interpolated into an SSH `ProxyCommand` string that OpenSSH -// executes through `/bin/sh -c` on the caller's workstation. Servers MUST -// uphold the charset contract below; clients MUST reject responses that -// violate it. The client's own escaping provides defense-in-depth, but -// narrow charsets close injection vectors at the trust boundary. -type CreateSshSessionResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox id. [A-Za-z0-9._-]{1,128}. - SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` - // Session token for the gateway tunnel. URL-safe ASCII - // ([A-Za-z0-9._~+/=-]) up to 4096 bytes. No shell metacharacters or - // whitespace. - Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` - // Gateway host for SSH proxy connection. IPv4 address, bracketed IPv6 - // address, or DNS hostname (Punycode-encoded for IDN). Alphanumeric plus - // `.-:[]` only, up to 253 bytes. - GatewayHost string `protobuf:"bytes,3,opt,name=gateway_host,json=gatewayHost,proto3" json:"gateway_host,omitempty"` - // Gateway port for SSH proxy connection. Must be in range 1..=65535. - GatewayPort uint32 `protobuf:"varint,4,opt,name=gateway_port,json=gatewayPort,proto3" json:"gateway_port,omitempty"` - // Gateway scheme. Must be exactly "http" or "https". - GatewayScheme string `protobuf:"bytes,5,opt,name=gateway_scheme,json=gatewayScheme,proto3" json:"gateway_scheme,omitempty"` - // HTTP path for the CONNECT/upgrade endpoint. Must begin with `/`. RFC - // 3986 path charset only ([A-Za-z0-9._~!$&'()*+,;=:@/-] plus %HH). - // Must not contain `?`, `#`, whitespace, backtick, or backslash. - ConnectPath string `protobuf:"bytes,6,opt,name=connect_path,json=connectPath,proto3" json:"connect_path,omitempty"` - // Optional host key fingerprint. If non-empty, [A-Za-z0-9:+/=-] only. - HostKeyFingerprint string `protobuf:"bytes,7,opt,name=host_key_fingerprint,json=hostKeyFingerprint,proto3" json:"host_key_fingerprint,omitempty"` - // Expiry timestamp in milliseconds since epoch. 0 means no expiry. - ExpiresAtMs int64 `protobuf:"varint,8,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` +// List sandboxes response. +type ListSandboxesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sandboxes []*Sandbox `protobuf:"bytes,1,rep,name=sandboxes,proto3" json:"sandboxes,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *CreateSshSessionResponse) Reset() { - *x = CreateSshSessionResponse{} +func (x *ListSandboxesResponse) Reset() { + *x = ListSandboxesResponse{} mi := &file_openshell_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *CreateSshSessionResponse) String() string { +func (x *ListSandboxesResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*CreateSshSessionResponse) ProtoMessage() {} +func (*ListSandboxesResponse) ProtoMessage() {} -func (x *CreateSshSessionResponse) ProtoReflect() protoreflect.Message { +func (x *ListSandboxesResponse) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1299,90 +1405,40 @@ func (x *CreateSshSessionResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use CreateSshSessionResponse.ProtoReflect.Descriptor instead. -func (*CreateSshSessionResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use ListSandboxesResponse.ProtoReflect.Descriptor instead. +func (*ListSandboxesResponse) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{16} } -func (x *CreateSshSessionResponse) GetSandboxId() string { +func (x *ListSandboxesResponse) GetSandboxes() []*Sandbox { if x != nil { - return x.SandboxId + return x.Sandboxes } - return "" + return nil } -func (x *CreateSshSessionResponse) GetToken() string { - if x != nil { - return x.Token - } - return "" +// List providers attached to a sandbox response. +type ListSandboxProvidersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Providers []*datamodelv1.Provider `protobuf:"bytes,1,rep,name=providers,proto3" json:"providers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *CreateSshSessionResponse) GetGatewayHost() string { - if x != nil { - return x.GatewayHost - } - return "" +func (x *ListSandboxProvidersResponse) Reset() { + *x = ListSandboxProvidersResponse{} + mi := &file_openshell_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *CreateSshSessionResponse) GetGatewayPort() uint32 { - if x != nil { - return x.GatewayPort - } - return 0 +func (x *ListSandboxProvidersResponse) String() string { + return protoimpl.X.MessageStringOf(x) } -func (x *CreateSshSessionResponse) GetGatewayScheme() string { - if x != nil { - return x.GatewayScheme - } - return "" -} - -func (x *CreateSshSessionResponse) GetConnectPath() string { - if x != nil { - return x.ConnectPath - } - return "" -} - -func (x *CreateSshSessionResponse) GetHostKeyFingerprint() string { - if x != nil { - return x.HostKeyFingerprint - } - return "" -} - -func (x *CreateSshSessionResponse) GetExpiresAtMs() int64 { - if x != nil { - return x.ExpiresAtMs - } - return 0 -} - -// Revoke SSH session request. -type RevokeSshSessionRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Session token to revoke. - Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *RevokeSshSessionRequest) Reset() { - *x = RevokeSshSessionRequest{} - mi := &file_openshell_proto_msgTypes[17] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} +func (*ListSandboxProvidersResponse) ProtoMessage() {} -func (x *RevokeSshSessionRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RevokeSshSessionRequest) ProtoMessage() {} - -func (x *RevokeSshSessionRequest) ProtoReflect() protoreflect.Message { +func (x *ListSandboxProvidersResponse) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1394,41 +1450,42 @@ func (x *RevokeSshSessionRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use RevokeSshSessionRequest.ProtoReflect.Descriptor instead. -func (*RevokeSshSessionRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use ListSandboxProvidersResponse.ProtoReflect.Descriptor instead. +func (*ListSandboxProvidersResponse) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{17} } -func (x *RevokeSshSessionRequest) GetToken() string { +func (x *ListSandboxProvidersResponse) GetProviders() []*datamodelv1.Provider { if x != nil { - return x.Token + return x.Providers } - return "" + return nil } -// Revoke SSH session response. -type RevokeSshSessionResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // True when a session was revoked. - Revoked bool `protobuf:"varint,1,opt,name=revoked,proto3" json:"revoked,omitempty"` +// Attach provider to sandbox response. +type AttachSandboxProviderResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // True when the provider was newly attached. False means it was already attached. + Attached bool `protobuf:"varint,2,opt,name=attached,proto3" json:"attached,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *RevokeSshSessionResponse) Reset() { - *x = RevokeSshSessionResponse{} +func (x *AttachSandboxProviderResponse) Reset() { + *x = AttachSandboxProviderResponse{} mi := &file_openshell_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *RevokeSshSessionResponse) String() string { +func (x *AttachSandboxProviderResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*RevokeSshSessionResponse) ProtoMessage() {} +func (*AttachSandboxProviderResponse) ProtoMessage() {} -func (x *RevokeSshSessionResponse) ProtoReflect() protoreflect.Message { +func (x *AttachSandboxProviderResponse) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1440,53 +1497,49 @@ func (x *RevokeSshSessionResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use RevokeSshSessionResponse.ProtoReflect.Descriptor instead. -func (*RevokeSshSessionResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use AttachSandboxProviderResponse.ProtoReflect.Descriptor instead. +func (*AttachSandboxProviderResponse) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{18} } -func (x *RevokeSshSessionResponse) GetRevoked() bool { +func (x *AttachSandboxProviderResponse) GetSandbox() *Sandbox { if x != nil { - return x.Revoked + return x.Sandbox + } + return nil +} + +func (x *AttachSandboxProviderResponse) GetAttached() bool { + if x != nil { + return x.Attached } return false } -// Execute command request. -type ExecSandboxRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox id. - SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` - // Command and arguments. - Command []string `protobuf:"bytes,2,rep,name=command,proto3" json:"command,omitempty"` - // Optional working directory. - Workdir string `protobuf:"bytes,3,opt,name=workdir,proto3" json:"workdir,omitempty"` - // Optional environment overrides. - Environment map[string]string `protobuf:"bytes,4,rep,name=environment,proto3" json:"environment,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - // Optional timeout in seconds. 0 means no timeout. - TimeoutSeconds uint32 `protobuf:"varint,5,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"` - // Optional stdin payload passed to the command. - Stdin []byte `protobuf:"bytes,6,opt,name=stdin,proto3" json:"stdin,omitempty"` - // Request a pseudo-terminal for the remote command. - Tty bool `protobuf:"varint,7,opt,name=tty,proto3" json:"tty,omitempty"` +// Detach provider from sandbox response. +type DetachSandboxProviderResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // True when the provider was removed. False means it was not attached. + Detached bool `protobuf:"varint,2,opt,name=detached,proto3" json:"detached,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ExecSandboxRequest) Reset() { - *x = ExecSandboxRequest{} +func (x *DetachSandboxProviderResponse) Reset() { + *x = DetachSandboxProviderResponse{} mi := &file_openshell_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ExecSandboxRequest) String() string { +func (x *DetachSandboxProviderResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ExecSandboxRequest) ProtoMessage() {} +func (*DetachSandboxProviderResponse) ProtoMessage() {} -func (x *ExecSandboxRequest) ProtoReflect() protoreflect.Message { +func (x *DetachSandboxProviderResponse) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1498,82 +1551,47 @@ func (x *ExecSandboxRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ExecSandboxRequest.ProtoReflect.Descriptor instead. -func (*ExecSandboxRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use DetachSandboxProviderResponse.ProtoReflect.Descriptor instead. +func (*DetachSandboxProviderResponse) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{19} } -func (x *ExecSandboxRequest) GetSandboxId() string { - if x != nil { - return x.SandboxId - } - return "" -} - -func (x *ExecSandboxRequest) GetCommand() []string { - if x != nil { - return x.Command - } - return nil -} - -func (x *ExecSandboxRequest) GetWorkdir() string { - if x != nil { - return x.Workdir - } - return "" -} - -func (x *ExecSandboxRequest) GetEnvironment() map[string]string { - if x != nil { - return x.Environment - } - return nil -} - -func (x *ExecSandboxRequest) GetTimeoutSeconds() uint32 { - if x != nil { - return x.TimeoutSeconds - } - return 0 -} - -func (x *ExecSandboxRequest) GetStdin() []byte { +func (x *DetachSandboxProviderResponse) GetSandbox() *Sandbox { if x != nil { - return x.Stdin + return x.Sandbox } return nil } -func (x *ExecSandboxRequest) GetTty() bool { +func (x *DetachSandboxProviderResponse) GetDetached() bool { if x != nil { - return x.Tty + return x.Detached } return false } -// One stdout chunk from a sandbox exec. -type ExecSandboxStdout struct { +// Delete sandbox response. +type DeleteSandboxResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ExecSandboxStdout) Reset() { - *x = ExecSandboxStdout{} +func (x *DeleteSandboxResponse) Reset() { + *x = DeleteSandboxResponse{} mi := &file_openshell_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ExecSandboxStdout) String() string { +func (x *DeleteSandboxResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ExecSandboxStdout) ProtoMessage() {} +func (*DeleteSandboxResponse) ProtoMessage() {} -func (x *ExecSandboxStdout) ProtoReflect() protoreflect.Message { +func (x *DeleteSandboxResponse) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1585,40 +1603,41 @@ func (x *ExecSandboxStdout) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ExecSandboxStdout.ProtoReflect.Descriptor instead. -func (*ExecSandboxStdout) Descriptor() ([]byte, []int) { +// Deprecated: Use DeleteSandboxResponse.ProtoReflect.Descriptor instead. +func (*DeleteSandboxResponse) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{20} } -func (x *ExecSandboxStdout) GetData() []byte { +func (x *DeleteSandboxResponse) GetDeleted() bool { if x != nil { - return x.Data + return x.Deleted } - return nil + return false } -// One stderr chunk from a sandbox exec. -type ExecSandboxStderr struct { - state protoimpl.MessageState `protogen:"open.v1"` - Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` +// Create SSH session request. +type CreateSshSessionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox id. + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ExecSandboxStderr) Reset() { - *x = ExecSandboxStderr{} +func (x *CreateSshSessionRequest) Reset() { + *x = CreateSshSessionRequest{} mi := &file_openshell_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ExecSandboxStderr) String() string { +func (x *CreateSshSessionRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ExecSandboxStderr) ProtoMessage() {} +func (*CreateSshSessionRequest) ProtoMessage() {} -func (x *ExecSandboxStderr) ProtoReflect() protoreflect.Message { +func (x *CreateSshSessionRequest) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1630,40 +1649,63 @@ func (x *ExecSandboxStderr) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ExecSandboxStderr.ProtoReflect.Descriptor instead. -func (*ExecSandboxStderr) Descriptor() ([]byte, []int) { +// Deprecated: Use CreateSshSessionRequest.ProtoReflect.Descriptor instead. +func (*CreateSshSessionRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{21} } -func (x *ExecSandboxStderr) GetData() []byte { +func (x *CreateSshSessionRequest) GetSandboxId() string { if x != nil { - return x.Data + return x.SandboxId } - return nil + return "" } -// Final exit status for a sandbox exec. -type ExecSandboxExit struct { - state protoimpl.MessageState `protogen:"open.v1"` - ExitCode int32 `protobuf:"varint,1,opt,name=exit_code,json=exitCode,proto3" json:"exit_code,omitempty"` +// Create SSH session response. +// +// Fields are interpolated into an SSH `ProxyCommand` string that OpenSSH +// executes through `/bin/sh -c` on the caller's workstation. Servers MUST +// uphold the charset contract below; clients MUST reject responses that +// violate it. The client's own escaping provides defense-in-depth, but +// narrow charsets close injection vectors at the trust boundary. +type CreateSshSessionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox id. [A-Za-z0-9._-]{1,128}. + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Session token for the gateway tunnel. URL-safe ASCII + // ([A-Za-z0-9._~+/=-]) up to 4096 bytes. No shell metacharacters or + // whitespace. + Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` + // Gateway host for SSH proxy connection. IPv4 address, bracketed IPv6 + // address, or DNS hostname (Punycode-encoded for IDN). Alphanumeric plus + // `.-:[]` only, up to 253 bytes. + GatewayHost string `protobuf:"bytes,3,opt,name=gateway_host,json=gatewayHost,proto3" json:"gateway_host,omitempty"` + // Gateway port for SSH proxy connection. Must be in range 1..=65535. + GatewayPort uint32 `protobuf:"varint,4,opt,name=gateway_port,json=gatewayPort,proto3" json:"gateway_port,omitempty"` + // Gateway scheme. Must be exactly "http" or "https". + GatewayScheme string `protobuf:"bytes,5,opt,name=gateway_scheme,json=gatewayScheme,proto3" json:"gateway_scheme,omitempty"` + // Optional host key fingerprint. If non-empty, [A-Za-z0-9:+/=-] only. + HostKeyFingerprint string `protobuf:"bytes,7,opt,name=host_key_fingerprint,json=hostKeyFingerprint,proto3" json:"host_key_fingerprint,omitempty"` + // Expiry timestamp in milliseconds since epoch. 0 means no expiry. + ExpiresAtMs int64 `protobuf:"varint,8,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ExecSandboxExit) Reset() { - *x = ExecSandboxExit{} +func (x *CreateSshSessionResponse) Reset() { + *x = CreateSshSessionResponse{} mi := &file_openshell_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ExecSandboxExit) String() string { +func (x *CreateSshSessionResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ExecSandboxExit) ProtoMessage() {} +func (*CreateSshSessionResponse) ProtoMessage() {} -func (x *ExecSandboxExit) ProtoReflect() protoreflect.Message { +func (x *CreateSshSessionResponse) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1675,150 +1717,90 @@ func (x *ExecSandboxExit) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ExecSandboxExit.ProtoReflect.Descriptor instead. -func (*ExecSandboxExit) Descriptor() ([]byte, []int) { +// Deprecated: Use CreateSshSessionResponse.ProtoReflect.Descriptor instead. +func (*CreateSshSessionResponse) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{22} } -func (x *ExecSandboxExit) GetExitCode() int32 { +func (x *CreateSshSessionResponse) GetSandboxId() string { if x != nil { - return x.ExitCode + return x.SandboxId } - return 0 -} - -// One event in a sandbox exec stream. -type ExecSandboxEvent struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to Payload: - // - // *ExecSandboxEvent_Stdout - // *ExecSandboxEvent_Stderr - // *ExecSandboxEvent_Exit - Payload isExecSandboxEvent_Payload `protobuf_oneof:"payload"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + return "" } -func (x *ExecSandboxEvent) Reset() { - *x = ExecSandboxEvent{} - mi := &file_openshell_proto_msgTypes[23] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ExecSandboxEvent) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ExecSandboxEvent) ProtoMessage() {} - -func (x *ExecSandboxEvent) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[23] +func (x *CreateSshSessionResponse) GetToken() string { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.Token } - return mi.MessageOf(x) -} - -// Deprecated: Use ExecSandboxEvent.ProtoReflect.Descriptor instead. -func (*ExecSandboxEvent) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{23} + return "" } -func (x *ExecSandboxEvent) GetPayload() isExecSandboxEvent_Payload { +func (x *CreateSshSessionResponse) GetGatewayHost() string { if x != nil { - return x.Payload + return x.GatewayHost } - return nil + return "" } -func (x *ExecSandboxEvent) GetStdout() *ExecSandboxStdout { +func (x *CreateSshSessionResponse) GetGatewayPort() uint32 { if x != nil { - if x, ok := x.Payload.(*ExecSandboxEvent_Stdout); ok { - return x.Stdout - } + return x.GatewayPort } - return nil + return 0 } -func (x *ExecSandboxEvent) GetStderr() *ExecSandboxStderr { +func (x *CreateSshSessionResponse) GetGatewayScheme() string { if x != nil { - if x, ok := x.Payload.(*ExecSandboxEvent_Stderr); ok { - return x.Stderr - } + return x.GatewayScheme } - return nil + return "" } -func (x *ExecSandboxEvent) GetExit() *ExecSandboxExit { +func (x *CreateSshSessionResponse) GetHostKeyFingerprint() string { if x != nil { - if x, ok := x.Payload.(*ExecSandboxEvent_Exit); ok { - return x.Exit - } + return x.HostKeyFingerprint } - return nil -} - -type isExecSandboxEvent_Payload interface { - isExecSandboxEvent_Payload() -} - -type ExecSandboxEvent_Stdout struct { - Stdout *ExecSandboxStdout `protobuf:"bytes,1,opt,name=stdout,proto3,oneof"` -} - -type ExecSandboxEvent_Stderr struct { - Stderr *ExecSandboxStderr `protobuf:"bytes,2,opt,name=stderr,proto3,oneof"` + return "" } -type ExecSandboxEvent_Exit struct { - Exit *ExecSandboxExit `protobuf:"bytes,3,opt,name=exit,proto3,oneof"` +func (x *CreateSshSessionResponse) GetExpiresAtMs() int64 { + if x != nil { + return x.ExpiresAtMs + } + return 0 } -func (*ExecSandboxEvent_Stdout) isExecSandboxEvent_Payload() {} - -func (*ExecSandboxEvent_Stderr) isExecSandboxEvent_Payload() {} - -func (*ExecSandboxEvent_Exit) isExecSandboxEvent_Payload() {} - -// SSH session record stored in persistence. -type SshSession struct { +// Request to expose an HTTP service running inside a sandbox. +type ExposeServiceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Kubernetes-style metadata (id, name, labels, timestamps, resource version). - Metadata *datamodelv1.ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` - // Sandbox id. - SandboxId string `protobuf:"bytes,2,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` - // Session token. - Token string `protobuf:"bytes,3,opt,name=token,proto3" json:"token,omitempty"` - // Expiry timestamp in milliseconds since epoch. 0 means no expiry - // (backward-compatible default for sessions created before this field existed). - ExpiresAtMs int64 `protobuf:"varint,4,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` - // Revoked flag. - Revoked bool `protobuf:"varint,5,opt,name=revoked,proto3" json:"revoked,omitempty"` + // Sandbox name. + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Service name within the sandbox. + Service string `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"` + // Loopback TCP port inside the sandbox. + TargetPort uint32 `protobuf:"varint,3,opt,name=target_port,json=targetPort,proto3" json:"target_port,omitempty"` + // Whether to print/use the browser-facing service URL. + Domain bool `protobuf:"varint,4,opt,name=domain,proto3" json:"domain,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *SshSession) Reset() { - *x = SshSession{} - mi := &file_openshell_proto_msgTypes[24] +func (x *ExposeServiceRequest) Reset() { + *x = ExposeServiceRequest{} + mi := &file_openshell_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *SshSession) String() string { +func (x *ExposeServiceRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SshSession) ProtoMessage() {} +func (*ExposeServiceRequest) ProtoMessage() {} -func (x *SshSession) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[24] +func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1829,89 +1811,65 @@ func (x *SshSession) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SshSession.ProtoReflect.Descriptor instead. -func (*SshSession) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{24} -} - -func (x *SshSession) GetMetadata() *datamodelv1.ObjectMeta { - if x != nil { - return x.Metadata - } - return nil +// Deprecated: Use ExposeServiceRequest.ProtoReflect.Descriptor instead. +func (*ExposeServiceRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{23} } -func (x *SshSession) GetSandboxId() string { +func (x *ExposeServiceRequest) GetSandbox() string { if x != nil { - return x.SandboxId + return x.Sandbox } return "" } -func (x *SshSession) GetToken() string { +func (x *ExposeServiceRequest) GetService() string { if x != nil { - return x.Token + return x.Service } return "" } -func (x *SshSession) GetExpiresAtMs() int64 { +func (x *ExposeServiceRequest) GetTargetPort() uint32 { if x != nil { - return x.ExpiresAtMs + return x.TargetPort } return 0 } -func (x *SshSession) GetRevoked() bool { +func (x *ExposeServiceRequest) GetDomain() bool { if x != nil { - return x.Revoked + return x.Domain } return false } -// Watch sandbox request. -type WatchSandboxRequest struct { +// Request to fetch an exposed sandbox service endpoint. +type GetServiceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox id. - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - // Stream sandbox status snapshots. - FollowStatus bool `protobuf:"varint,2,opt,name=follow_status,json=followStatus,proto3" json:"follow_status,omitempty"` - // Stream openshell-server process logs correlated to this sandbox. - FollowLogs bool `protobuf:"varint,3,opt,name=follow_logs,json=followLogs,proto3" json:"follow_logs,omitempty"` - // Stream platform events correlated to this sandbox. - FollowEvents bool `protobuf:"varint,4,opt,name=follow_events,json=followEvents,proto3" json:"follow_events,omitempty"` - // Replay the last N log lines (best-effort) before following. - LogTailLines uint32 `protobuf:"varint,5,opt,name=log_tail_lines,json=logTailLines,proto3" json:"log_tail_lines,omitempty"` - // Replay the last N platform events (best-effort) before following. - EventTail uint32 `protobuf:"varint,6,opt,name=event_tail,json=eventTail,proto3" json:"event_tail,omitempty"` - // Stop streaming once the sandbox reaches a terminal phase (READY or ERROR). - StopOnTerminal bool `protobuf:"varint,7,opt,name=stop_on_terminal,json=stopOnTerminal,proto3" json:"stop_on_terminal,omitempty"` - // Only include log lines with timestamp >= this value (milliseconds since epoch). - // 0 means no time filter. Applies to both tail replay and live streaming. - LogSinceMs int64 `protobuf:"varint,8,opt,name=log_since_ms,json=logSinceMs,proto3" json:"log_since_ms,omitempty"` - // Filter by log source (e.g. "gateway", "sandbox"). Empty means all sources. - LogSources []string `protobuf:"bytes,9,rep,name=log_sources,json=logSources,proto3" json:"log_sources,omitempty"` - // Minimum log level to include (e.g. "INFO", "WARN", "ERROR"). Empty means all levels. - LogMinLevel string `protobuf:"bytes,10,opt,name=log_min_level,json=logMinLevel,proto3" json:"log_min_level,omitempty"` + // Sandbox name. + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Service name within the sandbox. Empty selects the unnamed endpoint. + Service string `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *WatchSandboxRequest) Reset() { - *x = WatchSandboxRequest{} - mi := &file_openshell_proto_msgTypes[25] +func (x *GetServiceRequest) Reset() { + *x = GetServiceRequest{} + mi := &file_openshell_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *WatchSandboxRequest) String() string { +func (x *GetServiceRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*WatchSandboxRequest) ProtoMessage() {} +func (*GetServiceRequest) ProtoMessage() {} -func (x *WatchSandboxRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[25] +func (x *GetServiceRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1922,110 +1880,111 @@ func (x *WatchSandboxRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use WatchSandboxRequest.ProtoReflect.Descriptor instead. -func (*WatchSandboxRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{25} +// Deprecated: Use GetServiceRequest.ProtoReflect.Descriptor instead. +func (*GetServiceRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{24} } -func (x *WatchSandboxRequest) GetId() string { +func (x *GetServiceRequest) GetSandbox() string { if x != nil { - return x.Id + return x.Sandbox } return "" } -func (x *WatchSandboxRequest) GetFollowStatus() bool { +func (x *GetServiceRequest) GetService() string { if x != nil { - return x.FollowStatus + return x.Service } - return false + return "" } -func (x *WatchSandboxRequest) GetFollowLogs() bool { - if x != nil { - return x.FollowLogs - } - return false +// Request to list exposed sandbox service endpoints. +type ListServicesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional sandbox name. Empty lists endpoints for all sandboxes. + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Page size. Zero uses the server default. + Limit uint32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` + // Page offset. + Offset uint32 `protobuf:"varint,3,opt,name=offset,proto3" json:"offset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *WatchSandboxRequest) GetFollowEvents() bool { - if x != nil { - return x.FollowEvents - } - return false +func (x *ListServicesRequest) Reset() { + *x = ListServicesRequest{} + mi := &file_openshell_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *WatchSandboxRequest) GetLogTailLines() uint32 { - if x != nil { - return x.LogTailLines - } - return 0 +func (x *ListServicesRequest) String() string { + return protoimpl.X.MessageStringOf(x) } -func (x *WatchSandboxRequest) GetEventTail() uint32 { +func (*ListServicesRequest) ProtoMessage() {} + +func (x *ListServicesRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[25] if x != nil { - return x.EventTail + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return 0 + return mi.MessageOf(x) } -func (x *WatchSandboxRequest) GetStopOnTerminal() bool { - if x != nil { - return x.StopOnTerminal - } - return false +// Deprecated: Use ListServicesRequest.ProtoReflect.Descriptor instead. +func (*ListServicesRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{25} } -func (x *WatchSandboxRequest) GetLogSinceMs() int64 { +func (x *ListServicesRequest) GetSandbox() string { if x != nil { - return x.LogSinceMs + return x.Sandbox } - return 0 + return "" } -func (x *WatchSandboxRequest) GetLogSources() []string { +func (x *ListServicesRequest) GetLimit() uint32 { if x != nil { - return x.LogSources + return x.Limit } - return nil + return 0 } -func (x *WatchSandboxRequest) GetLogMinLevel() string { +func (x *ListServicesRequest) GetOffset() uint32 { if x != nil { - return x.LogMinLevel + return x.Offset } - return "" + return 0 } -// One event in a sandbox watch stream. -type SandboxStreamEvent struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to Payload: - // - // *SandboxStreamEvent_Sandbox - // *SandboxStreamEvent_Log - // *SandboxStreamEvent_Event - // *SandboxStreamEvent_Warning - // *SandboxStreamEvent_DraftPolicyUpdate - Payload isSandboxStreamEvent_Payload `protobuf_oneof:"payload"` +// Response containing exposed sandbox service endpoints. +type ListServicesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Services []*ServiceEndpointResponse `protobuf:"bytes,1,rep,name=services,proto3" json:"services,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *SandboxStreamEvent) Reset() { - *x = SandboxStreamEvent{} +func (x *ListServicesResponse) Reset() { + *x = ListServicesResponse{} mi := &file_openshell_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *SandboxStreamEvent) String() string { +func (x *ListServicesResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SandboxStreamEvent) ProtoMessage() {} +func (*ListServicesResponse) ProtoMessage() {} -func (x *SandboxStreamEvent) ProtoReflect() protoreflect.Message { +func (x *ListServicesResponse) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2037,134 +1996,2125 @@ func (x *SandboxStreamEvent) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SandboxStreamEvent.ProtoReflect.Descriptor instead. -func (*SandboxStreamEvent) Descriptor() ([]byte, []int) { +// Deprecated: Use ListServicesResponse.ProtoReflect.Descriptor instead. +func (*ListServicesResponse) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{26} } -func (x *SandboxStreamEvent) GetPayload() isSandboxStreamEvent_Payload { +func (x *ListServicesResponse) GetServices() []*ServiceEndpointResponse { if x != nil { - return x.Payload + return x.Services } return nil } -func (x *SandboxStreamEvent) GetSandbox() *Sandbox { - if x != nil { - if x, ok := x.Payload.(*SandboxStreamEvent_Sandbox); ok { - return x.Sandbox - } - } - return nil +// Request to delete an exposed sandbox service endpoint. +type DeleteServiceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox name. + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Service name within the sandbox. Empty selects the unnamed endpoint. + Service string `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteServiceRequest) Reset() { + *x = DeleteServiceRequest{} + mi := &file_openshell_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteServiceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteServiceRequest) ProtoMessage() {} + +func (x *DeleteServiceRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[27] + 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 DeleteServiceRequest.ProtoReflect.Descriptor instead. +func (*DeleteServiceRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{27} +} + +func (x *DeleteServiceRequest) GetSandbox() string { + if x != nil { + return x.Sandbox + } + return "" +} + +func (x *DeleteServiceRequest) GetService() string { + if x != nil { + return x.Service + } + return "" +} + +// Response for deleting an exposed sandbox service endpoint. +type DeleteServiceResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // True when an endpoint existed and was deleted. + Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteServiceResponse) Reset() { + *x = DeleteServiceResponse{} + mi := &file_openshell_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteServiceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteServiceResponse) ProtoMessage() {} + +func (x *DeleteServiceResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[28] + 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 DeleteServiceResponse.ProtoReflect.Descriptor instead. +func (*DeleteServiceResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{28} +} + +func (x *DeleteServiceResponse) GetDeleted() bool { + if x != nil { + return x.Deleted + } + return false +} + +// Persisted sandbox service endpoint. +type ServiceEndpoint struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Kubernetes-style metadata. + Metadata *datamodelv1.ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + // Sandbox object ID. + SandboxId string `protobuf:"bytes,2,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Sandbox name. + SandboxName string `protobuf:"bytes,3,opt,name=sandbox_name,json=sandboxName,proto3" json:"sandbox_name,omitempty"` + // Service name within the sandbox. + ServiceName string `protobuf:"bytes,4,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + // Loopback TCP port inside the sandbox. + TargetPort uint32 `protobuf:"varint,5,opt,name=target_port,json=targetPort,proto3" json:"target_port,omitempty"` + // Whether browser-facing service routing is enabled for this endpoint. + Domain bool `protobuf:"varint,6,opt,name=domain,proto3" json:"domain,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServiceEndpoint) Reset() { + *x = ServiceEndpoint{} + mi := &file_openshell_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServiceEndpoint) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServiceEndpoint) ProtoMessage() {} + +func (x *ServiceEndpoint) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[29] + 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 ServiceEndpoint.ProtoReflect.Descriptor instead. +func (*ServiceEndpoint) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{29} +} + +func (x *ServiceEndpoint) GetMetadata() *datamodelv1.ObjectMeta { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *ServiceEndpoint) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *ServiceEndpoint) GetSandboxName() string { + if x != nil { + return x.SandboxName + } + return "" +} + +func (x *ServiceEndpoint) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +func (x *ServiceEndpoint) GetTargetPort() uint32 { + if x != nil { + return x.TargetPort + } + return 0 +} + +func (x *ServiceEndpoint) GetDomain() bool { + if x != nil { + return x.Domain + } + return false +} + +// Response containing a service endpoint and, when available, its local URL. +type ServiceEndpointResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Endpoint *ServiceEndpoint `protobuf:"bytes,1,opt,name=endpoint,proto3" json:"endpoint,omitempty"` + Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServiceEndpointResponse) Reset() { + *x = ServiceEndpointResponse{} + mi := &file_openshell_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServiceEndpointResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServiceEndpointResponse) ProtoMessage() {} + +func (x *ServiceEndpointResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[30] + 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 ServiceEndpointResponse.ProtoReflect.Descriptor instead. +func (*ServiceEndpointResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{30} +} + +func (x *ServiceEndpointResponse) GetEndpoint() *ServiceEndpoint { + if x != nil { + return x.Endpoint + } + return nil +} + +func (x *ServiceEndpointResponse) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +// Revoke SSH session request. +type RevokeSshSessionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Session token to revoke. + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RevokeSshSessionRequest) Reset() { + *x = RevokeSshSessionRequest{} + mi := &file_openshell_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RevokeSshSessionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeSshSessionRequest) ProtoMessage() {} + +func (x *RevokeSshSessionRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[31] + 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 RevokeSshSessionRequest.ProtoReflect.Descriptor instead. +func (*RevokeSshSessionRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{31} +} + +func (x *RevokeSshSessionRequest) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +// Revoke SSH session response. +type RevokeSshSessionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // True when a session was revoked. + Revoked bool `protobuf:"varint,1,opt,name=revoked,proto3" json:"revoked,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RevokeSshSessionResponse) Reset() { + *x = RevokeSshSessionResponse{} + mi := &file_openshell_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RevokeSshSessionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeSshSessionResponse) ProtoMessage() {} + +func (x *RevokeSshSessionResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[32] + 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 RevokeSshSessionResponse.ProtoReflect.Descriptor instead. +func (*RevokeSshSessionResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{32} +} + +func (x *RevokeSshSessionResponse) GetRevoked() bool { + if x != nil { + return x.Revoked + } + return false +} + +// Execute command request. +type ExecSandboxRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox id. + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Command and arguments. + Command []string `protobuf:"bytes,2,rep,name=command,proto3" json:"command,omitempty"` + // Optional working directory. + Workdir string `protobuf:"bytes,3,opt,name=workdir,proto3" json:"workdir,omitempty"` + // Optional environment overrides. + Environment map[string]string `protobuf:"bytes,4,rep,name=environment,proto3" json:"environment,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Optional timeout in seconds. 0 means no timeout. + TimeoutSeconds uint32 `protobuf:"varint,5,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"` + // Optional stdin payload passed to the command. + Stdin []byte `protobuf:"bytes,6,opt,name=stdin,proto3" json:"stdin,omitempty"` + // Request a pseudo-terminal for the remote command. + Tty bool `protobuf:"varint,7,opt,name=tty,proto3" json:"tty,omitempty"` + // Initial terminal columns (used when tty=true, 0 = use default). + Cols uint32 `protobuf:"varint,8,opt,name=cols,proto3" json:"cols,omitempty"` + // Initial terminal rows (used when tty=true, 0 = use default). + Rows uint32 `protobuf:"varint,9,opt,name=rows,proto3" json:"rows,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecSandboxRequest) Reset() { + *x = ExecSandboxRequest{} + mi := &file_openshell_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecSandboxRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecSandboxRequest) ProtoMessage() {} + +func (x *ExecSandboxRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[33] + 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 ExecSandboxRequest.ProtoReflect.Descriptor instead. +func (*ExecSandboxRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{33} +} + +func (x *ExecSandboxRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *ExecSandboxRequest) GetCommand() []string { + if x != nil { + return x.Command + } + return nil +} + +func (x *ExecSandboxRequest) GetWorkdir() string { + if x != nil { + return x.Workdir + } + return "" +} + +func (x *ExecSandboxRequest) GetEnvironment() map[string]string { + if x != nil { + return x.Environment + } + return nil +} + +func (x *ExecSandboxRequest) GetTimeoutSeconds() uint32 { + if x != nil { + return x.TimeoutSeconds + } + return 0 +} + +func (x *ExecSandboxRequest) GetStdin() []byte { + if x != nil { + return x.Stdin + } + return nil +} + +func (x *ExecSandboxRequest) GetTty() bool { + if x != nil { + return x.Tty + } + return false +} + +func (x *ExecSandboxRequest) GetCols() uint32 { + if x != nil { + return x.Cols + } + return 0 +} + +func (x *ExecSandboxRequest) GetRows() uint32 { + if x != nil { + return x.Rows + } + return 0 +} + +// One stdout chunk from a sandbox exec. +type ExecSandboxStdout struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecSandboxStdout) Reset() { + *x = ExecSandboxStdout{} + mi := &file_openshell_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecSandboxStdout) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecSandboxStdout) ProtoMessage() {} + +func (x *ExecSandboxStdout) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecSandboxStdout.ProtoReflect.Descriptor instead. +func (*ExecSandboxStdout) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{34} +} + +func (x *ExecSandboxStdout) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +// One stderr chunk from a sandbox exec. +type ExecSandboxStderr struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecSandboxStderr) Reset() { + *x = ExecSandboxStderr{} + mi := &file_openshell_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecSandboxStderr) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecSandboxStderr) ProtoMessage() {} + +func (x *ExecSandboxStderr) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecSandboxStderr.ProtoReflect.Descriptor instead. +func (*ExecSandboxStderr) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{35} +} + +func (x *ExecSandboxStderr) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +// Final exit status for a sandbox exec. +type ExecSandboxExit struct { + state protoimpl.MessageState `protogen:"open.v1"` + ExitCode int32 `protobuf:"varint,1,opt,name=exit_code,json=exitCode,proto3" json:"exit_code,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecSandboxExit) Reset() { + *x = ExecSandboxExit{} + mi := &file_openshell_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecSandboxExit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecSandboxExit) ProtoMessage() {} + +func (x *ExecSandboxExit) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[36] + 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 ExecSandboxExit.ProtoReflect.Descriptor instead. +func (*ExecSandboxExit) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{36} +} + +func (x *ExecSandboxExit) GetExitCode() int32 { + if x != nil { + return x.ExitCode + } + return 0 +} + +// One event in a sandbox exec stream. +type ExecSandboxEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *ExecSandboxEvent_Stdout + // *ExecSandboxEvent_Stderr + // *ExecSandboxEvent_Exit + Payload isExecSandboxEvent_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecSandboxEvent) Reset() { + *x = ExecSandboxEvent{} + mi := &file_openshell_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecSandboxEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecSandboxEvent) ProtoMessage() {} + +func (x *ExecSandboxEvent) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[37] + 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 ExecSandboxEvent.ProtoReflect.Descriptor instead. +func (*ExecSandboxEvent) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{37} +} + +func (x *ExecSandboxEvent) GetPayload() isExecSandboxEvent_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *ExecSandboxEvent) GetStdout() *ExecSandboxStdout { + if x != nil { + if x, ok := x.Payload.(*ExecSandboxEvent_Stdout); ok { + return x.Stdout + } + } + return nil +} + +func (x *ExecSandboxEvent) GetStderr() *ExecSandboxStderr { + if x != nil { + if x, ok := x.Payload.(*ExecSandboxEvent_Stderr); ok { + return x.Stderr + } + } + return nil +} + +func (x *ExecSandboxEvent) GetExit() *ExecSandboxExit { + if x != nil { + if x, ok := x.Payload.(*ExecSandboxEvent_Exit); ok { + return x.Exit + } + } + return nil +} + +type isExecSandboxEvent_Payload interface { + isExecSandboxEvent_Payload() +} + +type ExecSandboxEvent_Stdout struct { + Stdout *ExecSandboxStdout `protobuf:"bytes,1,opt,name=stdout,proto3,oneof"` +} + +type ExecSandboxEvent_Stderr struct { + Stderr *ExecSandboxStderr `protobuf:"bytes,2,opt,name=stderr,proto3,oneof"` +} + +type ExecSandboxEvent_Exit struct { + Exit *ExecSandboxExit `protobuf:"bytes,3,opt,name=exit,proto3,oneof"` +} + +func (*ExecSandboxEvent_Stdout) isExecSandboxEvent_Payload() {} + +func (*ExecSandboxEvent_Stderr) isExecSandboxEvent_Payload() {} + +func (*ExecSandboxEvent_Exit) isExecSandboxEvent_Payload() {} + +// Initial frame for one TCP forward stream. +type TcpForwardInit struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox id. + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Optional service identifier for audit/correlation. + ServiceId string `protobuf:"bytes,4,opt,name=service_id,json=serviceId,proto3" json:"service_id,omitempty"` + // Target the gateway should request from the supervisor. + // + // Types that are valid to be assigned to Target: + // + // *TcpForwardInit_Ssh + // *TcpForwardInit_Tcp + Target isTcpForwardInit_Target `protobuf_oneof:"target"` + // Optional target-specific authorization token. SSH targets use this as the + // short-lived SSH session token issued by CreateSshSession. + AuthorizationToken string `protobuf:"bytes,7,opt,name=authorization_token,json=authorizationToken,proto3" json:"authorization_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TcpForwardInit) Reset() { + *x = TcpForwardInit{} + mi := &file_openshell_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TcpForwardInit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TcpForwardInit) ProtoMessage() {} + +func (x *TcpForwardInit) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[38] + 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 TcpForwardInit.ProtoReflect.Descriptor instead. +func (*TcpForwardInit) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{38} +} + +func (x *TcpForwardInit) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *TcpForwardInit) GetServiceId() string { + if x != nil { + return x.ServiceId + } + return "" +} + +func (x *TcpForwardInit) GetTarget() isTcpForwardInit_Target { + if x != nil { + return x.Target + } + return nil +} + +func (x *TcpForwardInit) GetSsh() *SshRelayTarget { + if x != nil { + if x, ok := x.Target.(*TcpForwardInit_Ssh); ok { + return x.Ssh + } + } + return nil +} + +func (x *TcpForwardInit) GetTcp() *TcpRelayTarget { + if x != nil { + if x, ok := x.Target.(*TcpForwardInit_Tcp); ok { + return x.Tcp + } + } + return nil +} + +func (x *TcpForwardInit) GetAuthorizationToken() string { + if x != nil { + return x.AuthorizationToken + } + return "" +} + +type isTcpForwardInit_Target interface { + isTcpForwardInit_Target() +} + +type TcpForwardInit_Ssh struct { + Ssh *SshRelayTarget `protobuf:"bytes,5,opt,name=ssh,proto3,oneof"` +} + +type TcpForwardInit_Tcp struct { + Tcp *TcpRelayTarget `protobuf:"bytes,6,opt,name=tcp,proto3,oneof"` +} + +func (*TcpForwardInit_Ssh) isTcpForwardInit_Target() {} + +func (*TcpForwardInit_Tcp) isTcpForwardInit_Target() {} + +// A single frame on the CLI-to-gateway TCP forward stream. +type TcpForwardFrame struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *TcpForwardFrame_Init + // *TcpForwardFrame_Data + Payload isTcpForwardFrame_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TcpForwardFrame) Reset() { + *x = TcpForwardFrame{} + mi := &file_openshell_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TcpForwardFrame) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TcpForwardFrame) ProtoMessage() {} + +func (x *TcpForwardFrame) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[39] + 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 TcpForwardFrame.ProtoReflect.Descriptor instead. +func (*TcpForwardFrame) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{39} +} + +func (x *TcpForwardFrame) GetPayload() isTcpForwardFrame_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *TcpForwardFrame) GetInit() *TcpForwardInit { + if x != nil { + if x, ok := x.Payload.(*TcpForwardFrame_Init); ok { + return x.Init + } + } + return nil +} + +func (x *TcpForwardFrame) GetData() []byte { + if x != nil { + if x, ok := x.Payload.(*TcpForwardFrame_Data); ok { + return x.Data + } + } + return nil +} + +type isTcpForwardFrame_Payload interface { + isTcpForwardFrame_Payload() +} + +type TcpForwardFrame_Init struct { + Init *TcpForwardInit `protobuf:"bytes,1,opt,name=init,proto3,oneof"` +} + +type TcpForwardFrame_Data struct { + Data []byte `protobuf:"bytes,2,opt,name=data,proto3,oneof"` +} + +func (*TcpForwardFrame_Init) isTcpForwardFrame_Payload() {} + +func (*TcpForwardFrame_Data) isTcpForwardFrame_Payload() {} + +// Client-to-server message for interactive exec. +type ExecSandboxInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *ExecSandboxInput_Start + // *ExecSandboxInput_Stdin + // *ExecSandboxInput_Resize + Payload isExecSandboxInput_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecSandboxInput) Reset() { + *x = ExecSandboxInput{} + mi := &file_openshell_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecSandboxInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecSandboxInput) ProtoMessage() {} + +func (x *ExecSandboxInput) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[40] + 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 ExecSandboxInput.ProtoReflect.Descriptor instead. +func (*ExecSandboxInput) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{40} +} + +func (x *ExecSandboxInput) GetPayload() isExecSandboxInput_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *ExecSandboxInput) GetStart() *ExecSandboxRequest { + if x != nil { + if x, ok := x.Payload.(*ExecSandboxInput_Start); ok { + return x.Start + } + } + return nil +} + +func (x *ExecSandboxInput) GetStdin() []byte { + if x != nil { + if x, ok := x.Payload.(*ExecSandboxInput_Stdin); ok { + return x.Stdin + } + } + return nil +} + +func (x *ExecSandboxInput) GetResize() *ExecSandboxWindowResize { + if x != nil { + if x, ok := x.Payload.(*ExecSandboxInput_Resize); ok { + return x.Resize + } + } + return nil +} + +type isExecSandboxInput_Payload interface { + isExecSandboxInput_Payload() +} + +type ExecSandboxInput_Start struct { + // First message: exec request metadata. + Start *ExecSandboxRequest `protobuf:"bytes,1,opt,name=start,proto3,oneof"` +} + +type ExecSandboxInput_Stdin struct { + // Subsequent messages: raw stdin bytes. + Stdin []byte `protobuf:"bytes,2,opt,name=stdin,proto3,oneof"` +} + +type ExecSandboxInput_Resize struct { + // Terminal window size change. + Resize *ExecSandboxWindowResize `protobuf:"bytes,3,opt,name=resize,proto3,oneof"` +} + +func (*ExecSandboxInput_Start) isExecSandboxInput_Payload() {} + +func (*ExecSandboxInput_Stdin) isExecSandboxInput_Payload() {} + +func (*ExecSandboxInput_Resize) isExecSandboxInput_Payload() {} + +// Terminal window resize event for interactive exec. +type ExecSandboxWindowResize struct { + state protoimpl.MessageState `protogen:"open.v1"` + Cols uint32 `protobuf:"varint,1,opt,name=cols,proto3" json:"cols,omitempty"` + Rows uint32 `protobuf:"varint,2,opt,name=rows,proto3" json:"rows,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecSandboxWindowResize) Reset() { + *x = ExecSandboxWindowResize{} + mi := &file_openshell_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecSandboxWindowResize) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecSandboxWindowResize) ProtoMessage() {} + +func (x *ExecSandboxWindowResize) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[41] + 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 ExecSandboxWindowResize.ProtoReflect.Descriptor instead. +func (*ExecSandboxWindowResize) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{41} +} + +func (x *ExecSandboxWindowResize) GetCols() uint32 { + if x != nil { + return x.Cols + } + return 0 +} + +func (x *ExecSandboxWindowResize) GetRows() uint32 { + if x != nil { + return x.Rows + } + return 0 +} + +// SSH session record stored in persistence. +type SshSession struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Kubernetes-style metadata (id, name, labels, timestamps, resource version). + Metadata *datamodelv1.ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + // Sandbox id. + SandboxId string `protobuf:"bytes,2,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Session token. + Token string `protobuf:"bytes,3,opt,name=token,proto3" json:"token,omitempty"` + // Expiry timestamp in milliseconds since epoch. 0 means no expiry + // (backward-compatible default for sessions created before this field existed). + ExpiresAtMs int64 `protobuf:"varint,4,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` + // Revoked flag. + Revoked bool `protobuf:"varint,5,opt,name=revoked,proto3" json:"revoked,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SshSession) Reset() { + *x = SshSession{} + mi := &file_openshell_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SshSession) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SshSession) ProtoMessage() {} + +func (x *SshSession) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[42] + 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 SshSession.ProtoReflect.Descriptor instead. +func (*SshSession) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{42} +} + +func (x *SshSession) GetMetadata() *datamodelv1.ObjectMeta { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *SshSession) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *SshSession) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *SshSession) GetExpiresAtMs() int64 { + if x != nil { + return x.ExpiresAtMs + } + return 0 +} + +func (x *SshSession) GetRevoked() bool { + if x != nil { + return x.Revoked + } + return false +} + +// Watch sandbox request. +type WatchSandboxRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox id. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Stream sandbox status snapshots. + FollowStatus bool `protobuf:"varint,2,opt,name=follow_status,json=followStatus,proto3" json:"follow_status,omitempty"` + // Stream openshell-server process logs correlated to this sandbox. + FollowLogs bool `protobuf:"varint,3,opt,name=follow_logs,json=followLogs,proto3" json:"follow_logs,omitempty"` + // Stream platform events correlated to this sandbox. + FollowEvents bool `protobuf:"varint,4,opt,name=follow_events,json=followEvents,proto3" json:"follow_events,omitempty"` + // Replay the last N log lines (best-effort) before following. + LogTailLines uint32 `protobuf:"varint,5,opt,name=log_tail_lines,json=logTailLines,proto3" json:"log_tail_lines,omitempty"` + // Replay the last N platform events (best-effort) before following. + EventTail uint32 `protobuf:"varint,6,opt,name=event_tail,json=eventTail,proto3" json:"event_tail,omitempty"` + // Stop streaming once the sandbox reaches a terminal phase (READY or ERROR). + StopOnTerminal bool `protobuf:"varint,7,opt,name=stop_on_terminal,json=stopOnTerminal,proto3" json:"stop_on_terminal,omitempty"` + // Only include log lines with timestamp >= this value (milliseconds since epoch). + // 0 means no time filter. Applies to both tail replay and live streaming. + LogSinceMs int64 `protobuf:"varint,8,opt,name=log_since_ms,json=logSinceMs,proto3" json:"log_since_ms,omitempty"` + // Filter by log source (e.g. "gateway", "sandbox"). Empty means all sources. + LogSources []string `protobuf:"bytes,9,rep,name=log_sources,json=logSources,proto3" json:"log_sources,omitempty"` + // Minimum log level to include (e.g. "INFO", "WARN", "ERROR"). Empty means all levels. + LogMinLevel string `protobuf:"bytes,10,opt,name=log_min_level,json=logMinLevel,proto3" json:"log_min_level,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WatchSandboxRequest) Reset() { + *x = WatchSandboxRequest{} + mi := &file_openshell_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WatchSandboxRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WatchSandboxRequest) ProtoMessage() {} + +func (x *WatchSandboxRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[43] + 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 WatchSandboxRequest.ProtoReflect.Descriptor instead. +func (*WatchSandboxRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{43} +} + +func (x *WatchSandboxRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *WatchSandboxRequest) GetFollowStatus() bool { + if x != nil { + return x.FollowStatus + } + return false +} + +func (x *WatchSandboxRequest) GetFollowLogs() bool { + if x != nil { + return x.FollowLogs + } + return false +} + +func (x *WatchSandboxRequest) GetFollowEvents() bool { + if x != nil { + return x.FollowEvents + } + return false +} + +func (x *WatchSandboxRequest) GetLogTailLines() uint32 { + if x != nil { + return x.LogTailLines + } + return 0 +} + +func (x *WatchSandboxRequest) GetEventTail() uint32 { + if x != nil { + return x.EventTail + } + return 0 +} + +func (x *WatchSandboxRequest) GetStopOnTerminal() bool { + if x != nil { + return x.StopOnTerminal + } + return false +} + +func (x *WatchSandboxRequest) GetLogSinceMs() int64 { + if x != nil { + return x.LogSinceMs + } + return 0 +} + +func (x *WatchSandboxRequest) GetLogSources() []string { + if x != nil { + return x.LogSources + } + return nil +} + +func (x *WatchSandboxRequest) GetLogMinLevel() string { + if x != nil { + return x.LogMinLevel + } + return "" +} + +// One event in a sandbox watch stream. +type SandboxStreamEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *SandboxStreamEvent_Sandbox + // *SandboxStreamEvent_Log + // *SandboxStreamEvent_Event + // *SandboxStreamEvent_Warning + // *SandboxStreamEvent_DraftPolicyUpdate + Payload isSandboxStreamEvent_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxStreamEvent) Reset() { + *x = SandboxStreamEvent{} + mi := &file_openshell_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxStreamEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxStreamEvent) ProtoMessage() {} + +func (x *SandboxStreamEvent) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[44] + 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 SandboxStreamEvent.ProtoReflect.Descriptor instead. +func (*SandboxStreamEvent) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{44} +} + +func (x *SandboxStreamEvent) GetPayload() isSandboxStreamEvent_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *SandboxStreamEvent) GetSandbox() *Sandbox { + if x != nil { + if x, ok := x.Payload.(*SandboxStreamEvent_Sandbox); ok { + return x.Sandbox + } + } + return nil } func (x *SandboxStreamEvent) GetLog() *SandboxLogLine { if x != nil { - if x, ok := x.Payload.(*SandboxStreamEvent_Log); ok { - return x.Log + if x, ok := x.Payload.(*SandboxStreamEvent_Log); ok { + return x.Log + } + } + return nil +} + +func (x *SandboxStreamEvent) GetEvent() *PlatformEvent { + if x != nil { + if x, ok := x.Payload.(*SandboxStreamEvent_Event); ok { + return x.Event + } + } + return nil +} + +func (x *SandboxStreamEvent) GetWarning() *SandboxStreamWarning { + if x != nil { + if x, ok := x.Payload.(*SandboxStreamEvent_Warning); ok { + return x.Warning + } + } + return nil +} + +func (x *SandboxStreamEvent) GetDraftPolicyUpdate() *DraftPolicyUpdate { + if x != nil { + if x, ok := x.Payload.(*SandboxStreamEvent_DraftPolicyUpdate); ok { + return x.DraftPolicyUpdate + } + } + return nil +} + +type isSandboxStreamEvent_Payload interface { + isSandboxStreamEvent_Payload() +} + +type SandboxStreamEvent_Sandbox struct { + // Latest sandbox snapshot. + Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3,oneof"` +} + +type SandboxStreamEvent_Log struct { + // One server log line/event. + Log *SandboxLogLine `protobuf:"bytes,2,opt,name=log,proto3,oneof"` +} + +type SandboxStreamEvent_Event struct { + // One platform event. + Event *PlatformEvent `protobuf:"bytes,3,opt,name=event,proto3,oneof"` +} + +type SandboxStreamEvent_Warning struct { + // Warning from the server (e.g. missed messages due to lag). + Warning *SandboxStreamWarning `protobuf:"bytes,4,opt,name=warning,proto3,oneof"` +} + +type SandboxStreamEvent_DraftPolicyUpdate struct { + // Draft policy update notification. + DraftPolicyUpdate *DraftPolicyUpdate `protobuf:"bytes,5,opt,name=draft_policy_update,json=draftPolicyUpdate,proto3,oneof"` +} + +func (*SandboxStreamEvent_Sandbox) isSandboxStreamEvent_Payload() {} + +func (*SandboxStreamEvent_Log) isSandboxStreamEvent_Payload() {} + +func (*SandboxStreamEvent_Event) isSandboxStreamEvent_Payload() {} + +func (*SandboxStreamEvent_Warning) isSandboxStreamEvent_Payload() {} + +func (*SandboxStreamEvent_DraftPolicyUpdate) isSandboxStreamEvent_Payload() {} + +// Log line correlated to a sandbox. +type SandboxLogLine struct { + state protoimpl.MessageState `protogen:"open.v1"` + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + TimestampMs int64 `protobuf:"varint,2,opt,name=timestamp_ms,json=timestampMs,proto3" json:"timestamp_ms,omitempty"` + Level string `protobuf:"bytes,3,opt,name=level,proto3" json:"level,omitempty"` + Target string `protobuf:"bytes,4,opt,name=target,proto3" json:"target,omitempty"` + Message string `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"` + // Log source: "gateway" (server-side) or "sandbox" (supervisor). + // Empty is treated as "gateway" for backward compatibility. + Source string `protobuf:"bytes,6,opt,name=source,proto3" json:"source,omitempty"` + // Structured key-value fields from the tracing event (e.g. dst_host, action). + Fields map[string]string `protobuf:"bytes,7,rep,name=fields,proto3" json:"fields,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxLogLine) Reset() { + *x = SandboxLogLine{} + mi := &file_openshell_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxLogLine) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxLogLine) ProtoMessage() {} + +func (x *SandboxLogLine) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[45] + 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 SandboxLogLine.ProtoReflect.Descriptor instead. +func (*SandboxLogLine) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{45} +} + +func (x *SandboxLogLine) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *SandboxLogLine) GetTimestampMs() int64 { + if x != nil { + return x.TimestampMs + } + return 0 +} + +func (x *SandboxLogLine) GetLevel() string { + if x != nil { + return x.Level + } + return "" +} + +func (x *SandboxLogLine) GetTarget() string { + if x != nil { + return x.Target + } + return "" +} + +func (x *SandboxLogLine) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *SandboxLogLine) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *SandboxLogLine) GetFields() map[string]string { + if x != nil { + return x.Fields + } + return nil +} + +type SandboxStreamWarning struct { + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxStreamWarning) Reset() { + *x = SandboxStreamWarning{} + mi := &file_openshell_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxStreamWarning) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxStreamWarning) ProtoMessage() {} + +func (x *SandboxStreamWarning) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[46] + 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 SandboxStreamWarning.ProtoReflect.Descriptor instead. +func (*SandboxStreamWarning) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{46} +} + +func (x *SandboxStreamWarning) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +// Create provider request. +type CreateProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Provider *datamodelv1.Provider `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateProviderRequest) Reset() { + *x = CreateProviderRequest{} + mi := &file_openshell_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateProviderRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateProviderRequest) ProtoMessage() {} + +func (x *CreateProviderRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[47] + 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 CreateProviderRequest.ProtoReflect.Descriptor instead. +func (*CreateProviderRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{47} +} + +func (x *CreateProviderRequest) GetProvider() *datamodelv1.Provider { + if x != nil { + return x.Provider + } + return nil +} + +// Get provider request. +type GetProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProviderRequest) Reset() { + *x = GetProviderRequest{} + mi := &file_openshell_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProviderRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProviderRequest) ProtoMessage() {} + +func (x *GetProviderRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[48] + 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 GetProviderRequest.ProtoReflect.Descriptor instead. +func (*GetProviderRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{48} +} + +func (x *GetProviderRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +// List providers request. +type ListProvidersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Limit uint32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` + Offset uint32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListProvidersRequest) Reset() { + *x = ListProvidersRequest{} + mi := &file_openshell_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListProvidersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListProvidersRequest) ProtoMessage() {} + +func (x *ListProvidersRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[49] + 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 ListProvidersRequest.ProtoReflect.Descriptor instead. +func (*ListProvidersRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{49} +} + +func (x *ListProvidersRequest) GetLimit() uint32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *ListProvidersRequest) GetOffset() uint32 { + if x != nil { + return x.Offset + } + return 0 +} + +// Update provider request. +type UpdateProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Provider *datamodelv1.Provider `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateProviderRequest) Reset() { + *x = UpdateProviderRequest{} + mi := &file_openshell_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateProviderRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateProviderRequest) ProtoMessage() {} + +func (x *UpdateProviderRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[50] + 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 UpdateProviderRequest.ProtoReflect.Descriptor instead. +func (*UpdateProviderRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{50} +} + +func (x *UpdateProviderRequest) GetProvider() *datamodelv1.Provider { + if x != nil { + return x.Provider + } + return nil +} + +// Delete provider request. +type DeleteProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteProviderRequest) Reset() { + *x = DeleteProviderRequest{} + mi := &file_openshell_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteProviderRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteProviderRequest) ProtoMessage() {} + +func (x *DeleteProviderRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[51] + 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 DeleteProviderRequest.ProtoReflect.Descriptor instead. +func (*DeleteProviderRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{51} +} + +func (x *DeleteProviderRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +// Provider response. +type ProviderResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Provider *datamodelv1.Provider `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderResponse) Reset() { + *x = ProviderResponse{} + mi := &file_openshell_proto_msgTypes[52] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderResponse) ProtoMessage() {} + +func (x *ProviderResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[52] + 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 ProviderResponse.ProtoReflect.Descriptor instead. +func (*ProviderResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{52} +} + +func (x *ProviderResponse) GetProvider() *datamodelv1.Provider { + if x != nil { + return x.Provider + } + return nil +} + +// List providers response. +type ListProvidersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Providers []*datamodelv1.Provider `protobuf:"bytes,1,rep,name=providers,proto3" json:"providers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListProvidersResponse) Reset() { + *x = ListProvidersResponse{} + mi := &file_openshell_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListProvidersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListProvidersResponse) ProtoMessage() {} + +func (x *ListProvidersResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[53] + 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 ListProvidersResponse.ProtoReflect.Descriptor instead. +func (*ListProvidersResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{53} +} + +func (x *ListProvidersResponse) GetProviders() []*datamodelv1.Provider { + if x != nil { + return x.Providers + } + return nil +} + +// List provider type profiles request. +type ListProviderProfilesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Limit uint32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` + Offset uint32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListProviderProfilesRequest) Reset() { + *x = ListProviderProfilesRequest{} + mi := &file_openshell_proto_msgTypes[54] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListProviderProfilesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListProviderProfilesRequest) ProtoMessage() {} + +func (x *ListProviderProfilesRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[54] + 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 ListProviderProfilesRequest.ProtoReflect.Descriptor instead. +func (*ListProviderProfilesRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{54} +} + +func (x *ListProviderProfilesRequest) GetLimit() uint32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *ListProviderProfilesRequest) GetOffset() uint32 { + if x != nil { + return x.Offset + } + return 0 +} + +// Fetch provider type profile request. +type GetProviderProfileRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProviderProfileRequest) Reset() { + *x = GetProviderProfileRequest{} + mi := &file_openshell_proto_msgTypes[55] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProviderProfileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProviderProfileRequest) ProtoMessage() {} + +func (x *GetProviderProfileRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[55] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) } + return ms } - return nil + return mi.MessageOf(x) } -func (x *SandboxStreamEvent) GetEvent() *PlatformEvent { +// Deprecated: Use GetProviderProfileRequest.ProtoReflect.Descriptor instead. +func (*GetProviderProfileRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{55} +} + +func (x *GetProviderProfileRequest) GetId() string { if x != nil { - if x, ok := x.Payload.(*SandboxStreamEvent_Event); ok { - return x.Event - } + return x.Id } - return nil + return "" } -func (x *SandboxStreamEvent) GetWarning() *SandboxStreamWarning { +// Provider profile payload with optional source metadata for diagnostics. +type ProviderProfileImportItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + Profile *ProviderProfile `protobuf:"bytes,1,opt,name=profile,proto3" json:"profile,omitempty"` + Source string `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderProfileImportItem) Reset() { + *x = ProviderProfileImportItem{} + mi := &file_openshell_proto_msgTypes[56] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderProfileImportItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderProfileImportItem) ProtoMessage() {} + +func (x *ProviderProfileImportItem) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[56] if x != nil { - if x, ok := x.Payload.(*SandboxStreamEvent_Warning); ok { - return x.Warning + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) } + return ms } - return nil + return mi.MessageOf(x) } -func (x *SandboxStreamEvent) GetDraftPolicyUpdate() *DraftPolicyUpdate { +// Deprecated: Use ProviderProfileImportItem.ProtoReflect.Descriptor instead. +func (*ProviderProfileImportItem) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{56} +} + +func (x *ProviderProfileImportItem) GetProfile() *ProviderProfile { if x != nil { - if x, ok := x.Payload.(*SandboxStreamEvent_DraftPolicyUpdate); ok { - return x.DraftPolicyUpdate - } + return x.Profile } return nil } -type isSandboxStreamEvent_Payload interface { - isSandboxStreamEvent_Payload() +func (x *ProviderProfileImportItem) GetSource() string { + if x != nil { + return x.Source + } + return "" } -type SandboxStreamEvent_Sandbox struct { - // Latest sandbox snapshot. - Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3,oneof"` +// Provider profile validation diagnostic. +type ProviderProfileDiagnostic struct { + state protoimpl.MessageState `protogen:"open.v1"` + Source string `protobuf:"bytes,1,opt,name=source,proto3" json:"source,omitempty"` + ProfileId string `protobuf:"bytes,2,opt,name=profile_id,json=profileId,proto3" json:"profile_id,omitempty"` + Field string `protobuf:"bytes,3,opt,name=field,proto3" json:"field,omitempty"` + Message string `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"` + Severity string `protobuf:"bytes,5,opt,name=severity,proto3" json:"severity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -type SandboxStreamEvent_Log struct { - // One server log line/event. - Log *SandboxLogLine `protobuf:"bytes,2,opt,name=log,proto3,oneof"` +func (x *ProviderProfileDiagnostic) Reset() { + *x = ProviderProfileDiagnostic{} + mi := &file_openshell_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -type SandboxStreamEvent_Event struct { - // One platform event. - Event *PlatformEvent `protobuf:"bytes,3,opt,name=event,proto3,oneof"` +func (x *ProviderProfileDiagnostic) String() string { + return protoimpl.X.MessageStringOf(x) } -type SandboxStreamEvent_Warning struct { - // Warning from the server (e.g. missed messages due to lag). - Warning *SandboxStreamWarning `protobuf:"bytes,4,opt,name=warning,proto3,oneof"` +func (*ProviderProfileDiagnostic) ProtoMessage() {} + +func (x *ProviderProfileDiagnostic) ProtoReflect() protoreflect.Message { + mi := &file_openshell_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) } -type SandboxStreamEvent_DraftPolicyUpdate struct { - // Draft policy update notification. - DraftPolicyUpdate *DraftPolicyUpdate `protobuf:"bytes,5,opt,name=draft_policy_update,json=draftPolicyUpdate,proto3,oneof"` +// Deprecated: Use ProviderProfileDiagnostic.ProtoReflect.Descriptor instead. +func (*ProviderProfileDiagnostic) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{57} } -func (*SandboxStreamEvent_Sandbox) isSandboxStreamEvent_Payload() {} +func (x *ProviderProfileDiagnostic) GetSource() string { + if x != nil { + return x.Source + } + return "" +} -func (*SandboxStreamEvent_Log) isSandboxStreamEvent_Payload() {} +func (x *ProviderProfileDiagnostic) GetProfileId() string { + if x != nil { + return x.ProfileId + } + return "" +} -func (*SandboxStreamEvent_Event) isSandboxStreamEvent_Payload() {} +func (x *ProviderProfileDiagnostic) GetField() string { + if x != nil { + return x.Field + } + return "" +} -func (*SandboxStreamEvent_Warning) isSandboxStreamEvent_Payload() {} +func (x *ProviderProfileDiagnostic) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} -func (*SandboxStreamEvent_DraftPolicyUpdate) isSandboxStreamEvent_Payload() {} +func (x *ProviderProfileDiagnostic) GetSeverity() string { + if x != nil { + return x.Severity + } + return "" +} -// Log line correlated to a sandbox. -type SandboxLogLine struct { - state protoimpl.MessageState `protogen:"open.v1"` - SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` - TimestampMs int64 `protobuf:"varint,2,opt,name=timestamp_ms,json=timestampMs,proto3" json:"timestamp_ms,omitempty"` - Level string `protobuf:"bytes,3,opt,name=level,proto3" json:"level,omitempty"` - Target string `protobuf:"bytes,4,opt,name=target,proto3" json:"target,omitempty"` - Message string `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"` - // Log source: "gateway" (server-side) or "sandbox" (supervisor). - // Empty is treated as "gateway" for backward compatibility. - Source string `protobuf:"bytes,6,opt,name=source,proto3" json:"source,omitempty"` - // Structured key-value fields from the tracing event (e.g. dst_host, action). - Fields map[string]string `protobuf:"bytes,7,rep,name=fields,proto3" json:"fields,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` +// Provider credential declaration. +type ProviderProfileCredential struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + EnvVars []string `protobuf:"bytes,3,rep,name=env_vars,json=envVars,proto3" json:"env_vars,omitempty"` + Required bool `protobuf:"varint,4,opt,name=required,proto3" json:"required,omitempty"` + AuthStyle string `protobuf:"bytes,5,opt,name=auth_style,json=authStyle,proto3" json:"auth_style,omitempty"` + HeaderName string `protobuf:"bytes,6,opt,name=header_name,json=headerName,proto3" json:"header_name,omitempty"` + QueryParam string `protobuf:"bytes,7,opt,name=query_param,json=queryParam,proto3" json:"query_param,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *SandboxLogLine) Reset() { - *x = SandboxLogLine{} - mi := &file_openshell_proto_msgTypes[27] +func (x *ProviderProfileCredential) Reset() { + *x = ProviderProfileCredential{} + mi := &file_openshell_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *SandboxLogLine) String() string { +func (x *ProviderProfileCredential) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SandboxLogLine) ProtoMessage() {} +func (*ProviderProfileCredential) ProtoMessage() {} -func (x *SandboxLogLine) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[27] +func (x *ProviderProfileCredential) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2175,82 +4125,237 @@ func (x *SandboxLogLine) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SandboxLogLine.ProtoReflect.Descriptor instead. -func (*SandboxLogLine) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{27} +// Deprecated: Use ProviderProfileCredential.ProtoReflect.Descriptor instead. +func (*ProviderProfileCredential) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{58} } -func (x *SandboxLogLine) GetSandboxId() string { +func (x *ProviderProfileCredential) GetName() string { if x != nil { - return x.SandboxId + return x.Name } return "" } -func (x *SandboxLogLine) GetTimestampMs() int64 { +func (x *ProviderProfileCredential) GetDescription() string { if x != nil { - return x.TimestampMs + return x.Description } - return 0 + return "" } -func (x *SandboxLogLine) GetLevel() string { +func (x *ProviderProfileCredential) GetEnvVars() []string { if x != nil { - return x.Level + return x.EnvVars + } + return nil +} + +func (x *ProviderProfileCredential) GetRequired() bool { + if x != nil { + return x.Required + } + return false +} + +func (x *ProviderProfileCredential) GetAuthStyle() string { + if x != nil { + return x.AuthStyle } return "" } -func (x *SandboxLogLine) GetTarget() string { +func (x *ProviderProfileCredential) GetHeaderName() string { if x != nil { - return x.Target + return x.HeaderName } return "" } -func (x *SandboxLogLine) GetMessage() string { +func (x *ProviderProfileCredential) GetQueryParam() string { if x != nil { - return x.Message + return x.QueryParam + } + return "" +} + +// Provider type profile metadata exposed to clients. +type ProviderProfile struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + Category ProviderProfileCategory `protobuf:"varint,4,opt,name=category,proto3,enum=openshell.v1.ProviderProfileCategory" json:"category,omitempty"` + Credentials []*ProviderProfileCredential `protobuf:"bytes,5,rep,name=credentials,proto3" json:"credentials,omitempty"` + Endpoints []*sandboxv1.NetworkEndpoint `protobuf:"bytes,6,rep,name=endpoints,proto3" json:"endpoints,omitempty"` + Binaries []*sandboxv1.NetworkBinary `protobuf:"bytes,7,rep,name=binaries,proto3" json:"binaries,omitempty"` + InferenceCapable bool `protobuf:"varint,8,opt,name=inference_capable,json=inferenceCapable,proto3" json:"inference_capable,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderProfile) Reset() { + *x = ProviderProfile{} + mi := &file_openshell_proto_msgTypes[59] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderProfile) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderProfile) ProtoMessage() {} + +func (x *ProviderProfile) ProtoReflect() protoreflect.Message { + mi := &file_openshell_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 ProviderProfile.ProtoReflect.Descriptor instead. +func (*ProviderProfile) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{59} +} + +func (x *ProviderProfile) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ProviderProfile) GetDisplayName() string { + if x != nil { + return x.DisplayName } return "" } -func (x *SandboxLogLine) GetSource() string { +func (x *ProviderProfile) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *ProviderProfile) GetCategory() ProviderProfileCategory { + if x != nil { + return x.Category + } + return ProviderProfileCategory_PROVIDER_PROFILE_CATEGORY_UNSPECIFIED +} + +func (x *ProviderProfile) GetCredentials() []*ProviderProfileCredential { + if x != nil { + return x.Credentials + } + return nil +} + +func (x *ProviderProfile) GetEndpoints() []*sandboxv1.NetworkEndpoint { + if x != nil { + return x.Endpoints + } + return nil +} + +func (x *ProviderProfile) GetBinaries() []*sandboxv1.NetworkBinary { + if x != nil { + return x.Binaries + } + return nil +} + +func (x *ProviderProfile) GetInferenceCapable() bool { + if x != nil { + return x.InferenceCapable + } + return false +} + +// Stored custom provider profile object. +type StoredProviderProfile struct { + state protoimpl.MessageState `protogen:"open.v1"` + Metadata *datamodelv1.ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + Profile *ProviderProfile `protobuf:"bytes,2,opt,name=profile,proto3" json:"profile,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StoredProviderProfile) Reset() { + *x = StoredProviderProfile{} + mi := &file_openshell_proto_msgTypes[60] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StoredProviderProfile) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StoredProviderProfile) ProtoMessage() {} + +func (x *StoredProviderProfile) ProtoReflect() protoreflect.Message { + mi := &file_openshell_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 StoredProviderProfile.ProtoReflect.Descriptor instead. +func (*StoredProviderProfile) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{60} +} + +func (x *StoredProviderProfile) GetMetadata() *datamodelv1.ObjectMeta { if x != nil { - return x.Source + return x.Metadata } - return "" + return nil } -func (x *SandboxLogLine) GetFields() map[string]string { +func (x *StoredProviderProfile) GetProfile() *ProviderProfile { if x != nil { - return x.Fields + return x.Profile } return nil } -type SandboxStreamWarning struct { +// Provider profile response. +type ProviderProfileResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + Profile *ProviderProfile `protobuf:"bytes,1,opt,name=profile,proto3" json:"profile,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *SandboxStreamWarning) Reset() { - *x = SandboxStreamWarning{} - mi := &file_openshell_proto_msgTypes[28] +func (x *ProviderProfileResponse) Reset() { + *x = ProviderProfileResponse{} + mi := &file_openshell_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *SandboxStreamWarning) String() string { +func (x *ProviderProfileResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SandboxStreamWarning) ProtoMessage() {} +func (*ProviderProfileResponse) ProtoMessage() {} -func (x *SandboxStreamWarning) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[28] +func (x *ProviderProfileResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2261,41 +4366,41 @@ func (x *SandboxStreamWarning) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SandboxStreamWarning.ProtoReflect.Descriptor instead. -func (*SandboxStreamWarning) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{28} +// Deprecated: Use ProviderProfileResponse.ProtoReflect.Descriptor instead. +func (*ProviderProfileResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{61} } -func (x *SandboxStreamWarning) GetMessage() string { +func (x *ProviderProfileResponse) GetProfile() *ProviderProfile { if x != nil { - return x.Message + return x.Profile } - return "" + return nil } -// Create provider request. -type CreateProviderRequest struct { +// List provider profiles response. +type ListProviderProfilesResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - Provider *datamodelv1.Provider `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + Profiles []*ProviderProfile `protobuf:"bytes,1,rep,name=profiles,proto3" json:"profiles,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *CreateProviderRequest) Reset() { - *x = CreateProviderRequest{} - mi := &file_openshell_proto_msgTypes[29] +func (x *ListProviderProfilesResponse) Reset() { + *x = ListProviderProfilesResponse{} + mi := &file_openshell_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *CreateProviderRequest) String() string { +func (x *ListProviderProfilesResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*CreateProviderRequest) ProtoMessage() {} +func (*ListProviderProfilesResponse) ProtoMessage() {} -func (x *CreateProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[29] +func (x *ListProviderProfilesResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2306,41 +4411,41 @@ func (x *CreateProviderRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use CreateProviderRequest.ProtoReflect.Descriptor instead. -func (*CreateProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{29} +// Deprecated: Use ListProviderProfilesResponse.ProtoReflect.Descriptor instead. +func (*ListProviderProfilesResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{62} } -func (x *CreateProviderRequest) GetProvider() *datamodelv1.Provider { +func (x *ListProviderProfilesResponse) GetProfiles() []*ProviderProfile { if x != nil { - return x.Provider + return x.Profiles } return nil } -// Get provider request. -type GetProviderRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` +// Import custom provider profiles request. +type ImportProviderProfilesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Profiles []*ProviderProfileImportItem `protobuf:"bytes,1,rep,name=profiles,proto3" json:"profiles,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *GetProviderRequest) Reset() { - *x = GetProviderRequest{} - mi := &file_openshell_proto_msgTypes[30] +func (x *ImportProviderProfilesRequest) Reset() { + *x = ImportProviderProfilesRequest{} + mi := &file_openshell_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *GetProviderRequest) String() string { +func (x *ImportProviderProfilesRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*GetProviderRequest) ProtoMessage() {} +func (*ImportProviderProfilesRequest) ProtoMessage() {} -func (x *GetProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[30] +func (x *ImportProviderProfilesRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2351,42 +4456,43 @@ func (x *GetProviderRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use GetProviderRequest.ProtoReflect.Descriptor instead. -func (*GetProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{30} +// Deprecated: Use ImportProviderProfilesRequest.ProtoReflect.Descriptor instead. +func (*ImportProviderProfilesRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{63} } -func (x *GetProviderRequest) GetName() string { +func (x *ImportProviderProfilesRequest) GetProfiles() []*ProviderProfileImportItem { if x != nil { - return x.Name + return x.Profiles } - return "" + return nil } -// List providers request. -type ListProvidersRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Limit uint32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` - Offset uint32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"` +// Import custom provider profiles response. +type ImportProviderProfilesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*ProviderProfileDiagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + Profiles []*ProviderProfile `protobuf:"bytes,2,rep,name=profiles,proto3" json:"profiles,omitempty"` + Imported bool `protobuf:"varint,3,opt,name=imported,proto3" json:"imported,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ListProvidersRequest) Reset() { - *x = ListProvidersRequest{} - mi := &file_openshell_proto_msgTypes[31] +func (x *ImportProviderProfilesResponse) Reset() { + *x = ImportProviderProfilesResponse{} + mi := &file_openshell_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ListProvidersRequest) String() string { +func (x *ImportProviderProfilesResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ListProvidersRequest) ProtoMessage() {} +func (*ImportProviderProfilesResponse) ProtoMessage() {} -func (x *ListProvidersRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[31] +func (x *ImportProviderProfilesResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2397,48 +4503,55 @@ func (x *ListProvidersRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ListProvidersRequest.ProtoReflect.Descriptor instead. -func (*ListProvidersRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{31} +// Deprecated: Use ImportProviderProfilesResponse.ProtoReflect.Descriptor instead. +func (*ImportProviderProfilesResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{64} } -func (x *ListProvidersRequest) GetLimit() uint32 { +func (x *ImportProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { if x != nil { - return x.Limit + return x.Diagnostics } - return 0 + return nil } -func (x *ListProvidersRequest) GetOffset() uint32 { +func (x *ImportProviderProfilesResponse) GetProfiles() []*ProviderProfile { if x != nil { - return x.Offset + return x.Profiles } - return 0 + return nil } -// Update provider request. -type UpdateProviderRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Provider *datamodelv1.Provider `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` +func (x *ImportProviderProfilesResponse) GetImported() bool { + if x != nil { + return x.Imported + } + return false +} + +// Lint provider profiles request. +type LintProviderProfilesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Profiles []*ProviderProfileImportItem `protobuf:"bytes,1,rep,name=profiles,proto3" json:"profiles,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *UpdateProviderRequest) Reset() { - *x = UpdateProviderRequest{} - mi := &file_openshell_proto_msgTypes[32] +func (x *LintProviderProfilesRequest) Reset() { + *x = LintProviderProfilesRequest{} + mi := &file_openshell_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *UpdateProviderRequest) String() string { +func (x *LintProviderProfilesRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*UpdateProviderRequest) ProtoMessage() {} +func (*LintProviderProfilesRequest) ProtoMessage() {} -func (x *UpdateProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[32] +func (x *LintProviderProfilesRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2449,41 +4562,42 @@ func (x *UpdateProviderRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use UpdateProviderRequest.ProtoReflect.Descriptor instead. -func (*UpdateProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{32} +// Deprecated: Use LintProviderProfilesRequest.ProtoReflect.Descriptor instead. +func (*LintProviderProfilesRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{65} } -func (x *UpdateProviderRequest) GetProvider() *datamodelv1.Provider { +func (x *LintProviderProfilesRequest) GetProfiles() []*ProviderProfileImportItem { if x != nil { - return x.Provider + return x.Profiles } return nil } -// Delete provider request. -type DeleteProviderRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` +// Lint provider profiles response. +type LintProviderProfilesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*ProviderProfileDiagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + Valid bool `protobuf:"varint,2,opt,name=valid,proto3" json:"valid,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *DeleteProviderRequest) Reset() { - *x = DeleteProviderRequest{} - mi := &file_openshell_proto_msgTypes[33] +func (x *LintProviderProfilesResponse) Reset() { + *x = LintProviderProfilesResponse{} + mi := &file_openshell_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *DeleteProviderRequest) String() string { +func (x *LintProviderProfilesResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*DeleteProviderRequest) ProtoMessage() {} +func (*LintProviderProfilesResponse) ProtoMessage() {} -func (x *DeleteProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[33] +func (x *LintProviderProfilesResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2494,41 +4608,48 @@ func (x *DeleteProviderRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use DeleteProviderRequest.ProtoReflect.Descriptor instead. -func (*DeleteProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{33} +// Deprecated: Use LintProviderProfilesResponse.ProtoReflect.Descriptor instead. +func (*LintProviderProfilesResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{66} } -func (x *DeleteProviderRequest) GetName() string { +func (x *LintProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { if x != nil { - return x.Name + return x.Diagnostics } - return "" + return nil } -// Provider response. -type ProviderResponse struct { +func (x *LintProviderProfilesResponse) GetValid() bool { + if x != nil { + return x.Valid + } + return false +} + +// Delete provider response. +type DeleteProviderResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - Provider *datamodelv1.Provider `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ProviderResponse) Reset() { - *x = ProviderResponse{} - mi := &file_openshell_proto_msgTypes[34] +func (x *DeleteProviderResponse) Reset() { + *x = DeleteProviderResponse{} + mi := &file_openshell_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ProviderResponse) String() string { +func (x *DeleteProviderResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ProviderResponse) ProtoMessage() {} +func (*DeleteProviderResponse) ProtoMessage() {} -func (x *ProviderResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[34] +func (x *DeleteProviderResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2539,41 +4660,41 @@ func (x *ProviderResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ProviderResponse.ProtoReflect.Descriptor instead. -func (*ProviderResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{34} +// Deprecated: Use DeleteProviderResponse.ProtoReflect.Descriptor instead. +func (*DeleteProviderResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{67} } -func (x *ProviderResponse) GetProvider() *datamodelv1.Provider { +func (x *DeleteProviderResponse) GetDeleted() bool { if x != nil { - return x.Provider + return x.Deleted } - return nil + return false } -// List providers response. -type ListProvidersResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Providers []*datamodelv1.Provider `protobuf:"bytes,1,rep,name=providers,proto3" json:"providers,omitempty"` +// Delete custom provider profile request. +type DeleteProviderProfileRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ListProvidersResponse) Reset() { - *x = ListProvidersResponse{} - mi := &file_openshell_proto_msgTypes[35] +func (x *DeleteProviderProfileRequest) Reset() { + *x = DeleteProviderProfileRequest{} + mi := &file_openshell_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ListProvidersResponse) String() string { +func (x *DeleteProviderProfileRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ListProvidersResponse) ProtoMessage() {} +func (*DeleteProviderProfileRequest) ProtoMessage() {} -func (x *ListProvidersResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[35] +func (x *DeleteProviderProfileRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2584,41 +4705,41 @@ func (x *ListProvidersResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ListProvidersResponse.ProtoReflect.Descriptor instead. -func (*ListProvidersResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{35} +// Deprecated: Use DeleteProviderProfileRequest.ProtoReflect.Descriptor instead. +func (*DeleteProviderProfileRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{68} } -func (x *ListProvidersResponse) GetProviders() []*datamodelv1.Provider { +func (x *DeleteProviderProfileRequest) GetId() string { if x != nil { - return x.Providers + return x.Id } - return nil + return "" } -// Delete provider response. -type DeleteProviderResponse struct { +// Delete custom provider profile response. +type DeleteProviderProfileResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *DeleteProviderResponse) Reset() { - *x = DeleteProviderResponse{} - mi := &file_openshell_proto_msgTypes[36] +func (x *DeleteProviderProfileResponse) Reset() { + *x = DeleteProviderProfileResponse{} + mi := &file_openshell_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *DeleteProviderResponse) String() string { +func (x *DeleteProviderProfileResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*DeleteProviderResponse) ProtoMessage() {} +func (*DeleteProviderProfileResponse) ProtoMessage() {} -func (x *DeleteProviderResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[36] +func (x *DeleteProviderProfileResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2629,12 +4750,12 @@ func (x *DeleteProviderResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use DeleteProviderResponse.ProtoReflect.Descriptor instead. -func (*DeleteProviderResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{36} +// Deprecated: Use DeleteProviderProfileResponse.ProtoReflect.Descriptor instead. +func (*DeleteProviderProfileResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{69} } -func (x *DeleteProviderResponse) GetDeleted() bool { +func (x *DeleteProviderProfileResponse) GetDeleted() bool { if x != nil { return x.Deleted } @@ -2652,7 +4773,7 @@ type GetSandboxProviderEnvironmentRequest struct { func (x *GetSandboxProviderEnvironmentRequest) Reset() { *x = GetSandboxProviderEnvironmentRequest{} - mi := &file_openshell_proto_msgTypes[37] + mi := &file_openshell_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2664,7 +4785,7 @@ func (x *GetSandboxProviderEnvironmentRequest) String() string { func (*GetSandboxProviderEnvironmentRequest) ProtoMessage() {} func (x *GetSandboxProviderEnvironmentRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[37] + mi := &file_openshell_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2677,7 +4798,7 @@ func (x *GetSandboxProviderEnvironmentRequest) ProtoReflect() protoreflect.Messa // Deprecated: Use GetSandboxProviderEnvironmentRequest.ProtoReflect.Descriptor instead. func (*GetSandboxProviderEnvironmentRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{37} + return file_openshell_proto_rawDescGZIP(), []int{70} } func (x *GetSandboxProviderEnvironmentRequest) GetSandboxId() string { @@ -2691,14 +4812,16 @@ func (x *GetSandboxProviderEnvironmentRequest) GetSandboxId() string { type GetSandboxProviderEnvironmentResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // Provider credential environment variables. - Environment map[string]string `protobuf:"bytes,1,rep,name=environment,proto3" json:"environment,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Environment map[string]string `protobuf:"bytes,1,rep,name=environment,proto3" json:"environment,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Fingerprint for the provider credential inputs that produced environment. + ProviderEnvRevision uint64 `protobuf:"varint,2,opt,name=provider_env_revision,json=providerEnvRevision,proto3" json:"provider_env_revision,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetSandboxProviderEnvironmentResponse) Reset() { *x = GetSandboxProviderEnvironmentResponse{} - mi := &file_openshell_proto_msgTypes[38] + mi := &file_openshell_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2710,7 +4833,7 @@ func (x *GetSandboxProviderEnvironmentResponse) String() string { func (*GetSandboxProviderEnvironmentResponse) ProtoMessage() {} func (x *GetSandboxProviderEnvironmentResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[38] + mi := &file_openshell_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2723,7 +4846,7 @@ func (x *GetSandboxProviderEnvironmentResponse) ProtoReflect() protoreflect.Mess // Deprecated: Use GetSandboxProviderEnvironmentResponse.ProtoReflect.Descriptor instead. func (*GetSandboxProviderEnvironmentResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{38} + return file_openshell_proto_rawDescGZIP(), []int{71} } func (x *GetSandboxProviderEnvironmentResponse) GetEnvironment() map[string]string { @@ -2733,6 +4856,13 @@ func (x *GetSandboxProviderEnvironmentResponse) GetEnvironment() map[string]stri return nil } +func (x *GetSandboxProviderEnvironmentResponse) GetProviderEnvRevision() uint64 { + if x != nil { + return x.ProviderEnvRevision + } + return 0 +} + // Update sandbox policy request. type UpdateConfigRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -2759,13 +4889,19 @@ type UpdateConfigRequest struct { Global bool `protobuf:"varint,6,opt,name=global,proto3" json:"global,omitempty"` // Batched incremental policy merge operations. Sandbox-scoped only. MergeOperations []*PolicyMergeOperation `protobuf:"bytes,7,rep,name=merge_operations,json=mergeOperations,proto3" json:"merge_operations,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Expected resource version for optimistic concurrency control (sandbox-scoped only). + // If 0, the server uses the current version (backward compatibility). + // If non-zero, the server validates that the sandbox's current resource_version + // matches this value before applying the mutation, returning ABORTED on mismatch. + // Ignored for global-scoped updates. + ExpectedResourceVersion uint64 `protobuf:"varint,8,opt,name=expected_resource_version,json=expectedResourceVersion,proto3" json:"expected_resource_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UpdateConfigRequest) Reset() { *x = UpdateConfigRequest{} - mi := &file_openshell_proto_msgTypes[39] + mi := &file_openshell_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2777,7 +4913,7 @@ func (x *UpdateConfigRequest) String() string { func (*UpdateConfigRequest) ProtoMessage() {} func (x *UpdateConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[39] + mi := &file_openshell_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2790,7 +4926,7 @@ func (x *UpdateConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateConfigRequest.ProtoReflect.Descriptor instead. func (*UpdateConfigRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{39} + return file_openshell_proto_rawDescGZIP(), []int{72} } func (x *UpdateConfigRequest) GetName() string { @@ -2842,6 +4978,13 @@ func (x *UpdateConfigRequest) GetMergeOperations() []*PolicyMergeOperation { return nil } +func (x *UpdateConfigRequest) GetExpectedResourceVersion() uint64 { + if x != nil { + return x.ExpectedResourceVersion + } + return 0 +} + type PolicyMergeOperation struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Operation: @@ -2859,7 +5002,7 @@ type PolicyMergeOperation struct { func (x *PolicyMergeOperation) Reset() { *x = PolicyMergeOperation{} - mi := &file_openshell_proto_msgTypes[40] + mi := &file_openshell_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2871,7 +5014,7 @@ func (x *PolicyMergeOperation) String() string { func (*PolicyMergeOperation) ProtoMessage() {} func (x *PolicyMergeOperation) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[40] + mi := &file_openshell_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2884,7 +5027,7 @@ func (x *PolicyMergeOperation) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyMergeOperation.ProtoReflect.Descriptor instead. func (*PolicyMergeOperation) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{40} + return file_openshell_proto_rawDescGZIP(), []int{73} } func (x *PolicyMergeOperation) GetOperation() isPolicyMergeOperation_Operation { @@ -2998,7 +5141,7 @@ type AddNetworkRule struct { func (x *AddNetworkRule) Reset() { *x = AddNetworkRule{} - mi := &file_openshell_proto_msgTypes[41] + mi := &file_openshell_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3010,7 +5153,7 @@ func (x *AddNetworkRule) String() string { func (*AddNetworkRule) ProtoMessage() {} func (x *AddNetworkRule) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[41] + mi := &file_openshell_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3023,7 +5166,7 @@ func (x *AddNetworkRule) ProtoReflect() protoreflect.Message { // Deprecated: Use AddNetworkRule.ProtoReflect.Descriptor instead. func (*AddNetworkRule) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{41} + return file_openshell_proto_rawDescGZIP(), []int{74} } func (x *AddNetworkRule) GetRuleName() string { @@ -3051,7 +5194,7 @@ type RemoveNetworkEndpoint struct { func (x *RemoveNetworkEndpoint) Reset() { *x = RemoveNetworkEndpoint{} - mi := &file_openshell_proto_msgTypes[42] + mi := &file_openshell_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3063,7 +5206,7 @@ func (x *RemoveNetworkEndpoint) String() string { func (*RemoveNetworkEndpoint) ProtoMessage() {} func (x *RemoveNetworkEndpoint) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[42] + mi := &file_openshell_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3076,7 +5219,7 @@ func (x *RemoveNetworkEndpoint) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkEndpoint.ProtoReflect.Descriptor instead. func (*RemoveNetworkEndpoint) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{42} + return file_openshell_proto_rawDescGZIP(), []int{75} } func (x *RemoveNetworkEndpoint) GetRuleName() string { @@ -3109,7 +5252,7 @@ type RemoveNetworkRule struct { func (x *RemoveNetworkRule) Reset() { *x = RemoveNetworkRule{} - mi := &file_openshell_proto_msgTypes[43] + mi := &file_openshell_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3121,7 +5264,7 @@ func (x *RemoveNetworkRule) String() string { func (*RemoveNetworkRule) ProtoMessage() {} func (x *RemoveNetworkRule) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[43] + mi := &file_openshell_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3134,7 +5277,7 @@ func (x *RemoveNetworkRule) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkRule.ProtoReflect.Descriptor instead. func (*RemoveNetworkRule) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{43} + return file_openshell_proto_rawDescGZIP(), []int{76} } func (x *RemoveNetworkRule) GetRuleName() string { @@ -3155,7 +5298,7 @@ type AddDenyRules struct { func (x *AddDenyRules) Reset() { *x = AddDenyRules{} - mi := &file_openshell_proto_msgTypes[44] + mi := &file_openshell_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3167,7 +5310,7 @@ func (x *AddDenyRules) String() string { func (*AddDenyRules) ProtoMessage() {} func (x *AddDenyRules) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[44] + mi := &file_openshell_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3180,7 +5323,7 @@ func (x *AddDenyRules) ProtoReflect() protoreflect.Message { // Deprecated: Use AddDenyRules.ProtoReflect.Descriptor instead. func (*AddDenyRules) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{44} + return file_openshell_proto_rawDescGZIP(), []int{77} } func (x *AddDenyRules) GetHost() string { @@ -3215,7 +5358,7 @@ type AddAllowRules struct { func (x *AddAllowRules) Reset() { *x = AddAllowRules{} - mi := &file_openshell_proto_msgTypes[45] + mi := &file_openshell_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3227,7 +5370,7 @@ func (x *AddAllowRules) String() string { func (*AddAllowRules) ProtoMessage() {} func (x *AddAllowRules) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[45] + mi := &file_openshell_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3240,7 +5383,7 @@ func (x *AddAllowRules) ProtoReflect() protoreflect.Message { // Deprecated: Use AddAllowRules.ProtoReflect.Descriptor instead. func (*AddAllowRules) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{45} + return file_openshell_proto_rawDescGZIP(), []int{78} } func (x *AddAllowRules) GetHost() string { @@ -3274,7 +5417,7 @@ type RemoveNetworkBinary struct { func (x *RemoveNetworkBinary) Reset() { *x = RemoveNetworkBinary{} - mi := &file_openshell_proto_msgTypes[46] + mi := &file_openshell_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3286,7 +5429,7 @@ func (x *RemoveNetworkBinary) String() string { func (*RemoveNetworkBinary) ProtoMessage() {} func (x *RemoveNetworkBinary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[46] + mi := &file_openshell_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3299,7 +5442,7 @@ func (x *RemoveNetworkBinary) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkBinary.ProtoReflect.Descriptor instead. func (*RemoveNetworkBinary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{46} + return file_openshell_proto_rawDescGZIP(), []int{79} } func (x *RemoveNetworkBinary) GetRuleName() string { @@ -3333,7 +5476,7 @@ type UpdateConfigResponse struct { func (x *UpdateConfigResponse) Reset() { *x = UpdateConfigResponse{} - mi := &file_openshell_proto_msgTypes[47] + mi := &file_openshell_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3345,7 +5488,7 @@ func (x *UpdateConfigResponse) String() string { func (*UpdateConfigResponse) ProtoMessage() {} func (x *UpdateConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[47] + mi := &file_openshell_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3358,7 +5501,7 @@ func (x *UpdateConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateConfigResponse.ProtoReflect.Descriptor instead. func (*UpdateConfigResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{47} + return file_openshell_proto_rawDescGZIP(), []int{80} } func (x *UpdateConfigResponse) GetVersion() uint32 { @@ -3404,7 +5547,7 @@ type GetSandboxPolicyStatusRequest struct { func (x *GetSandboxPolicyStatusRequest) Reset() { *x = GetSandboxPolicyStatusRequest{} - mi := &file_openshell_proto_msgTypes[48] + mi := &file_openshell_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3416,7 +5559,7 @@ func (x *GetSandboxPolicyStatusRequest) String() string { func (*GetSandboxPolicyStatusRequest) ProtoMessage() {} func (x *GetSandboxPolicyStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[48] + mi := &file_openshell_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3429,7 +5572,7 @@ func (x *GetSandboxPolicyStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxPolicyStatusRequest.ProtoReflect.Descriptor instead. func (*GetSandboxPolicyStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{48} + return file_openshell_proto_rawDescGZIP(), []int{81} } func (x *GetSandboxPolicyStatusRequest) GetName() string { @@ -3466,7 +5609,7 @@ type GetSandboxPolicyStatusResponse struct { func (x *GetSandboxPolicyStatusResponse) Reset() { *x = GetSandboxPolicyStatusResponse{} - mi := &file_openshell_proto_msgTypes[49] + mi := &file_openshell_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3478,7 +5621,7 @@ func (x *GetSandboxPolicyStatusResponse) String() string { func (*GetSandboxPolicyStatusResponse) ProtoMessage() {} func (x *GetSandboxPolicyStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[49] + mi := &file_openshell_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3491,7 +5634,7 @@ func (x *GetSandboxPolicyStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxPolicyStatusResponse.ProtoReflect.Descriptor instead. func (*GetSandboxPolicyStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{49} + return file_openshell_proto_rawDescGZIP(), []int{82} } func (x *GetSandboxPolicyStatusResponse) GetRevision() *SandboxPolicyRevision { @@ -3523,7 +5666,7 @@ type ListSandboxPoliciesRequest struct { func (x *ListSandboxPoliciesRequest) Reset() { *x = ListSandboxPoliciesRequest{} - mi := &file_openshell_proto_msgTypes[50] + mi := &file_openshell_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3535,7 +5678,7 @@ func (x *ListSandboxPoliciesRequest) String() string { func (*ListSandboxPoliciesRequest) ProtoMessage() {} func (x *ListSandboxPoliciesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[50] + mi := &file_openshell_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3548,7 +5691,7 @@ func (x *ListSandboxPoliciesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxPoliciesRequest.ProtoReflect.Descriptor instead. func (*ListSandboxPoliciesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{50} + return file_openshell_proto_rawDescGZIP(), []int{83} } func (x *ListSandboxPoliciesRequest) GetName() string { @@ -3589,7 +5732,7 @@ type ListSandboxPoliciesResponse struct { func (x *ListSandboxPoliciesResponse) Reset() { *x = ListSandboxPoliciesResponse{} - mi := &file_openshell_proto_msgTypes[51] + mi := &file_openshell_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3601,7 +5744,7 @@ func (x *ListSandboxPoliciesResponse) String() string { func (*ListSandboxPoliciesResponse) ProtoMessage() {} func (x *ListSandboxPoliciesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[51] + mi := &file_openshell_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3614,7 +5757,7 @@ func (x *ListSandboxPoliciesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxPoliciesResponse.ProtoReflect.Descriptor instead. func (*ListSandboxPoliciesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{51} + return file_openshell_proto_rawDescGZIP(), []int{84} } func (x *ListSandboxPoliciesResponse) GetRevisions() []*SandboxPolicyRevision { @@ -3641,7 +5784,7 @@ type ReportPolicyStatusRequest struct { func (x *ReportPolicyStatusRequest) Reset() { *x = ReportPolicyStatusRequest{} - mi := &file_openshell_proto_msgTypes[52] + mi := &file_openshell_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3653,7 +5796,7 @@ func (x *ReportPolicyStatusRequest) String() string { func (*ReportPolicyStatusRequest) ProtoMessage() {} func (x *ReportPolicyStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[52] + mi := &file_openshell_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3666,7 +5809,7 @@ func (x *ReportPolicyStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportPolicyStatusRequest.ProtoReflect.Descriptor instead. func (*ReportPolicyStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{52} + return file_openshell_proto_rawDescGZIP(), []int{85} } func (x *ReportPolicyStatusRequest) GetSandboxId() string { @@ -3706,7 +5849,7 @@ type ReportPolicyStatusResponse struct { func (x *ReportPolicyStatusResponse) Reset() { *x = ReportPolicyStatusResponse{} - mi := &file_openshell_proto_msgTypes[53] + mi := &file_openshell_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3718,7 +5861,7 @@ func (x *ReportPolicyStatusResponse) String() string { func (*ReportPolicyStatusResponse) ProtoMessage() {} func (x *ReportPolicyStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[53] + mi := &file_openshell_proto_msgTypes[86] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3731,7 +5874,7 @@ func (x *ReportPolicyStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportPolicyStatusResponse.ProtoReflect.Descriptor instead. func (*ReportPolicyStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{53} + return file_openshell_proto_rawDescGZIP(), []int{86} } // A versioned policy revision with metadata. @@ -3757,7 +5900,7 @@ type SandboxPolicyRevision struct { func (x *SandboxPolicyRevision) Reset() { *x = SandboxPolicyRevision{} - mi := &file_openshell_proto_msgTypes[54] + mi := &file_openshell_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3769,7 +5912,7 @@ func (x *SandboxPolicyRevision) String() string { func (*SandboxPolicyRevision) ProtoMessage() {} func (x *SandboxPolicyRevision) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[54] + mi := &file_openshell_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3782,7 +5925,7 @@ func (x *SandboxPolicyRevision) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxPolicyRevision.ProtoReflect.Descriptor instead. func (*SandboxPolicyRevision) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{54} + return file_openshell_proto_rawDescGZIP(), []int{87} } func (x *SandboxPolicyRevision) GetVersion() uint32 { @@ -3853,7 +5996,7 @@ type GetSandboxLogsRequest struct { func (x *GetSandboxLogsRequest) Reset() { *x = GetSandboxLogsRequest{} - mi := &file_openshell_proto_msgTypes[55] + mi := &file_openshell_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3865,7 +6008,7 @@ func (x *GetSandboxLogsRequest) String() string { func (*GetSandboxLogsRequest) ProtoMessage() {} func (x *GetSandboxLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[55] + mi := &file_openshell_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3878,7 +6021,7 @@ func (x *GetSandboxLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxLogsRequest.ProtoReflect.Descriptor instead. func (*GetSandboxLogsRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{55} + return file_openshell_proto_rawDescGZIP(), []int{88} } func (x *GetSandboxLogsRequest) GetSandboxId() string { @@ -3929,7 +6072,7 @@ type PushSandboxLogsRequest struct { func (x *PushSandboxLogsRequest) Reset() { *x = PushSandboxLogsRequest{} - mi := &file_openshell_proto_msgTypes[56] + mi := &file_openshell_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3941,7 +6084,7 @@ func (x *PushSandboxLogsRequest) String() string { func (*PushSandboxLogsRequest) ProtoMessage() {} func (x *PushSandboxLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[56] + mi := &file_openshell_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3954,7 +6097,7 @@ func (x *PushSandboxLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PushSandboxLogsRequest.ProtoReflect.Descriptor instead. func (*PushSandboxLogsRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{56} + return file_openshell_proto_rawDescGZIP(), []int{89} } func (x *PushSandboxLogsRequest) GetSandboxId() string { @@ -3980,7 +6123,7 @@ type PushSandboxLogsResponse struct { func (x *PushSandboxLogsResponse) Reset() { *x = PushSandboxLogsResponse{} - mi := &file_openshell_proto_msgTypes[57] + mi := &file_openshell_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3992,7 +6135,7 @@ func (x *PushSandboxLogsResponse) String() string { func (*PushSandboxLogsResponse) ProtoMessage() {} func (x *PushSandboxLogsResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[57] + mi := &file_openshell_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4005,7 +6148,7 @@ func (x *PushSandboxLogsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PushSandboxLogsResponse.ProtoReflect.Descriptor instead. func (*PushSandboxLogsResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{57} + return file_openshell_proto_rawDescGZIP(), []int{90} } // Get sandbox logs response. @@ -4021,7 +6164,7 @@ type GetSandboxLogsResponse struct { func (x *GetSandboxLogsResponse) Reset() { *x = GetSandboxLogsResponse{} - mi := &file_openshell_proto_msgTypes[58] + mi := &file_openshell_proto_msgTypes[91] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4033,7 +6176,7 @@ func (x *GetSandboxLogsResponse) String() string { func (*GetSandboxLogsResponse) ProtoMessage() {} func (x *GetSandboxLogsResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[58] + mi := &file_openshell_proto_msgTypes[91] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4046,7 +6189,7 @@ func (x *GetSandboxLogsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxLogsResponse.ProtoReflect.Descriptor instead. func (*GetSandboxLogsResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{58} + return file_openshell_proto_rawDescGZIP(), []int{91} } func (x *GetSandboxLogsResponse) GetLogs() []*SandboxLogLine { @@ -4079,7 +6222,7 @@ type SupervisorMessage struct { func (x *SupervisorMessage) Reset() { *x = SupervisorMessage{} - mi := &file_openshell_proto_msgTypes[59] + mi := &file_openshell_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4091,7 +6234,7 @@ func (x *SupervisorMessage) String() string { func (*SupervisorMessage) ProtoMessage() {} func (x *SupervisorMessage) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[59] + mi := &file_openshell_proto_msgTypes[92] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4104,7 +6247,7 @@ func (x *SupervisorMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorMessage.ProtoReflect.Descriptor instead. func (*SupervisorMessage) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{59} + return file_openshell_proto_rawDescGZIP(), []int{92} } func (x *SupervisorMessage) GetPayload() isSupervisorMessage_Payload { @@ -4195,7 +6338,7 @@ type GatewayMessage struct { func (x *GatewayMessage) Reset() { *x = GatewayMessage{} - mi := &file_openshell_proto_msgTypes[60] + mi := &file_openshell_proto_msgTypes[93] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4207,7 +6350,7 @@ func (x *GatewayMessage) String() string { func (*GatewayMessage) ProtoMessage() {} func (x *GatewayMessage) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[60] + mi := &file_openshell_proto_msgTypes[93] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4220,7 +6363,7 @@ func (x *GatewayMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use GatewayMessage.ProtoReflect.Descriptor instead. func (*GatewayMessage) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{60} + return file_openshell_proto_rawDescGZIP(), []int{93} } func (x *GatewayMessage) GetPayload() isGatewayMessage_Payload { @@ -4322,7 +6465,7 @@ type SupervisorHello struct { func (x *SupervisorHello) Reset() { *x = SupervisorHello{} - mi := &file_openshell_proto_msgTypes[61] + mi := &file_openshell_proto_msgTypes[94] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4334,7 +6477,7 @@ func (x *SupervisorHello) String() string { func (*SupervisorHello) ProtoMessage() {} func (x *SupervisorHello) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[61] + mi := &file_openshell_proto_msgTypes[94] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4347,7 +6490,7 @@ func (x *SupervisorHello) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorHello.ProtoReflect.Descriptor instead. func (*SupervisorHello) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{61} + return file_openshell_proto_rawDescGZIP(), []int{94} } func (x *SupervisorHello) GetSandboxId() string { @@ -4377,7 +6520,7 @@ type SessionAccepted struct { func (x *SessionAccepted) Reset() { *x = SessionAccepted{} - mi := &file_openshell_proto_msgTypes[62] + mi := &file_openshell_proto_msgTypes[95] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4389,7 +6532,7 @@ func (x *SessionAccepted) String() string { func (*SessionAccepted) ProtoMessage() {} func (x *SessionAccepted) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[62] + mi := &file_openshell_proto_msgTypes[95] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4402,7 +6545,7 @@ func (x *SessionAccepted) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionAccepted.ProtoReflect.Descriptor instead. func (*SessionAccepted) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{62} + return file_openshell_proto_rawDescGZIP(), []int{95} } func (x *SessionAccepted) GetSessionId() string { @@ -4430,7 +6573,7 @@ type SessionRejected struct { func (x *SessionRejected) Reset() { *x = SessionRejected{} - mi := &file_openshell_proto_msgTypes[63] + mi := &file_openshell_proto_msgTypes[96] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4441,8 +6584,143 @@ func (x *SessionRejected) String() string { func (*SessionRejected) ProtoMessage() {} -func (x *SessionRejected) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[63] +func (x *SessionRejected) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[96] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SessionRejected.ProtoReflect.Descriptor instead. +func (*SessionRejected) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{96} +} + +func (x *SessionRejected) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +// Supervisor heartbeat. +type SupervisorHeartbeat struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SupervisorHeartbeat) Reset() { + *x = SupervisorHeartbeat{} + mi := &file_openshell_proto_msgTypes[97] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SupervisorHeartbeat) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SupervisorHeartbeat) ProtoMessage() {} + +func (x *SupervisorHeartbeat) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[97] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SupervisorHeartbeat.ProtoReflect.Descriptor instead. +func (*SupervisorHeartbeat) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{97} +} + +// Gateway heartbeat. +type GatewayHeartbeat struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GatewayHeartbeat) Reset() { + *x = GatewayHeartbeat{} + mi := &file_openshell_proto_msgTypes[98] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GatewayHeartbeat) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GatewayHeartbeat) ProtoMessage() {} + +func (x *GatewayHeartbeat) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[98] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GatewayHeartbeat.ProtoReflect.Descriptor instead. +func (*GatewayHeartbeat) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{98} +} + +// Gateway requests the supervisor to open a relay channel. +// +// On receiving this, the supervisor should initiate a RelayStream RPC to +// the gateway, sending a RelayInit in the first RelayFrame to associate +// the new HTTP/2 stream with the pending relay slot. The supervisor +// bridges that stream to the requested local target. +type RelayOpen struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Gateway-allocated channel identifier (UUID). + ChannelId string `protobuf:"bytes,1,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` + // Target the supervisor should dial inside the sandbox. + // If absent, supervisors treat the relay as SSH for compatibility. + // + // Types that are valid to be assigned to Target: + // + // *RelayOpen_Ssh + // *RelayOpen_Tcp + Target isRelayOpen_Target `protobuf_oneof:"target"` + // Optional service identifier for audit/correlation. + ServiceId string `protobuf:"bytes,5,opt,name=service_id,json=serviceId,proto3" json:"service_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelayOpen) Reset() { + *x = RelayOpen{} + mi := &file_openshell_proto_msgTypes[99] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelayOpen) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelayOpen) ProtoMessage() {} + +func (x *RelayOpen) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[99] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4453,77 +6731,88 @@ func (x *SessionRejected) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SessionRejected.ProtoReflect.Descriptor instead. -func (*SessionRejected) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{63} +// Deprecated: Use RelayOpen.ProtoReflect.Descriptor instead. +func (*RelayOpen) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{99} } -func (x *SessionRejected) GetReason() string { +func (x *RelayOpen) GetChannelId() string { if x != nil { - return x.Reason + return x.ChannelId } return "" } -// Supervisor heartbeat. -type SupervisorHeartbeat struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *RelayOpen) GetTarget() isRelayOpen_Target { + if x != nil { + return x.Target + } + return nil } -func (x *SupervisorHeartbeat) Reset() { - *x = SupervisorHeartbeat{} - mi := &file_openshell_proto_msgTypes[64] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) +func (x *RelayOpen) GetSsh() *SshRelayTarget { + if x != nil { + if x, ok := x.Target.(*RelayOpen_Ssh); ok { + return x.Ssh + } + } + return nil } -func (x *SupervisorHeartbeat) String() string { - return protoimpl.X.MessageStringOf(x) +func (x *RelayOpen) GetTcp() *TcpRelayTarget { + if x != nil { + if x, ok := x.Target.(*RelayOpen_Tcp); ok { + return x.Tcp + } + } + return nil } -func (*SupervisorHeartbeat) ProtoMessage() {} - -func (x *SupervisorHeartbeat) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[64] +func (x *RelayOpen) GetServiceId() string { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.ServiceId } - return mi.MessageOf(x) + return "" } -// Deprecated: Use SupervisorHeartbeat.ProtoReflect.Descriptor instead. -func (*SupervisorHeartbeat) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{64} +type isRelayOpen_Target interface { + isRelayOpen_Target() } -// Gateway heartbeat. -type GatewayHeartbeat struct { +type RelayOpen_Ssh struct { + Ssh *SshRelayTarget `protobuf:"bytes,2,opt,name=ssh,proto3,oneof"` +} + +type RelayOpen_Tcp struct { + Tcp *TcpRelayTarget `protobuf:"bytes,3,opt,name=tcp,proto3,oneof"` +} + +func (*RelayOpen_Ssh) isRelayOpen_Target() {} + +func (*RelayOpen_Tcp) isRelayOpen_Target() {} + +// Built-in SSH relay target. +type SshRelayTarget struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *GatewayHeartbeat) Reset() { - *x = GatewayHeartbeat{} - mi := &file_openshell_proto_msgTypes[65] +func (x *SshRelayTarget) Reset() { + *x = SshRelayTarget{} + mi := &file_openshell_proto_msgTypes[100] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *GatewayHeartbeat) String() string { +func (x *SshRelayTarget) String() string { return protoimpl.X.MessageStringOf(x) } -func (*GatewayHeartbeat) ProtoMessage() {} +func (*SshRelayTarget) ProtoMessage() {} -func (x *GatewayHeartbeat) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[65] +func (x *SshRelayTarget) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[100] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4534,40 +6823,37 @@ func (x *GatewayHeartbeat) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use GatewayHeartbeat.ProtoReflect.Descriptor instead. -func (*GatewayHeartbeat) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{65} +// Deprecated: Use SshRelayTarget.ProtoReflect.Descriptor instead. +func (*SshRelayTarget) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{100} } -// Gateway requests the supervisor to open a relay channel. -// -// On receiving this, the supervisor should initiate a RelayStream RPC to -// the gateway, sending a RelayInit in the first RelayFrame to associate -// the new HTTP/2 stream with the pending relay slot. The supervisor -// bridges that stream to the local SSH daemon. -type RelayOpen struct { +// TCP target dialed by the supervisor from inside the sandbox. +type TcpRelayTarget struct { state protoimpl.MessageState `protogen:"open.v1"` - // Gateway-allocated channel identifier (UUID). - ChannelId string `protobuf:"bytes,1,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` + // Phase 1 accepts loopback only: 127.0.0.1, ::1, or localhost. + Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"` + // Target port. Must fit in u16 and be non-zero. + Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *RelayOpen) Reset() { - *x = RelayOpen{} - mi := &file_openshell_proto_msgTypes[66] +func (x *TcpRelayTarget) Reset() { + *x = TcpRelayTarget{} + mi := &file_openshell_proto_msgTypes[101] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *RelayOpen) String() string { +func (x *TcpRelayTarget) String() string { return protoimpl.X.MessageStringOf(x) } -func (*RelayOpen) ProtoMessage() {} +func (*TcpRelayTarget) ProtoMessage() {} -func (x *RelayOpen) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[66] +func (x *TcpRelayTarget) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[101] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4578,18 +6864,25 @@ func (x *RelayOpen) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use RelayOpen.ProtoReflect.Descriptor instead. -func (*RelayOpen) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{66} +// Deprecated: Use TcpRelayTarget.ProtoReflect.Descriptor instead. +func (*TcpRelayTarget) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{101} } -func (x *RelayOpen) GetChannelId() string { +func (x *TcpRelayTarget) GetHost() string { if x != nil { - return x.ChannelId + return x.Host } return "" } +func (x *TcpRelayTarget) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + // Initial RelayStream frame sent by the supervisor to claim a pending relay. type RelayInit struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -4601,7 +6894,7 @@ type RelayInit struct { func (x *RelayInit) Reset() { *x = RelayInit{} - mi := &file_openshell_proto_msgTypes[67] + mi := &file_openshell_proto_msgTypes[102] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4613,7 +6906,7 @@ func (x *RelayInit) String() string { func (*RelayInit) ProtoMessage() {} func (x *RelayInit) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[67] + mi := &file_openshell_proto_msgTypes[102] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4626,7 +6919,7 @@ func (x *RelayInit) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayInit.ProtoReflect.Descriptor instead. func (*RelayInit) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{67} + return file_openshell_proto_rawDescGZIP(), []int{102} } func (x *RelayInit) GetChannelId() string { @@ -4653,7 +6946,7 @@ type RelayFrame struct { func (x *RelayFrame) Reset() { *x = RelayFrame{} - mi := &file_openshell_proto_msgTypes[68] + mi := &file_openshell_proto_msgTypes[103] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4665,7 +6958,7 @@ func (x *RelayFrame) String() string { func (*RelayFrame) ProtoMessage() {} func (x *RelayFrame) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[68] + mi := &file_openshell_proto_msgTypes[103] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4678,7 +6971,7 @@ func (x *RelayFrame) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayFrame.ProtoReflect.Descriptor instead. func (*RelayFrame) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{68} + return file_openshell_proto_rawDescGZIP(), []int{103} } func (x *RelayFrame) GetPayload() isRelayFrame_Payload { @@ -4737,7 +7030,7 @@ type RelayOpenResult struct { func (x *RelayOpenResult) Reset() { *x = RelayOpenResult{} - mi := &file_openshell_proto_msgTypes[69] + mi := &file_openshell_proto_msgTypes[104] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4749,7 +7042,7 @@ func (x *RelayOpenResult) String() string { func (*RelayOpenResult) ProtoMessage() {} func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[69] + mi := &file_openshell_proto_msgTypes[104] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4762,7 +7055,7 @@ func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayOpenResult.ProtoReflect.Descriptor instead. func (*RelayOpenResult) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{69} + return file_openshell_proto_rawDescGZIP(), []int{104} } func (x *RelayOpenResult) GetChannelId() string { @@ -4799,7 +7092,7 @@ type RelayClose struct { func (x *RelayClose) Reset() { *x = RelayClose{} - mi := &file_openshell_proto_msgTypes[70] + mi := &file_openshell_proto_msgTypes[105] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4811,7 +7104,7 @@ func (x *RelayClose) String() string { func (*RelayClose) ProtoMessage() {} func (x *RelayClose) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[70] + mi := &file_openshell_proto_msgTypes[105] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4824,7 +7117,7 @@ func (x *RelayClose) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayClose.ProtoReflect.Descriptor instead. func (*RelayClose) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{70} + return file_openshell_proto_rawDescGZIP(), []int{105} } func (x *RelayClose) GetChannelId() string { @@ -4858,7 +7151,7 @@ type L7RequestSample struct { func (x *L7RequestSample) Reset() { *x = L7RequestSample{} - mi := &file_openshell_proto_msgTypes[71] + mi := &file_openshell_proto_msgTypes[106] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4870,7 +7163,7 @@ func (x *L7RequestSample) String() string { func (*L7RequestSample) ProtoMessage() {} func (x *L7RequestSample) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[71] + mi := &file_openshell_proto_msgTypes[106] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4883,7 +7176,7 @@ func (x *L7RequestSample) ProtoReflect() protoreflect.Message { // Deprecated: Use L7RequestSample.ProtoReflect.Descriptor instead. func (*L7RequestSample) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{71} + return file_openshell_proto_rawDescGZIP(), []int{106} } func (x *L7RequestSample) GetMethod() string { @@ -4957,7 +7250,7 @@ type DenialSummary struct { func (x *DenialSummary) Reset() { *x = DenialSummary{} - mi := &file_openshell_proto_msgTypes[72] + mi := &file_openshell_proto_msgTypes[107] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4969,7 +7262,7 @@ func (x *DenialSummary) String() string { func (*DenialSummary) ProtoMessage() {} func (x *DenialSummary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[72] + mi := &file_openshell_proto_msgTypes[107] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4982,7 +7275,7 @@ func (x *DenialSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use DenialSummary.ProtoReflect.Descriptor instead. func (*DenialSummary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{72} + return file_openshell_proto_rawDescGZIP(), []int{107} } func (x *DenialSummary) GetSandboxId() string { @@ -5138,14 +7431,23 @@ type PolicyChunk struct { // Most recent time this endpoint was re-proposed (ms since epoch). LastSeenMs int64 `protobuf:"varint,15,opt,name=last_seen_ms,json=lastSeenMs,proto3" json:"last_seen_ms,omitempty"` // Binary path that triggered the denial (denormalized for display convenience). - Binary string `protobuf:"bytes,16,opt,name=binary,proto3" json:"binary,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Binary string `protobuf:"bytes,16,opt,name=binary,proto3" json:"binary,omitempty"` + // Validation verdict from gateway-side static checks (prover output). + // Free-form summary string for human consumption in the inbox card. + // Empty until the prover has run for this chunk. + ValidationResult string `protobuf:"bytes,17,opt,name=validation_result,json=validationResult,proto3" json:"validation_result,omitempty"` + // Operator-supplied free-form text accompanying a rejection. Populated + // when the reviewer rejects via `RejectDraftChunkRequest.reason`; surfaced + // back to the in-sandbox agent so it can revise the proposal. + // Empty for non-rejected chunks. + RejectionReason string `protobuf:"bytes,18,opt,name=rejection_reason,json=rejectionReason,proto3" json:"rejection_reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PolicyChunk) Reset() { *x = PolicyChunk{} - mi := &file_openshell_proto_msgTypes[73] + mi := &file_openshell_proto_msgTypes[108] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5157,7 +7459,7 @@ func (x *PolicyChunk) String() string { func (*PolicyChunk) ProtoMessage() {} func (x *PolicyChunk) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[73] + mi := &file_openshell_proto_msgTypes[108] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5170,7 +7472,7 @@ func (x *PolicyChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyChunk.ProtoReflect.Descriptor instead. func (*PolicyChunk) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{73} + return file_openshell_proto_rawDescGZIP(), []int{108} } func (x *PolicyChunk) GetId() string { @@ -5285,6 +7587,20 @@ func (x *PolicyChunk) GetBinary() string { return "" } +func (x *PolicyChunk) GetValidationResult() string { + if x != nil { + return x.ValidationResult + } + return "" +} + +func (x *PolicyChunk) GetRejectionReason() string { + if x != nil { + return x.RejectionReason + } + return "" +} + // Notification that the draft policy was updated. type DraftPolicyUpdate struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -5302,7 +7618,7 @@ type DraftPolicyUpdate struct { func (x *DraftPolicyUpdate) Reset() { *x = DraftPolicyUpdate{} - mi := &file_openshell_proto_msgTypes[74] + mi := &file_openshell_proto_msgTypes[109] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5314,7 +7630,7 @@ func (x *DraftPolicyUpdate) String() string { func (*DraftPolicyUpdate) ProtoMessage() {} func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[74] + mi := &file_openshell_proto_msgTypes[109] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5327,7 +7643,7 @@ func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftPolicyUpdate.ProtoReflect.Descriptor instead. func (*DraftPolicyUpdate) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{74} + return file_openshell_proto_rawDescGZIP(), []int{109} } func (x *DraftPolicyUpdate) GetDraftVersion() uint64 { @@ -5365,7 +7681,13 @@ type SubmitPolicyAnalysisRequest struct { Summaries []*DenialSummary `protobuf:"bytes,1,rep,name=summaries,proto3" json:"summaries,omitempty"` // Proposed policy chunks (validated by sandbox OPA engine). ProposedChunks []*PolicyChunk `protobuf:"bytes,2,rep,name=proposed_chunks,json=proposedChunks,proto3" json:"proposed_chunks,omitempty"` - // Analysis mode: "mechanistic" or "llm". + // Analysis mode. `mechanistic` is the observation-driven path from the + // denial aggregator — chunks targeting the same host|port|binary fold + // into one row with hit_count incremented. `agent_authored` is an + // intentional proposal from an in-sandbox agent — each submission lands + // as its own chunk so the redraft-after-rejection loop has a stable id + // to watch. Other values are treated as agent-style (no dedup) so a new + // mode does not silently collapse proposals. AnalysisMode string `protobuf:"bytes,3,opt,name=analysis_mode,json=analysisMode,proto3" json:"analysis_mode,omitempty"` // Sandbox name. Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` @@ -5375,7 +7697,7 @@ type SubmitPolicyAnalysisRequest struct { func (x *SubmitPolicyAnalysisRequest) Reset() { *x = SubmitPolicyAnalysisRequest{} - mi := &file_openshell_proto_msgTypes[75] + mi := &file_openshell_proto_msgTypes[110] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5387,7 +7709,7 @@ func (x *SubmitPolicyAnalysisRequest) String() string { func (*SubmitPolicyAnalysisRequest) ProtoMessage() {} func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[75] + mi := &file_openshell_proto_msgTypes[110] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5400,7 +7722,7 @@ func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SubmitPolicyAnalysisRequest.ProtoReflect.Descriptor instead. func (*SubmitPolicyAnalysisRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{75} + return file_openshell_proto_rawDescGZIP(), []int{110} } func (x *SubmitPolicyAnalysisRequest) GetSummaries() []*DenialSummary { @@ -5439,13 +7761,17 @@ type SubmitPolicyAnalysisResponse struct { RejectedChunks uint32 `protobuf:"varint,2,opt,name=rejected_chunks,json=rejectedChunks,proto3" json:"rejected_chunks,omitempty"` // Reasons for each rejected chunk. RejectionReasons []string `protobuf:"bytes,3,rep,name=rejection_reasons,json=rejectionReasons,proto3" json:"rejection_reasons,omitempty"` + // Server-assigned chunk IDs for the accepted chunks, in submission order. + // Agents use these to watch proposal state via policy.local's + // GET /v1/proposals/{id} and /wait endpoints. + AcceptedChunkIds []string `protobuf:"bytes,4,rep,name=accepted_chunk_ids,json=acceptedChunkIds,proto3" json:"accepted_chunk_ids,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SubmitPolicyAnalysisResponse) Reset() { *x = SubmitPolicyAnalysisResponse{} - mi := &file_openshell_proto_msgTypes[76] + mi := &file_openshell_proto_msgTypes[111] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5457,7 +7783,7 @@ func (x *SubmitPolicyAnalysisResponse) String() string { func (*SubmitPolicyAnalysisResponse) ProtoMessage() {} func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[76] + mi := &file_openshell_proto_msgTypes[111] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5470,7 +7796,7 @@ func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SubmitPolicyAnalysisResponse.ProtoReflect.Descriptor instead. func (*SubmitPolicyAnalysisResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{76} + return file_openshell_proto_rawDescGZIP(), []int{111} } func (x *SubmitPolicyAnalysisResponse) GetAcceptedChunks() uint32 { @@ -5494,6 +7820,13 @@ func (x *SubmitPolicyAnalysisResponse) GetRejectionReasons() []string { return nil } +func (x *SubmitPolicyAnalysisResponse) GetAcceptedChunkIds() []string { + if x != nil { + return x.AcceptedChunkIds + } + return nil +} + // Get draft policy for a sandbox. type GetDraftPolicyRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -5507,7 +7840,7 @@ type GetDraftPolicyRequest struct { func (x *GetDraftPolicyRequest) Reset() { *x = GetDraftPolicyRequest{} - mi := &file_openshell_proto_msgTypes[77] + mi := &file_openshell_proto_msgTypes[112] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5519,7 +7852,7 @@ func (x *GetDraftPolicyRequest) String() string { func (*GetDraftPolicyRequest) ProtoMessage() {} func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[77] + mi := &file_openshell_proto_msgTypes[112] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5532,7 +7865,7 @@ func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftPolicyRequest.ProtoReflect.Descriptor instead. func (*GetDraftPolicyRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{77} + return file_openshell_proto_rawDescGZIP(), []int{112} } func (x *GetDraftPolicyRequest) GetName() string { @@ -5565,7 +7898,7 @@ type GetDraftPolicyResponse struct { func (x *GetDraftPolicyResponse) Reset() { *x = GetDraftPolicyResponse{} - mi := &file_openshell_proto_msgTypes[78] + mi := &file_openshell_proto_msgTypes[113] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5577,7 +7910,7 @@ func (x *GetDraftPolicyResponse) String() string { func (*GetDraftPolicyResponse) ProtoMessage() {} func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[78] + mi := &file_openshell_proto_msgTypes[113] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5590,7 +7923,7 @@ func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftPolicyResponse.ProtoReflect.Descriptor instead. func (*GetDraftPolicyResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{78} + return file_openshell_proto_rawDescGZIP(), []int{113} } func (x *GetDraftPolicyResponse) GetChunks() []*PolicyChunk { @@ -5634,7 +7967,7 @@ type ApproveDraftChunkRequest struct { func (x *ApproveDraftChunkRequest) Reset() { *x = ApproveDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[79] + mi := &file_openshell_proto_msgTypes[114] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5646,7 +7979,7 @@ func (x *ApproveDraftChunkRequest) String() string { func (*ApproveDraftChunkRequest) ProtoMessage() {} func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[79] + mi := &file_openshell_proto_msgTypes[114] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5659,7 +7992,7 @@ func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveDraftChunkRequest.ProtoReflect.Descriptor instead. func (*ApproveDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{79} + return file_openshell_proto_rawDescGZIP(), []int{114} } func (x *ApproveDraftChunkRequest) GetName() string { @@ -5688,7 +8021,7 @@ type ApproveDraftChunkResponse struct { func (x *ApproveDraftChunkResponse) Reset() { *x = ApproveDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[80] + mi := &file_openshell_proto_msgTypes[115] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5700,7 +8033,7 @@ func (x *ApproveDraftChunkResponse) String() string { func (*ApproveDraftChunkResponse) ProtoMessage() {} func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[80] + mi := &file_openshell_proto_msgTypes[115] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5713,7 +8046,7 @@ func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveDraftChunkResponse.ProtoReflect.Descriptor instead. func (*ApproveDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{80} + return file_openshell_proto_rawDescGZIP(), []int{115} } func (x *ApproveDraftChunkResponse) GetPolicyVersion() uint32 { @@ -5745,7 +8078,7 @@ type RejectDraftChunkRequest struct { func (x *RejectDraftChunkRequest) Reset() { *x = RejectDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[81] + mi := &file_openshell_proto_msgTypes[116] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5757,7 +8090,7 @@ func (x *RejectDraftChunkRequest) String() string { func (*RejectDraftChunkRequest) ProtoMessage() {} func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[81] + mi := &file_openshell_proto_msgTypes[116] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5770,7 +8103,7 @@ func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectDraftChunkRequest.ProtoReflect.Descriptor instead. func (*RejectDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{81} + return file_openshell_proto_rawDescGZIP(), []int{116} } func (x *RejectDraftChunkRequest) GetName() string { @@ -5802,7 +8135,7 @@ type RejectDraftChunkResponse struct { func (x *RejectDraftChunkResponse) Reset() { *x = RejectDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[82] + mi := &file_openshell_proto_msgTypes[117] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5814,7 +8147,7 @@ func (x *RejectDraftChunkResponse) String() string { func (*RejectDraftChunkResponse) ProtoMessage() {} func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[82] + mi := &file_openshell_proto_msgTypes[117] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5827,7 +8160,7 @@ func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectDraftChunkResponse.ProtoReflect.Descriptor instead. func (*RejectDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{82} + return file_openshell_proto_rawDescGZIP(), []int{117} } // Approve all pending chunks. @@ -5843,7 +8176,7 @@ type ApproveAllDraftChunksRequest struct { func (x *ApproveAllDraftChunksRequest) Reset() { *x = ApproveAllDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[83] + mi := &file_openshell_proto_msgTypes[118] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5855,7 +8188,7 @@ func (x *ApproveAllDraftChunksRequest) String() string { func (*ApproveAllDraftChunksRequest) ProtoMessage() {} func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[83] + mi := &file_openshell_proto_msgTypes[118] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5868,7 +8201,7 @@ func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{83} + return file_openshell_proto_rawDescGZIP(), []int{118} } func (x *ApproveAllDraftChunksRequest) GetName() string { @@ -5901,7 +8234,7 @@ type ApproveAllDraftChunksResponse struct { func (x *ApproveAllDraftChunksResponse) Reset() { *x = ApproveAllDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[84] + mi := &file_openshell_proto_msgTypes[119] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5913,7 +8246,7 @@ func (x *ApproveAllDraftChunksResponse) String() string { func (*ApproveAllDraftChunksResponse) ProtoMessage() {} func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[84] + mi := &file_openshell_proto_msgTypes[119] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5926,7 +8259,7 @@ func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{84} + return file_openshell_proto_rawDescGZIP(), []int{119} } func (x *ApproveAllDraftChunksResponse) GetPolicyVersion() uint32 { @@ -5972,7 +8305,7 @@ type EditDraftChunkRequest struct { func (x *EditDraftChunkRequest) Reset() { *x = EditDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[85] + mi := &file_openshell_proto_msgTypes[120] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5984,7 +8317,7 @@ func (x *EditDraftChunkRequest) String() string { func (*EditDraftChunkRequest) ProtoMessage() {} func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[85] + mi := &file_openshell_proto_msgTypes[120] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5997,7 +8330,7 @@ func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkRequest.ProtoReflect.Descriptor instead. func (*EditDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{85} + return file_openshell_proto_rawDescGZIP(), []int{120} } func (x *EditDraftChunkRequest) GetName() string { @@ -6029,7 +8362,7 @@ type EditDraftChunkResponse struct { func (x *EditDraftChunkResponse) Reset() { *x = EditDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[86] + mi := &file_openshell_proto_msgTypes[121] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6041,7 +8374,7 @@ func (x *EditDraftChunkResponse) String() string { func (*EditDraftChunkResponse) ProtoMessage() {} func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[86] + mi := &file_openshell_proto_msgTypes[121] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6054,7 +8387,7 @@ func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkResponse.ProtoReflect.Descriptor instead. func (*EditDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{86} + return file_openshell_proto_rawDescGZIP(), []int{121} } // Reverse an approval (remove merged rule from active policy). @@ -6070,7 +8403,7 @@ type UndoDraftChunkRequest struct { func (x *UndoDraftChunkRequest) Reset() { *x = UndoDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[87] + mi := &file_openshell_proto_msgTypes[122] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6082,7 +8415,7 @@ func (x *UndoDraftChunkRequest) String() string { func (*UndoDraftChunkRequest) ProtoMessage() {} func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[87] + mi := &file_openshell_proto_msgTypes[122] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6095,7 +8428,7 @@ func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkRequest.ProtoReflect.Descriptor instead. func (*UndoDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{87} + return file_openshell_proto_rawDescGZIP(), []int{122} } func (x *UndoDraftChunkRequest) GetName() string { @@ -6124,7 +8457,7 @@ type UndoDraftChunkResponse struct { func (x *UndoDraftChunkResponse) Reset() { *x = UndoDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[88] + mi := &file_openshell_proto_msgTypes[123] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6136,7 +8469,7 @@ func (x *UndoDraftChunkResponse) String() string { func (*UndoDraftChunkResponse) ProtoMessage() {} func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[88] + mi := &file_openshell_proto_msgTypes[123] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6149,7 +8482,7 @@ func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkResponse.ProtoReflect.Descriptor instead. func (*UndoDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{88} + return file_openshell_proto_rawDescGZIP(), []int{123} } func (x *UndoDraftChunkResponse) GetPolicyVersion() uint32 { @@ -6177,7 +8510,7 @@ type ClearDraftChunksRequest struct { func (x *ClearDraftChunksRequest) Reset() { *x = ClearDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[89] + mi := &file_openshell_proto_msgTypes[124] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6189,7 +8522,7 @@ func (x *ClearDraftChunksRequest) String() string { func (*ClearDraftChunksRequest) ProtoMessage() {} func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[89] + mi := &file_openshell_proto_msgTypes[124] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6202,7 +8535,7 @@ func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ClearDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{89} + return file_openshell_proto_rawDescGZIP(), []int{124} } func (x *ClearDraftChunksRequest) GetName() string { @@ -6222,7 +8555,7 @@ type ClearDraftChunksResponse struct { func (x *ClearDraftChunksResponse) Reset() { *x = ClearDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[90] + mi := &file_openshell_proto_msgTypes[125] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6234,7 +8567,7 @@ func (x *ClearDraftChunksResponse) String() string { func (*ClearDraftChunksResponse) ProtoMessage() {} func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[90] + mi := &file_openshell_proto_msgTypes[125] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6247,7 +8580,7 @@ func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ClearDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{90} + return file_openshell_proto_rawDescGZIP(), []int{125} } func (x *ClearDraftChunksResponse) GetChunksCleared() uint32 { @@ -6268,7 +8601,7 @@ type GetDraftHistoryRequest struct { func (x *GetDraftHistoryRequest) Reset() { *x = GetDraftHistoryRequest{} - mi := &file_openshell_proto_msgTypes[91] + mi := &file_openshell_proto_msgTypes[126] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6280,7 +8613,7 @@ func (x *GetDraftHistoryRequest) String() string { func (*GetDraftHistoryRequest) ProtoMessage() {} func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[91] + mi := &file_openshell_proto_msgTypes[126] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6293,7 +8626,7 @@ func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryRequest.ProtoReflect.Descriptor instead. func (*GetDraftHistoryRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{91} + return file_openshell_proto_rawDescGZIP(), []int{126} } func (x *GetDraftHistoryRequest) GetName() string { @@ -6320,7 +8653,7 @@ type DraftHistoryEntry struct { func (x *DraftHistoryEntry) Reset() { *x = DraftHistoryEntry{} - mi := &file_openshell_proto_msgTypes[92] + mi := &file_openshell_proto_msgTypes[127] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6332,7 +8665,7 @@ func (x *DraftHistoryEntry) String() string { func (*DraftHistoryEntry) ProtoMessage() {} func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[92] + mi := &file_openshell_proto_msgTypes[127] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6345,7 +8678,7 @@ func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftHistoryEntry.ProtoReflect.Descriptor instead. func (*DraftHistoryEntry) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{92} + return file_openshell_proto_rawDescGZIP(), []int{127} } func (x *DraftHistoryEntry) GetTimestampMs() int64 { @@ -6386,7 +8719,7 @@ type GetDraftHistoryResponse struct { func (x *GetDraftHistoryResponse) Reset() { *x = GetDraftHistoryResponse{} - mi := &file_openshell_proto_msgTypes[93] + mi := &file_openshell_proto_msgTypes[128] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6398,7 +8731,7 @@ func (x *GetDraftHistoryResponse) String() string { func (*GetDraftHistoryResponse) ProtoMessage() {} func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[93] + mi := &file_openshell_proto_msgTypes[128] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6411,7 +8744,7 @@ func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryResponse.ProtoReflect.Descriptor instead. func (*GetDraftHistoryResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{93} + return file_openshell_proto_rawDescGZIP(), []int{128} } func (x *GetDraftHistoryResponse) GetEntries() []*DraftHistoryEntry { @@ -6438,7 +8771,7 @@ type PolicyRevisionPayload struct { func (x *PolicyRevisionPayload) Reset() { *x = PolicyRevisionPayload{} - mi := &file_openshell_proto_msgTypes[94] + mi := &file_openshell_proto_msgTypes[129] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6450,7 +8783,7 @@ func (x *PolicyRevisionPayload) String() string { func (*PolicyRevisionPayload) ProtoMessage() {} func (x *PolicyRevisionPayload) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[94] + mi := &file_openshell_proto_msgTypes[129] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6463,7 +8796,7 @@ func (x *PolicyRevisionPayload) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyRevisionPayload.ProtoReflect.Descriptor instead. func (*PolicyRevisionPayload) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{94} + return file_openshell_proto_rawDescGZIP(), []int{129} } func (x *PolicyRevisionPayload) GetPolicy() *sandboxv1.SandboxPolicy { @@ -6516,14 +8849,20 @@ type DraftChunkPayload struct { // Binary path that triggered the denial. Binary string `protobuf:"bytes,9,opt,name=binary,proto3" json:"binary,omitempty"` // Current draft version for the owning sandbox. - DraftVersion int64 `protobuf:"varint,10,opt,name=draft_version,json=draftVersion,proto3" json:"draft_version,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + DraftVersion int64 `protobuf:"varint,10,opt,name=draft_version,json=draftVersion,proto3" json:"draft_version,omitempty"` + // Gateway prover verdict for this chunk; empty until prover runs. + // Mirrors PolicyChunk.validation_result. + ValidationResult string `protobuf:"bytes,11,opt,name=validation_result,json=validationResult,proto3" json:"validation_result,omitempty"` + // Operator-supplied free-form rejection text; empty for non-rejected + // chunks. Mirrors PolicyChunk.rejection_reason. + RejectionReason string `protobuf:"bytes,12,opt,name=rejection_reason,json=rejectionReason,proto3" json:"rejection_reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DraftChunkPayload) Reset() { *x = DraftChunkPayload{} - mi := &file_openshell_proto_msgTypes[95] + mi := &file_openshell_proto_msgTypes[130] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6535,7 +8874,7 @@ func (x *DraftChunkPayload) String() string { func (*DraftChunkPayload) ProtoMessage() {} func (x *DraftChunkPayload) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[95] + mi := &file_openshell_proto_msgTypes[130] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6548,7 +8887,7 @@ func (x *DraftChunkPayload) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftChunkPayload.ProtoReflect.Descriptor instead. func (*DraftChunkPayload) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{95} + return file_openshell_proto_rawDescGZIP(), []int{130} } func (x *DraftChunkPayload) GetRuleName() string { @@ -6621,6 +8960,20 @@ func (x *DraftChunkPayload) GetDraftVersion() int64 { return 0 } +func (x *DraftChunkPayload) GetValidationResult() string { + if x != nil { + return x.ValidationResult + } + return "" +} + +func (x *DraftChunkPayload) GetRejectionReason() string { + if x != nil { + return x.RejectionReason + } + return "" +} + // Internal stored policy revision row materialized from the generic objects table. type StoredPolicyRevision struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -6639,7 +8992,7 @@ type StoredPolicyRevision struct { func (x *StoredPolicyRevision) Reset() { *x = StoredPolicyRevision{} - mi := &file_openshell_proto_msgTypes[96] + mi := &file_openshell_proto_msgTypes[131] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6651,7 +9004,7 @@ func (x *StoredPolicyRevision) String() string { func (*StoredPolicyRevision) ProtoMessage() {} func (x *StoredPolicyRevision) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[96] + mi := &file_openshell_proto_msgTypes[131] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6664,7 +9017,7 @@ func (x *StoredPolicyRevision) ProtoReflect() protoreflect.Message { // Deprecated: Use StoredPolicyRevision.ProtoReflect.Descriptor instead. func (*StoredPolicyRevision) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{96} + return file_openshell_proto_rawDescGZIP(), []int{131} } func (x *StoredPolicyRevision) GetId() string { @@ -6750,13 +9103,17 @@ type StoredDraftChunk struct { HitCount int32 `protobuf:"varint,15,opt,name=hit_count,json=hitCount,proto3" json:"hit_count,omitempty"` FirstSeenMs int64 `protobuf:"varint,16,opt,name=first_seen_ms,json=firstSeenMs,proto3" json:"first_seen_ms,omitempty"` LastSeenMs int64 `protobuf:"varint,17,opt,name=last_seen_ms,json=lastSeenMs,proto3" json:"last_seen_ms,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Gateway prover verdict; empty until the prover runs. See PolicyChunk. + ValidationResult string `protobuf:"bytes,18,opt,name=validation_result,json=validationResult,proto3" json:"validation_result,omitempty"` + // Operator-supplied free-form rejection text. See PolicyChunk. + RejectionReason string `protobuf:"bytes,19,opt,name=rejection_reason,json=rejectionReason,proto3" json:"rejection_reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *StoredDraftChunk) Reset() { *x = StoredDraftChunk{} - mi := &file_openshell_proto_msgTypes[97] + mi := &file_openshell_proto_msgTypes[132] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6768,7 +9125,7 @@ func (x *StoredDraftChunk) String() string { func (*StoredDraftChunk) ProtoMessage() {} func (x *StoredDraftChunk) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[97] + mi := &file_openshell_proto_msgTypes[132] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6781,7 +9138,7 @@ func (x *StoredDraftChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use StoredDraftChunk.ProtoReflect.Descriptor instead. func (*StoredDraftChunk) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{97} + return file_openshell_proto_rawDescGZIP(), []int{132} } func (x *StoredDraftChunk) GetId() string { @@ -6903,6 +9260,20 @@ func (x *StoredDraftChunk) GetLastSeenMs() int64 { return 0 } +func (x *StoredDraftChunk) GetValidationResult() string { + if x != nil { + return x.ValidationResult + } + return "" +} + +func (x *StoredDraftChunk) GetRejectionReason() string { + if x != nil { + return x.RejectionReason + } + return "" +} + var File_openshell_proto protoreflect.FileDescriptor const file_openshell_proto_rawDesc = "" + @@ -6917,17 +9288,20 @@ const file_openshell_proto_rawDesc = "" + "\x04spec\x18\x02 \x01(\v2\x19.openshell.v1.SandboxSpecR\x04spec\x123\n" + "\x06status\x18\x03 \x01(\v2\x1b.openshell.v1.SandboxStatusR\x06status\x120\n" + "\x05phase\x18\x04 \x01(\x0e2\x1a.openshell.v1.SandboxPhaseR\x05phase\x124\n" + - "\x16current_policy_version\x18\x05 \x01(\rR\x14currentPolicyVersion\"\xe0\x02\n" + + "\x16current_policy_version\x18\x05 \x01(\rR\x14currentPolicyVersion\"\xff\x02\n" + "\vSandboxSpec\x12\x1b\n" + "\tlog_level\x18\x01 \x01(\tR\blogLevel\x12L\n" + "\venvironment\x18\x05 \x03(\v2*.openshell.v1.SandboxSpec.EnvironmentEntryR\venvironment\x129\n" + "\btemplate\x18\x06 \x01(\v2\x1d.openshell.v1.SandboxTemplateR\btemplate\x12;\n" + "\x06policy\x18\a \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x06policy\x12\x1c\n" + "\tproviders\x18\b \x03(\tR\tproviders\x12\x10\n" + - "\x03gpu\x18\t \x01(\bR\x03gpu\x1a>\n" + + "\x03gpu\x18\t \x01(\bR\x03gpu\x12\x1d\n" + + "\n" + + "gpu_device\x18\n" + + " \x01(\tR\tgpuDevice\x1a>\n" + "\x10EnvironmentEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xa0\x05\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xe2\x05\n" + "\x0fSandboxTemplate\x12\x14\n" + "\x05image\x18\x01 \x01(\tR\x05image\x12,\n" + "\x12runtime_class_name\x18\x02 \x01(\tR\x10runtimeClassName\x12!\n" + @@ -6936,7 +9310,9 @@ const file_openshell_proto_rawDesc = "" + "\vannotations\x18\x05 \x03(\v2..openshell.v1.SandboxTemplate.AnnotationsEntryR\vannotations\x12P\n" + "\venvironment\x18\x06 \x03(\v2..openshell.v1.SandboxTemplate.EnvironmentEntryR\venvironment\x125\n" + "\tresources\x18\a \x01(\v2\x17.google.protobuf.StructR\tresources\x12M\n" + - "\x16volume_claim_templates\x18\t \x01(\v2\x17.google.protobuf.StructR\x14volumeClaimTemplates\x1a9\n" + + "\x16volume_claim_templates\x18\t \x01(\v2\x17.google.protobuf.StructR\x14volumeClaimTemplates\x12,\n" + + "\x0fuser_namespaces\x18\n" + + " \x01(\bH\x00R\x0euserNamespaces\x88\x01\x01\x1a9\n" + "\vLabelsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" + @@ -6945,7 +9321,8 @@ const file_openshell_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" + "\x10EnvironmentEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xc9\x01\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x12\n" + + "\x10_user_namespaces\"\xc9\x01\n" + "\rSandboxStatus\x12!\n" + "\fsandbox_name\x18\x01 \x01(\tR\vsandboxName\x12\x1b\n" + "\tagent_pod\x18\x02 \x01(\tR\bagentPod\x12\x19\n" + @@ -6983,32 +9360,81 @@ const file_openshell_proto_rawDesc = "" + "\x14ListSandboxesRequest\x12\x14\n" + "\x05limit\x18\x01 \x01(\rR\x05limit\x12\x16\n" + "\x06offset\x18\x02 \x01(\rR\x06offset\x12%\n" + - "\x0elabel_selector\x18\x03 \x01(\tR\rlabelSelector\"*\n" + + "\x0elabel_selector\x18\x03 \x01(\tR\rlabelSelector\"@\n" + + "\x1bListSandboxProvidersRequest\x12!\n" + + "\fsandbox_name\x18\x01 \x01(\tR\vsandboxName\"\xa2\x01\n" + + "\x1cAttachSandboxProviderRequest\x12!\n" + + "\fsandbox_name\x18\x01 \x01(\tR\vsandboxName\x12#\n" + + "\rprovider_name\x18\x02 \x01(\tR\fproviderName\x12:\n" + + "\x19expected_resource_version\x18\x03 \x01(\x04R\x17expectedResourceVersion\"\xa2\x01\n" + + "\x1cDetachSandboxProviderRequest\x12!\n" + + "\fsandbox_name\x18\x01 \x01(\tR\vsandboxName\x12#\n" + + "\rprovider_name\x18\x02 \x01(\tR\fproviderName\x12:\n" + + "\x19expected_resource_version\x18\x03 \x01(\x04R\x17expectedResourceVersion\"*\n" + "\x14DeleteSandboxRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\"B\n" + "\x0fSandboxResponse\x12/\n" + "\asandbox\x18\x01 \x01(\v2\x15.openshell.v1.SandboxR\asandbox\"L\n" + "\x15ListSandboxesResponse\x123\n" + - "\tsandboxes\x18\x01 \x03(\v2\x15.openshell.v1.SandboxR\tsandboxes\"1\n" + + "\tsandboxes\x18\x01 \x03(\v2\x15.openshell.v1.SandboxR\tsandboxes\"^\n" + + "\x1cListSandboxProvidersResponse\x12>\n" + + "\tproviders\x18\x01 \x03(\v2 .openshell.datamodel.v1.ProviderR\tproviders\"l\n" + + "\x1dAttachSandboxProviderResponse\x12/\n" + + "\asandbox\x18\x01 \x01(\v2\x15.openshell.v1.SandboxR\asandbox\x12\x1a\n" + + "\battached\x18\x02 \x01(\bR\battached\"l\n" + + "\x1dDetachSandboxProviderResponse\x12/\n" + + "\asandbox\x18\x01 \x01(\v2\x15.openshell.v1.SandboxR\asandbox\x12\x1a\n" + + "\bdetached\x18\x02 \x01(\bR\bdetached\"1\n" + "\x15DeleteSandboxResponse\x12\x18\n" + "\adeleted\x18\x01 \x01(\bR\adeleted\"8\n" + "\x17CreateSshSessionRequest\x12\x1d\n" + "\n" + - "sandbox_id\x18\x01 \x01(\tR\tsandboxId\"\xb5\x02\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\"\x92\x02\n" + "\x18CreateSshSessionResponse\x12\x1d\n" + "\n" + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x14\n" + "\x05token\x18\x02 \x01(\tR\x05token\x12!\n" + "\fgateway_host\x18\x03 \x01(\tR\vgatewayHost\x12!\n" + "\fgateway_port\x18\x04 \x01(\rR\vgatewayPort\x12%\n" + - "\x0egateway_scheme\x18\x05 \x01(\tR\rgatewayScheme\x12!\n" + - "\fconnect_path\x18\x06 \x01(\tR\vconnectPath\x120\n" + + "\x0egateway_scheme\x18\x05 \x01(\tR\rgatewayScheme\x120\n" + "\x14host_key_fingerprint\x18\a \x01(\tR\x12hostKeyFingerprint\x12\"\n" + - "\rexpires_at_ms\x18\b \x01(\x03R\vexpiresAtMs\"/\n" + + "\rexpires_at_ms\x18\b \x01(\x03R\vexpiresAtMs\"\x83\x01\n" + + "\x14ExposeServiceRequest\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x18\n" + + "\aservice\x18\x02 \x01(\tR\aservice\x12\x1f\n" + + "\vtarget_port\x18\x03 \x01(\rR\n" + + "targetPort\x12\x16\n" + + "\x06domain\x18\x04 \x01(\bR\x06domain\"G\n" + + "\x11GetServiceRequest\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x18\n" + + "\aservice\x18\x02 \x01(\tR\aservice\"]\n" + + "\x13ListServicesRequest\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x14\n" + + "\x05limit\x18\x02 \x01(\rR\x05limit\x12\x16\n" + + "\x06offset\x18\x03 \x01(\rR\x06offset\"Y\n" + + "\x14ListServicesResponse\x12A\n" + + "\bservices\x18\x01 \x03(\v2%.openshell.v1.ServiceEndpointResponseR\bservices\"J\n" + + "\x14DeleteServiceRequest\x12\x18\n" + + "\asandbox\x18\x01 \x01(\tR\asandbox\x12\x18\n" + + "\aservice\x18\x02 \x01(\tR\aservice\"1\n" + + "\x15DeleteServiceResponse\x12\x18\n" + + "\adeleted\x18\x01 \x01(\bR\adeleted\"\xef\x01\n" + + "\x0fServiceEndpoint\x12>\n" + + "\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x02 \x01(\tR\tsandboxId\x12!\n" + + "\fsandbox_name\x18\x03 \x01(\tR\vsandboxName\x12!\n" + + "\fservice_name\x18\x04 \x01(\tR\vserviceName\x12\x1f\n" + + "\vtarget_port\x18\x05 \x01(\rR\n" + + "targetPort\x12\x16\n" + + "\x06domain\x18\x06 \x01(\bR\x06domain\"f\n" + + "\x17ServiceEndpointResponse\x129\n" + + "\bendpoint\x18\x01 \x01(\v2\x1d.openshell.v1.ServiceEndpointR\bendpoint\x12\x10\n" + + "\x03url\x18\x02 \x01(\tR\x03url\"/\n" + "\x17RevokeSshSessionRequest\x12\x14\n" + "\x05token\x18\x01 \x01(\tR\x05token\"4\n" + "\x18RevokeSshSessionResponse\x12\x18\n" + - "\arevoked\x18\x01 \x01(\bR\arevoked\"\xcd\x02\n" + + "\arevoked\x18\x01 \x01(\bR\arevoked\"\xf5\x02\n" + "\x12ExecSandboxRequest\x12\x1d\n" + "\n" + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x18\n" + @@ -7017,7 +9443,9 @@ const file_openshell_proto_rawDesc = "" + "\venvironment\x18\x04 \x03(\v21.openshell.v1.ExecSandboxRequest.EnvironmentEntryR\venvironment\x12'\n" + "\x0ftimeout_seconds\x18\x05 \x01(\rR\x0etimeoutSeconds\x12\x14\n" + "\x05stdin\x18\x06 \x01(\fR\x05stdin\x12\x10\n" + - "\x03tty\x18\a \x01(\bR\x03tty\x1a>\n" + + "\x03tty\x18\a \x01(\bR\x03tty\x12\x12\n" + + "\x04cols\x18\b \x01(\rR\x04cols\x12\x12\n" + + "\x04rows\x18\t \x01(\rR\x04rows\x1a>\n" + "\x10EnvironmentEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"'\n" + @@ -7031,7 +9459,28 @@ const file_openshell_proto_rawDesc = "" + "\x06stdout\x18\x01 \x01(\v2\x1f.openshell.v1.ExecSandboxStdoutH\x00R\x06stdout\x129\n" + "\x06stderr\x18\x02 \x01(\v2\x1f.openshell.v1.ExecSandboxStderrH\x00R\x06stderr\x123\n" + "\x04exit\x18\x03 \x01(\v2\x1d.openshell.v1.ExecSandboxExitH\x00R\x04exitB\t\n" + - "\apayload\"\xbf\x01\n" + + "\apayload\"\xed\x01\n" + + "\x0eTcpForwardInit\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x1d\n" + + "\n" + + "service_id\x18\x04 \x01(\tR\tserviceId\x120\n" + + "\x03ssh\x18\x05 \x01(\v2\x1c.openshell.v1.SshRelayTargetH\x00R\x03ssh\x120\n" + + "\x03tcp\x18\x06 \x01(\v2\x1c.openshell.v1.TcpRelayTargetH\x00R\x03tcp\x12/\n" + + "\x13authorization_token\x18\a \x01(\tR\x12authorizationTokenB\b\n" + + "\x06target\"f\n" + + "\x0fTcpForwardFrame\x122\n" + + "\x04init\x18\x01 \x01(\v2\x1c.openshell.v1.TcpForwardInitH\x00R\x04init\x12\x14\n" + + "\x04data\x18\x02 \x01(\fH\x00R\x04dataB\t\n" + + "\apayload\"\xb0\x01\n" + + "\x10ExecSandboxInput\x128\n" + + "\x05start\x18\x01 \x01(\v2 .openshell.v1.ExecSandboxRequestH\x00R\x05start\x12\x16\n" + + "\x05stdin\x18\x02 \x01(\fH\x00R\x05stdin\x12?\n" + + "\x06resize\x18\x03 \x01(\v2%.openshell.v1.ExecSandboxWindowResizeH\x00R\x06resizeB\t\n" + + "\apayload\"A\n" + + "\x17ExecSandboxWindowResize\x12\x12\n" + + "\x04cols\x18\x01 \x01(\rR\x04cols\x12\x12\n" + + "\x04rows\x18\x02 \x01(\rR\x04rows\"\xbf\x01\n" + "\n" + "SshSession\x12>\n" + "\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x12\x1d\n" + @@ -7091,17 +9540,75 @@ const file_openshell_proto_rawDesc = "" + "\x10ProviderResponse\x12<\n" + "\bprovider\x18\x01 \x01(\v2 .openshell.datamodel.v1.ProviderR\bprovider\"W\n" + "\x15ListProvidersResponse\x12>\n" + - "\tproviders\x18\x01 \x03(\v2 .openshell.datamodel.v1.ProviderR\tproviders\"2\n" + + "\tproviders\x18\x01 \x03(\v2 .openshell.datamodel.v1.ProviderR\tproviders\"K\n" + + "\x1bListProviderProfilesRequest\x12\x14\n" + + "\x05limit\x18\x01 \x01(\rR\x05limit\x12\x16\n" + + "\x06offset\x18\x02 \x01(\rR\x06offset\"+\n" + + "\x19GetProviderProfileRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"l\n" + + "\x19ProviderProfileImportItem\x127\n" + + "\aprofile\x18\x01 \x01(\v2\x1d.openshell.v1.ProviderProfileR\aprofile\x12\x16\n" + + "\x06source\x18\x02 \x01(\tR\x06source\"\x9e\x01\n" + + "\x19ProviderProfileDiagnostic\x12\x16\n" + + "\x06source\x18\x01 \x01(\tR\x06source\x12\x1d\n" + + "\n" + + "profile_id\x18\x02 \x01(\tR\tprofileId\x12\x14\n" + + "\x05field\x18\x03 \x01(\tR\x05field\x12\x18\n" + + "\amessage\x18\x04 \x01(\tR\amessage\x12\x1a\n" + + "\bseverity\x18\x05 \x01(\tR\bseverity\"\xe9\x01\n" + + "\x19ProviderProfileCredential\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x19\n" + + "\benv_vars\x18\x03 \x03(\tR\aenvVars\x12\x1a\n" + + "\brequired\x18\x04 \x01(\bR\brequired\x12\x1d\n" + + "\n" + + "auth_style\x18\x05 \x01(\tR\tauthStyle\x12\x1f\n" + + "\vheader_name\x18\x06 \x01(\tR\n" + + "headerName\x12\x1f\n" + + "\vquery_param\x18\a \x01(\tR\n" + + "queryParam\"\xa7\x03\n" + + "\x0fProviderProfile\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12!\n" + + "\fdisplay_name\x18\x02 \x01(\tR\vdisplayName\x12 \n" + + "\vdescription\x18\x03 \x01(\tR\vdescription\x12A\n" + + "\bcategory\x18\x04 \x01(\x0e2%.openshell.v1.ProviderProfileCategoryR\bcategory\x12I\n" + + "\vcredentials\x18\x05 \x03(\v2'.openshell.v1.ProviderProfileCredentialR\vcredentials\x12C\n" + + "\tendpoints\x18\x06 \x03(\v2%.openshell.sandbox.v1.NetworkEndpointR\tendpoints\x12?\n" + + "\bbinaries\x18\a \x03(\v2#.openshell.sandbox.v1.NetworkBinaryR\bbinaries\x12+\n" + + "\x11inference_capable\x18\b \x01(\bR\x10inferenceCapable\"\x90\x01\n" + + "\x15StoredProviderProfile\x12>\n" + + "\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x127\n" + + "\aprofile\x18\x02 \x01(\v2\x1d.openshell.v1.ProviderProfileR\aprofile\"R\n" + + "\x17ProviderProfileResponse\x127\n" + + "\aprofile\x18\x01 \x01(\v2\x1d.openshell.v1.ProviderProfileR\aprofile\"Y\n" + + "\x1cListProviderProfilesResponse\x129\n" + + "\bprofiles\x18\x01 \x03(\v2\x1d.openshell.v1.ProviderProfileR\bprofiles\"d\n" + + "\x1dImportProviderProfilesRequest\x12C\n" + + "\bprofiles\x18\x01 \x03(\v2'.openshell.v1.ProviderProfileImportItemR\bprofiles\"\xc2\x01\n" + + "\x1eImportProviderProfilesResponse\x12I\n" + + "\vdiagnostics\x18\x01 \x03(\v2'.openshell.v1.ProviderProfileDiagnosticR\vdiagnostics\x129\n" + + "\bprofiles\x18\x02 \x03(\v2\x1d.openshell.v1.ProviderProfileR\bprofiles\x12\x1a\n" + + "\bimported\x18\x03 \x01(\bR\bimported\"b\n" + + "\x1bLintProviderProfilesRequest\x12C\n" + + "\bprofiles\x18\x01 \x03(\v2'.openshell.v1.ProviderProfileImportItemR\bprofiles\"\x7f\n" + + "\x1cLintProviderProfilesResponse\x12I\n" + + "\vdiagnostics\x18\x01 \x03(\v2'.openshell.v1.ProviderProfileDiagnosticR\vdiagnostics\x12\x14\n" + + "\x05valid\x18\x02 \x01(\bR\x05valid\"2\n" + "\x16DeleteProviderResponse\x12\x18\n" + + "\adeleted\x18\x01 \x01(\bR\adeleted\".\n" + + "\x1cDeleteProviderProfileRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"9\n" + + "\x1dDeleteProviderProfileResponse\x12\x18\n" + "\adeleted\x18\x01 \x01(\bR\adeleted\"E\n" + "$GetSandboxProviderEnvironmentRequest\x12\x1d\n" + "\n" + - "sandbox_id\x18\x01 \x01(\tR\tsandboxId\"\xcf\x01\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\"\x83\x02\n" + "%GetSandboxProviderEnvironmentResponse\x12f\n" + - "\venvironment\x18\x01 \x03(\v2D.openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntryR\venvironment\x1a>\n" + + "\venvironment\x18\x01 \x03(\v2D.openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntryR\venvironment\x122\n" + + "\x15provider_env_revision\x18\x02 \x01(\x04R\x13providerEnvRevision\x1a>\n" + "\x10EnvironmentEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xde\x02\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x9a\x03\n" + "\x13UpdateConfigRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12;\n" + "\x06policy\x18\x02 \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x06policy\x12\x1f\n" + @@ -7110,7 +9617,8 @@ const file_openshell_proto_rawDesc = "" + "\rsetting_value\x18\x04 \x01(\v2\".openshell.sandbox.v1.SettingValueR\fsettingValue\x12%\n" + "\x0edelete_setting\x18\x05 \x01(\bR\rdeleteSetting\x12\x16\n" + "\x06global\x18\x06 \x01(\bR\x06global\x12M\n" + - "\x10merge_operations\x18\a \x03(\v2\".openshell.v1.PolicyMergeOperationR\x0fmergeOperations\"\xc7\x03\n" + + "\x10merge_operations\x18\a \x03(\v2\".openshell.v1.PolicyMergeOperationR\x0fmergeOperations\x12:\n" + + "\x19expected_resource_version\x18\b \x01(\x04R\x17expectedResourceVersion\"\xc7\x03\n" + "\x14PolicyMergeOperation\x129\n" + "\badd_rule\x18\x01 \x01(\v2\x1c.openshell.v1.AddNetworkRuleH\x00R\aaddRule\x12N\n" + "\x0fremove_endpoint\x18\x02 \x01(\v2#.openshell.v1.RemoveNetworkEndpointH\x00R\x0eremoveEndpoint\x12B\n" + @@ -7224,10 +9732,19 @@ const file_openshell_proto_rawDesc = "" + "\x0fSessionRejected\x12\x16\n" + "\x06reason\x18\x01 \x01(\tR\x06reason\"\x15\n" + "\x13SupervisorHeartbeat\"\x12\n" + - "\x10GatewayHeartbeat\"*\n" + + "\x10GatewayHeartbeat\"\xb7\x01\n" + "\tRelayOpen\x12\x1d\n" + "\n" + - "channel_id\x18\x01 \x01(\tR\tchannelId\"*\n" + + "channel_id\x18\x01 \x01(\tR\tchannelId\x120\n" + + "\x03ssh\x18\x02 \x01(\v2\x1c.openshell.v1.SshRelayTargetH\x00R\x03ssh\x120\n" + + "\x03tcp\x18\x03 \x01(\v2\x1c.openshell.v1.TcpRelayTargetH\x00R\x03tcp\x12\x1d\n" + + "\n" + + "service_id\x18\x05 \x01(\tR\tserviceIdB\b\n" + + "\x06target\"\x10\n" + + "\x0eSshRelayTarget\"8\n" + + "\x0eTcpRelayTarget\x12\x12\n" + + "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" + + "\x04port\x18\x02 \x01(\rR\x04port\"*\n" + "\tRelayInit\x12\x1d\n" + "\n" + "channel_id\x18\x01 \x01(\tR\tchannelId\"\\\n" + @@ -7275,7 +9792,7 @@ const file_openshell_proto_rawDesc = "" + "persistent\x12!\n" + "\fdenial_stage\x18\x0f \x01(\tR\vdenialStage\x12K\n" + "\x12l7_request_samples\x18\x10 \x03(\v2\x1d.openshell.v1.L7RequestSampleR\x10l7RequestSamples\x120\n" + - "\x14l7_inspection_active\x18\x11 \x01(\bR\x12l7InspectionActive\"\xbc\x04\n" + + "\x14l7_inspection_active\x18\x11 \x01(\bR\x12l7InspectionActive\"\x94\x05\n" + "\vPolicyChunk\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x16\n" + "\x06status\x18\x02 \x01(\tR\x06status\x12\x1b\n" + @@ -7296,7 +9813,9 @@ const file_openshell_proto_rawDesc = "" + "\rfirst_seen_ms\x18\x0e \x01(\x03R\vfirstSeenMs\x12 \n" + "\flast_seen_ms\x18\x0f \x01(\x03R\n" + "lastSeenMs\x12\x16\n" + - "\x06binary\x18\x10 \x01(\tR\x06binary\"\x96\x01\n" + + "\x06binary\x18\x10 \x01(\tR\x06binary\x12+\n" + + "\x11validation_result\x18\x11 \x01(\tR\x10validationResult\x12)\n" + + "\x10rejection_reason\x18\x12 \x01(\tR\x0frejectionReason\"\x96\x01\n" + "\x11DraftPolicyUpdate\x12#\n" + "\rdraft_version\x18\x01 \x01(\x04R\fdraftVersion\x12\x1d\n" + "\n" + @@ -7307,11 +9826,12 @@ const file_openshell_proto_rawDesc = "" + "\tsummaries\x18\x01 \x03(\v2\x1b.openshell.v1.DenialSummaryR\tsummaries\x12B\n" + "\x0fproposed_chunks\x18\x02 \x03(\v2\x19.openshell.v1.PolicyChunkR\x0eproposedChunks\x12#\n" + "\ranalysis_mode\x18\x03 \x01(\tR\fanalysisMode\x12\x12\n" + - "\x04name\x18\x04 \x01(\tR\x04name\"\x9d\x01\n" + + "\x04name\x18\x04 \x01(\tR\x04name\"\xcb\x01\n" + "\x1cSubmitPolicyAnalysisResponse\x12'\n" + "\x0faccepted_chunks\x18\x01 \x01(\rR\x0eacceptedChunks\x12'\n" + "\x0frejected_chunks\x18\x02 \x01(\rR\x0erejectedChunks\x12+\n" + - "\x11rejection_reasons\x18\x03 \x03(\tR\x10rejectionReasons\"P\n" + + "\x11rejection_reasons\x18\x03 \x03(\tR\x10rejectionReasons\x12,\n" + + "\x12accepted_chunk_ids\x18\x04 \x03(\tR\x10acceptedChunkIds\"P\n" + "\x15GetDraftPolicyRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12#\n" + "\rstatus_filter\x18\x02 \x01(\tR\fstatusFilter\"\xc8\x01\n" + @@ -7373,7 +9893,7 @@ const file_openshell_proto_rawDesc = "" + "\n" + "load_error\x18\x03 \x01(\tR\tloadError\x12 \n" + "\floaded_at_ms\x18\x04 \x01(\x03R\n" + - "loadedAtMs\"\xec\x02\n" + + "loadedAtMs\"\xc4\x03\n" + "\x11DraftChunkPayload\x12\x1b\n" + "\trule_name\x18\x01 \x01(\tR\bruleName\x12L\n" + "\rproposed_rule\x18\x02 \x01(\v2'.openshell.sandbox.v1.NetworkPolicyRuleR\fproposedRule\x12\x1c\n" + @@ -7387,7 +9907,9 @@ const file_openshell_proto_rawDesc = "" + "\x04port\x18\b \x01(\x05R\x04port\x12\x16\n" + "\x06binary\x18\t \x01(\tR\x06binary\x12#\n" + "\rdraft_version\x18\n" + - " \x01(\x03R\fdraftVersion\"\xce\x02\n" + + " \x01(\x03R\fdraftVersion\x12+\n" + + "\x11validation_result\x18\v \x01(\tR\x10validationResult\x12)\n" + + "\x10rejection_reason\x18\f \x01(\tR\x0frejectionReason\"\xce\x02\n" + "\x14StoredPolicyRevision\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n" + "\n" + @@ -7403,7 +9925,7 @@ const file_openshell_proto_rawDesc = "" + "\floaded_at_ms\x18\t \x01(\x03H\x01R\n" + "loadedAtMs\x88\x01\x01B\r\n" + "\v_load_errorB\x0f\n" + - "\r_loaded_at_ms\"\xa7\x04\n" + + "\r_loaded_at_ms\"\xff\x04\n" + "\x10StoredDraftChunk\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n" + "\n" + @@ -7426,7 +9948,9 @@ const file_openshell_proto_rawDesc = "" + "\thit_count\x18\x0f \x01(\x05R\bhitCount\x12\"\n" + "\rfirst_seen_ms\x18\x10 \x01(\x03R\vfirstSeenMs\x12 \n" + "\flast_seen_ms\x18\x11 \x01(\x03R\n" + - "lastSeenMsB\x10\n" + + "lastSeenMs\x12+\n" + + "\x11validation_result\x18\x12 \x01(\tR\x10validationResult\x12)\n" + + "\x10rejection_reason\x18\x13 \x01(\tR\x0frejectionReasonB\x10\n" + "\x0e_decided_at_ms*\xb6\x01\n" + "\fSandboxPhase\x12\x1d\n" + "\x19SANDBOX_PHASE_UNSPECIFIED\x10\x00\x12\x1e\n" + @@ -7434,7 +9958,16 @@ const file_openshell_proto_rawDesc = "" + "\x13SANDBOX_PHASE_READY\x10\x02\x12\x17\n" + "\x13SANDBOX_PHASE_ERROR\x10\x03\x12\x1a\n" + "\x16SANDBOX_PHASE_DELETING\x10\x04\x12\x19\n" + - "\x15SANDBOX_PHASE_UNKNOWN\x10\x05*\x9a\x01\n" + + "\x15SANDBOX_PHASE_UNKNOWN\x10\x05*\xdb\x02\n" + + "\x17ProviderProfileCategory\x12)\n" + + "%PROVIDER_PROFILE_CATEGORY_UNSPECIFIED\x10\x00\x12#\n" + + "\x1fPROVIDER_PROFILE_CATEGORY_OTHER\x10\x01\x12'\n" + + "#PROVIDER_PROFILE_CATEGORY_INFERENCE\x10\x02\x12#\n" + + "\x1fPROVIDER_PROFILE_CATEGORY_AGENT\x10\x03\x12,\n" + + "(PROVIDER_PROFILE_CATEGORY_SOURCE_CONTROL\x10\x04\x12'\n" + + "#PROVIDER_PROFILE_CATEGORY_MESSAGING\x10\x05\x12\"\n" + + "\x1ePROVIDER_PROFILE_CATEGORY_DATA\x10\x06\x12'\n" + + "#PROVIDER_PROFILE_CATEGORY_KNOWLEDGE\x10\a*\x9a\x01\n" + "\fPolicyStatus\x12\x1d\n" + "\x19POLICY_STATUS_UNSPECIFIED\x10\x00\x12\x19\n" + "\x15POLICY_STATUS_PENDING\x10\x01\x12\x18\n" + @@ -7445,22 +9978,38 @@ const file_openshell_proto_rawDesc = "" + "\x1aSERVICE_STATUS_UNSPECIFIED\x10\x00\x12\x1a\n" + "\x16SERVICE_STATUS_HEALTHY\x10\x01\x12\x1b\n" + "\x17SERVICE_STATUS_DEGRADED\x10\x02\x12\x1c\n" + - "\x18SERVICE_STATUS_UNHEALTHY\x10\x032\xc0\x19\n" + + "\x18SERVICE_STATUS_UNHEALTHY\x10\x032\xcf$\n" + "\tOpenShell\x12C\n" + "\x06Health\x12\x1b.openshell.v1.HealthRequest\x1a\x1c.openshell.v1.HealthResponse\x12R\n" + "\rCreateSandbox\x12\".openshell.v1.CreateSandboxRequest\x1a\x1d.openshell.v1.SandboxResponse\x12L\n" + "\n" + "GetSandbox\x12\x1f.openshell.v1.GetSandboxRequest\x1a\x1d.openshell.v1.SandboxResponse\x12X\n" + - "\rListSandboxes\x12\".openshell.v1.ListSandboxesRequest\x1a#.openshell.v1.ListSandboxesResponse\x12X\n" + + "\rListSandboxes\x12\".openshell.v1.ListSandboxesRequest\x1a#.openshell.v1.ListSandboxesResponse\x12m\n" + + "\x14ListSandboxProviders\x12).openshell.v1.ListSandboxProvidersRequest\x1a*.openshell.v1.ListSandboxProvidersResponse\x12p\n" + + "\x15AttachSandboxProvider\x12*.openshell.v1.AttachSandboxProviderRequest\x1a+.openshell.v1.AttachSandboxProviderResponse\x12p\n" + + "\x15DetachSandboxProvider\x12*.openshell.v1.DetachSandboxProviderRequest\x1a+.openshell.v1.DetachSandboxProviderResponse\x12X\n" + "\rDeleteSandbox\x12\".openshell.v1.DeleteSandboxRequest\x1a#.openshell.v1.DeleteSandboxResponse\x12a\n" + - "\x10CreateSshSession\x12%.openshell.v1.CreateSshSessionRequest\x1a&.openshell.v1.CreateSshSessionResponse\x12a\n" + + "\x10CreateSshSession\x12%.openshell.v1.CreateSshSessionRequest\x1a&.openshell.v1.CreateSshSessionResponse\x12Z\n" + + "\rExposeService\x12\".openshell.v1.ExposeServiceRequest\x1a%.openshell.v1.ServiceEndpointResponse\x12T\n" + + "\n" + + "GetService\x12\x1f.openshell.v1.GetServiceRequest\x1a%.openshell.v1.ServiceEndpointResponse\x12U\n" + + "\fListServices\x12!.openshell.v1.ListServicesRequest\x1a\".openshell.v1.ListServicesResponse\x12X\n" + + "\rDeleteService\x12\".openshell.v1.DeleteServiceRequest\x1a#.openshell.v1.DeleteServiceResponse\x12a\n" + "\x10RevokeSshSession\x12%.openshell.v1.RevokeSshSessionRequest\x1a&.openshell.v1.RevokeSshSessionResponse\x12Q\n" + - "\vExecSandbox\x12 .openshell.v1.ExecSandboxRequest\x1a\x1e.openshell.v1.ExecSandboxEvent0\x01\x12U\n" + + "\vExecSandbox\x12 .openshell.v1.ExecSandboxRequest\x1a\x1e.openshell.v1.ExecSandboxEvent0\x01\x12N\n" + + "\n" + + "ForwardTcp\x12\x1d.openshell.v1.TcpForwardFrame\x1a\x1d.openshell.v1.TcpForwardFrame(\x010\x01\x12\\\n" + + "\x16ExecSandboxInteractive\x12\x1e.openshell.v1.ExecSandboxInput\x1a\x1e.openshell.v1.ExecSandboxEvent(\x010\x01\x12U\n" + "\x0eCreateProvider\x12#.openshell.v1.CreateProviderRequest\x1a\x1e.openshell.v1.ProviderResponse\x12O\n" + "\vGetProvider\x12 .openshell.v1.GetProviderRequest\x1a\x1e.openshell.v1.ProviderResponse\x12X\n" + - "\rListProviders\x12\".openshell.v1.ListProvidersRequest\x1a#.openshell.v1.ListProvidersResponse\x12U\n" + + "\rListProviders\x12\".openshell.v1.ListProvidersRequest\x1a#.openshell.v1.ListProvidersResponse\x12m\n" + + "\x14ListProviderProfiles\x12).openshell.v1.ListProviderProfilesRequest\x1a*.openshell.v1.ListProviderProfilesResponse\x12d\n" + + "\x12GetProviderProfile\x12'.openshell.v1.GetProviderProfileRequest\x1a%.openshell.v1.ProviderProfileResponse\x12s\n" + + "\x16ImportProviderProfiles\x12+.openshell.v1.ImportProviderProfilesRequest\x1a,.openshell.v1.ImportProviderProfilesResponse\x12m\n" + + "\x14LintProviderProfiles\x12).openshell.v1.LintProviderProfilesRequest\x1a*.openshell.v1.LintProviderProfilesResponse\x12U\n" + "\x0eUpdateProvider\x12#.openshell.v1.UpdateProviderRequest\x1a\x1e.openshell.v1.ProviderResponse\x12[\n" + - "\x0eDeleteProvider\x12#.openshell.v1.DeleteProviderRequest\x1a$.openshell.v1.DeleteProviderResponse\x12q\n" + + "\x0eDeleteProvider\x12#.openshell.v1.DeleteProviderRequest\x1a$.openshell.v1.DeleteProviderResponse\x12p\n" + + "\x15DeleteProviderProfile\x12*.openshell.v1.DeleteProviderProfileRequest\x1a+.openshell.v1.DeleteProviderProfileResponse\x12q\n" + "\x10GetSandboxConfig\x12-.openshell.sandbox.v1.GetSandboxConfigRequest\x1a..openshell.sandbox.v1.GetSandboxConfigResponse\x12q\n" + "\x10GetGatewayConfig\x12-.openshell.sandbox.v1.GetGatewayConfigRequest\x1a..openshell.sandbox.v1.GetGatewayConfigResponse\x12U\n" + "\fUpdateConfig\x12!.openshell.v1.UpdateConfigRequest\x1a\".openshell.v1.UpdateConfigResponse\x12s\n" + @@ -7481,7 +10030,8 @@ const file_openshell_proto_rawDesc = "" + "\x0eEditDraftChunk\x12#.openshell.v1.EditDraftChunkRequest\x1a$.openshell.v1.EditDraftChunkResponse\x12[\n" + "\x0eUndoDraftChunk\x12#.openshell.v1.UndoDraftChunkRequest\x1a$.openshell.v1.UndoDraftChunkResponse\x12a\n" + "\x10ClearDraftChunks\x12%.openshell.v1.ClearDraftChunksRequest\x1a&.openshell.v1.ClearDraftChunksResponse\x12^\n" + - "\x0fGetDraftHistory\x12$.openshell.v1.GetDraftHistoryRequest\x1a%.openshell.v1.GetDraftHistoryResponseb\x06proto3" + "\x0fGetDraftHistory\x12$.openshell.v1.GetDraftHistoryRequest\x1a%.openshell.v1.GetDraftHistoryResponseB\xb2\x01\n" + + "\x10com.openshell.v1B\x0eOpenshellProtoP\x01Z=github.com/kagent-dev/kagent/go/api/openshell/gen/openshellv1\xa2\x02\x03OXX\xaa\x02\fOpenshell.V1\xca\x02\fOpenshell\\V1\xe2\x02\x18Openshell\\V1\\GPBMetadata\xea\x02\rOpenshell::V1b\x06proto3" var ( file_openshell_proto_rawDescOnce sync.Once @@ -7495,279 +10045,372 @@ func file_openshell_proto_rawDescGZIP() []byte { return file_openshell_proto_rawDescData } -var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 107) +var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 4) +var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 142) var file_openshell_proto_goTypes = []any{ (SandboxPhase)(0), // 0: openshell.v1.SandboxPhase - (PolicyStatus)(0), // 1: openshell.v1.PolicyStatus - (ServiceStatus)(0), // 2: openshell.v1.ServiceStatus - (*HealthRequest)(nil), // 3: openshell.v1.HealthRequest - (*HealthResponse)(nil), // 4: openshell.v1.HealthResponse - (*Sandbox)(nil), // 5: openshell.v1.Sandbox - (*SandboxSpec)(nil), // 6: openshell.v1.SandboxSpec - (*SandboxTemplate)(nil), // 7: openshell.v1.SandboxTemplate - (*SandboxStatus)(nil), // 8: openshell.v1.SandboxStatus - (*SandboxCondition)(nil), // 9: openshell.v1.SandboxCondition - (*PlatformEvent)(nil), // 10: openshell.v1.PlatformEvent - (*CreateSandboxRequest)(nil), // 11: openshell.v1.CreateSandboxRequest - (*GetSandboxRequest)(nil), // 12: openshell.v1.GetSandboxRequest - (*ListSandboxesRequest)(nil), // 13: openshell.v1.ListSandboxesRequest - (*DeleteSandboxRequest)(nil), // 14: openshell.v1.DeleteSandboxRequest - (*SandboxResponse)(nil), // 15: openshell.v1.SandboxResponse - (*ListSandboxesResponse)(nil), // 16: openshell.v1.ListSandboxesResponse - (*DeleteSandboxResponse)(nil), // 17: openshell.v1.DeleteSandboxResponse - (*CreateSshSessionRequest)(nil), // 18: openshell.v1.CreateSshSessionRequest - (*CreateSshSessionResponse)(nil), // 19: openshell.v1.CreateSshSessionResponse - (*RevokeSshSessionRequest)(nil), // 20: openshell.v1.RevokeSshSessionRequest - (*RevokeSshSessionResponse)(nil), // 21: openshell.v1.RevokeSshSessionResponse - (*ExecSandboxRequest)(nil), // 22: openshell.v1.ExecSandboxRequest - (*ExecSandboxStdout)(nil), // 23: openshell.v1.ExecSandboxStdout - (*ExecSandboxStderr)(nil), // 24: openshell.v1.ExecSandboxStderr - (*ExecSandboxExit)(nil), // 25: openshell.v1.ExecSandboxExit - (*ExecSandboxEvent)(nil), // 26: openshell.v1.ExecSandboxEvent - (*SshSession)(nil), // 27: openshell.v1.SshSession - (*WatchSandboxRequest)(nil), // 28: openshell.v1.WatchSandboxRequest - (*SandboxStreamEvent)(nil), // 29: openshell.v1.SandboxStreamEvent - (*SandboxLogLine)(nil), // 30: openshell.v1.SandboxLogLine - (*SandboxStreamWarning)(nil), // 31: openshell.v1.SandboxStreamWarning - (*CreateProviderRequest)(nil), // 32: openshell.v1.CreateProviderRequest - (*GetProviderRequest)(nil), // 33: openshell.v1.GetProviderRequest - (*ListProvidersRequest)(nil), // 34: openshell.v1.ListProvidersRequest - (*UpdateProviderRequest)(nil), // 35: openshell.v1.UpdateProviderRequest - (*DeleteProviderRequest)(nil), // 36: openshell.v1.DeleteProviderRequest - (*ProviderResponse)(nil), // 37: openshell.v1.ProviderResponse - (*ListProvidersResponse)(nil), // 38: openshell.v1.ListProvidersResponse - (*DeleteProviderResponse)(nil), // 39: openshell.v1.DeleteProviderResponse - (*GetSandboxProviderEnvironmentRequest)(nil), // 40: openshell.v1.GetSandboxProviderEnvironmentRequest - (*GetSandboxProviderEnvironmentResponse)(nil), // 41: openshell.v1.GetSandboxProviderEnvironmentResponse - (*UpdateConfigRequest)(nil), // 42: openshell.v1.UpdateConfigRequest - (*PolicyMergeOperation)(nil), // 43: openshell.v1.PolicyMergeOperation - (*AddNetworkRule)(nil), // 44: openshell.v1.AddNetworkRule - (*RemoveNetworkEndpoint)(nil), // 45: openshell.v1.RemoveNetworkEndpoint - (*RemoveNetworkRule)(nil), // 46: openshell.v1.RemoveNetworkRule - (*AddDenyRules)(nil), // 47: openshell.v1.AddDenyRules - (*AddAllowRules)(nil), // 48: openshell.v1.AddAllowRules - (*RemoveNetworkBinary)(nil), // 49: openshell.v1.RemoveNetworkBinary - (*UpdateConfigResponse)(nil), // 50: openshell.v1.UpdateConfigResponse - (*GetSandboxPolicyStatusRequest)(nil), // 51: openshell.v1.GetSandboxPolicyStatusRequest - (*GetSandboxPolicyStatusResponse)(nil), // 52: openshell.v1.GetSandboxPolicyStatusResponse - (*ListSandboxPoliciesRequest)(nil), // 53: openshell.v1.ListSandboxPoliciesRequest - (*ListSandboxPoliciesResponse)(nil), // 54: openshell.v1.ListSandboxPoliciesResponse - (*ReportPolicyStatusRequest)(nil), // 55: openshell.v1.ReportPolicyStatusRequest - (*ReportPolicyStatusResponse)(nil), // 56: openshell.v1.ReportPolicyStatusResponse - (*SandboxPolicyRevision)(nil), // 57: openshell.v1.SandboxPolicyRevision - (*GetSandboxLogsRequest)(nil), // 58: openshell.v1.GetSandboxLogsRequest - (*PushSandboxLogsRequest)(nil), // 59: openshell.v1.PushSandboxLogsRequest - (*PushSandboxLogsResponse)(nil), // 60: openshell.v1.PushSandboxLogsResponse - (*GetSandboxLogsResponse)(nil), // 61: openshell.v1.GetSandboxLogsResponse - (*SupervisorMessage)(nil), // 62: openshell.v1.SupervisorMessage - (*GatewayMessage)(nil), // 63: openshell.v1.GatewayMessage - (*SupervisorHello)(nil), // 64: openshell.v1.SupervisorHello - (*SessionAccepted)(nil), // 65: openshell.v1.SessionAccepted - (*SessionRejected)(nil), // 66: openshell.v1.SessionRejected - (*SupervisorHeartbeat)(nil), // 67: openshell.v1.SupervisorHeartbeat - (*GatewayHeartbeat)(nil), // 68: openshell.v1.GatewayHeartbeat - (*RelayOpen)(nil), // 69: openshell.v1.RelayOpen - (*RelayInit)(nil), // 70: openshell.v1.RelayInit - (*RelayFrame)(nil), // 71: openshell.v1.RelayFrame - (*RelayOpenResult)(nil), // 72: openshell.v1.RelayOpenResult - (*RelayClose)(nil), // 73: openshell.v1.RelayClose - (*L7RequestSample)(nil), // 74: openshell.v1.L7RequestSample - (*DenialSummary)(nil), // 75: openshell.v1.DenialSummary - (*PolicyChunk)(nil), // 76: openshell.v1.PolicyChunk - (*DraftPolicyUpdate)(nil), // 77: openshell.v1.DraftPolicyUpdate - (*SubmitPolicyAnalysisRequest)(nil), // 78: openshell.v1.SubmitPolicyAnalysisRequest - (*SubmitPolicyAnalysisResponse)(nil), // 79: openshell.v1.SubmitPolicyAnalysisResponse - (*GetDraftPolicyRequest)(nil), // 80: openshell.v1.GetDraftPolicyRequest - (*GetDraftPolicyResponse)(nil), // 81: openshell.v1.GetDraftPolicyResponse - (*ApproveDraftChunkRequest)(nil), // 82: openshell.v1.ApproveDraftChunkRequest - (*ApproveDraftChunkResponse)(nil), // 83: openshell.v1.ApproveDraftChunkResponse - (*RejectDraftChunkRequest)(nil), // 84: openshell.v1.RejectDraftChunkRequest - (*RejectDraftChunkResponse)(nil), // 85: openshell.v1.RejectDraftChunkResponse - (*ApproveAllDraftChunksRequest)(nil), // 86: openshell.v1.ApproveAllDraftChunksRequest - (*ApproveAllDraftChunksResponse)(nil), // 87: openshell.v1.ApproveAllDraftChunksResponse - (*EditDraftChunkRequest)(nil), // 88: openshell.v1.EditDraftChunkRequest - (*EditDraftChunkResponse)(nil), // 89: openshell.v1.EditDraftChunkResponse - (*UndoDraftChunkRequest)(nil), // 90: openshell.v1.UndoDraftChunkRequest - (*UndoDraftChunkResponse)(nil), // 91: openshell.v1.UndoDraftChunkResponse - (*ClearDraftChunksRequest)(nil), // 92: openshell.v1.ClearDraftChunksRequest - (*ClearDraftChunksResponse)(nil), // 93: openshell.v1.ClearDraftChunksResponse - (*GetDraftHistoryRequest)(nil), // 94: openshell.v1.GetDraftHistoryRequest - (*DraftHistoryEntry)(nil), // 95: openshell.v1.DraftHistoryEntry - (*GetDraftHistoryResponse)(nil), // 96: openshell.v1.GetDraftHistoryResponse - (*PolicyRevisionPayload)(nil), // 97: openshell.v1.PolicyRevisionPayload - (*DraftChunkPayload)(nil), // 98: openshell.v1.DraftChunkPayload - (*StoredPolicyRevision)(nil), // 99: openshell.v1.StoredPolicyRevision - (*StoredDraftChunk)(nil), // 100: openshell.v1.StoredDraftChunk - nil, // 101: openshell.v1.SandboxSpec.EnvironmentEntry - nil, // 102: openshell.v1.SandboxTemplate.LabelsEntry - nil, // 103: openshell.v1.SandboxTemplate.AnnotationsEntry - nil, // 104: openshell.v1.SandboxTemplate.EnvironmentEntry - nil, // 105: openshell.v1.PlatformEvent.MetadataEntry - nil, // 106: openshell.v1.CreateSandboxRequest.LabelsEntry - nil, // 107: openshell.v1.ExecSandboxRequest.EnvironmentEntry - nil, // 108: openshell.v1.SandboxLogLine.FieldsEntry - nil, // 109: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - (*datamodelv1.ObjectMeta)(nil), // 110: openshell.datamodel.v1.ObjectMeta - (*sandboxv1.SandboxPolicy)(nil), // 111: openshell.sandbox.v1.SandboxPolicy - (*_struct.Struct)(nil), // 112: google.protobuf.Struct - (*datamodelv1.Provider)(nil), // 113: openshell.datamodel.v1.Provider - (*sandboxv1.SettingValue)(nil), // 114: openshell.sandbox.v1.SettingValue - (*sandboxv1.NetworkPolicyRule)(nil), // 115: openshell.sandbox.v1.NetworkPolicyRule - (*sandboxv1.L7DenyRule)(nil), // 116: openshell.sandbox.v1.L7DenyRule - (*sandboxv1.L7Rule)(nil), // 117: openshell.sandbox.v1.L7Rule - (*sandboxv1.GetSandboxConfigRequest)(nil), // 118: openshell.sandbox.v1.GetSandboxConfigRequest - (*sandboxv1.GetGatewayConfigRequest)(nil), // 119: openshell.sandbox.v1.GetGatewayConfigRequest - (*sandboxv1.GetSandboxConfigResponse)(nil), // 120: openshell.sandbox.v1.GetSandboxConfigResponse - (*sandboxv1.GetGatewayConfigResponse)(nil), // 121: openshell.sandbox.v1.GetGatewayConfigResponse + (ProviderProfileCategory)(0), // 1: openshell.v1.ProviderProfileCategory + (PolicyStatus)(0), // 2: openshell.v1.PolicyStatus + (ServiceStatus)(0), // 3: openshell.v1.ServiceStatus + (*HealthRequest)(nil), // 4: openshell.v1.HealthRequest + (*HealthResponse)(nil), // 5: openshell.v1.HealthResponse + (*Sandbox)(nil), // 6: openshell.v1.Sandbox + (*SandboxSpec)(nil), // 7: openshell.v1.SandboxSpec + (*SandboxTemplate)(nil), // 8: openshell.v1.SandboxTemplate + (*SandboxStatus)(nil), // 9: openshell.v1.SandboxStatus + (*SandboxCondition)(nil), // 10: openshell.v1.SandboxCondition + (*PlatformEvent)(nil), // 11: openshell.v1.PlatformEvent + (*CreateSandboxRequest)(nil), // 12: openshell.v1.CreateSandboxRequest + (*GetSandboxRequest)(nil), // 13: openshell.v1.GetSandboxRequest + (*ListSandboxesRequest)(nil), // 14: openshell.v1.ListSandboxesRequest + (*ListSandboxProvidersRequest)(nil), // 15: openshell.v1.ListSandboxProvidersRequest + (*AttachSandboxProviderRequest)(nil), // 16: openshell.v1.AttachSandboxProviderRequest + (*DetachSandboxProviderRequest)(nil), // 17: openshell.v1.DetachSandboxProviderRequest + (*DeleteSandboxRequest)(nil), // 18: openshell.v1.DeleteSandboxRequest + (*SandboxResponse)(nil), // 19: openshell.v1.SandboxResponse + (*ListSandboxesResponse)(nil), // 20: openshell.v1.ListSandboxesResponse + (*ListSandboxProvidersResponse)(nil), // 21: openshell.v1.ListSandboxProvidersResponse + (*AttachSandboxProviderResponse)(nil), // 22: openshell.v1.AttachSandboxProviderResponse + (*DetachSandboxProviderResponse)(nil), // 23: openshell.v1.DetachSandboxProviderResponse + (*DeleteSandboxResponse)(nil), // 24: openshell.v1.DeleteSandboxResponse + (*CreateSshSessionRequest)(nil), // 25: openshell.v1.CreateSshSessionRequest + (*CreateSshSessionResponse)(nil), // 26: openshell.v1.CreateSshSessionResponse + (*ExposeServiceRequest)(nil), // 27: openshell.v1.ExposeServiceRequest + (*GetServiceRequest)(nil), // 28: openshell.v1.GetServiceRequest + (*ListServicesRequest)(nil), // 29: openshell.v1.ListServicesRequest + (*ListServicesResponse)(nil), // 30: openshell.v1.ListServicesResponse + (*DeleteServiceRequest)(nil), // 31: openshell.v1.DeleteServiceRequest + (*DeleteServiceResponse)(nil), // 32: openshell.v1.DeleteServiceResponse + (*ServiceEndpoint)(nil), // 33: openshell.v1.ServiceEndpoint + (*ServiceEndpointResponse)(nil), // 34: openshell.v1.ServiceEndpointResponse + (*RevokeSshSessionRequest)(nil), // 35: openshell.v1.RevokeSshSessionRequest + (*RevokeSshSessionResponse)(nil), // 36: openshell.v1.RevokeSshSessionResponse + (*ExecSandboxRequest)(nil), // 37: openshell.v1.ExecSandboxRequest + (*ExecSandboxStdout)(nil), // 38: openshell.v1.ExecSandboxStdout + (*ExecSandboxStderr)(nil), // 39: openshell.v1.ExecSandboxStderr + (*ExecSandboxExit)(nil), // 40: openshell.v1.ExecSandboxExit + (*ExecSandboxEvent)(nil), // 41: openshell.v1.ExecSandboxEvent + (*TcpForwardInit)(nil), // 42: openshell.v1.TcpForwardInit + (*TcpForwardFrame)(nil), // 43: openshell.v1.TcpForwardFrame + (*ExecSandboxInput)(nil), // 44: openshell.v1.ExecSandboxInput + (*ExecSandboxWindowResize)(nil), // 45: openshell.v1.ExecSandboxWindowResize + (*SshSession)(nil), // 46: openshell.v1.SshSession + (*WatchSandboxRequest)(nil), // 47: openshell.v1.WatchSandboxRequest + (*SandboxStreamEvent)(nil), // 48: openshell.v1.SandboxStreamEvent + (*SandboxLogLine)(nil), // 49: openshell.v1.SandboxLogLine + (*SandboxStreamWarning)(nil), // 50: openshell.v1.SandboxStreamWarning + (*CreateProviderRequest)(nil), // 51: openshell.v1.CreateProviderRequest + (*GetProviderRequest)(nil), // 52: openshell.v1.GetProviderRequest + (*ListProvidersRequest)(nil), // 53: openshell.v1.ListProvidersRequest + (*UpdateProviderRequest)(nil), // 54: openshell.v1.UpdateProviderRequest + (*DeleteProviderRequest)(nil), // 55: openshell.v1.DeleteProviderRequest + (*ProviderResponse)(nil), // 56: openshell.v1.ProviderResponse + (*ListProvidersResponse)(nil), // 57: openshell.v1.ListProvidersResponse + (*ListProviderProfilesRequest)(nil), // 58: openshell.v1.ListProviderProfilesRequest + (*GetProviderProfileRequest)(nil), // 59: openshell.v1.GetProviderProfileRequest + (*ProviderProfileImportItem)(nil), // 60: openshell.v1.ProviderProfileImportItem + (*ProviderProfileDiagnostic)(nil), // 61: openshell.v1.ProviderProfileDiagnostic + (*ProviderProfileCredential)(nil), // 62: openshell.v1.ProviderProfileCredential + (*ProviderProfile)(nil), // 63: openshell.v1.ProviderProfile + (*StoredProviderProfile)(nil), // 64: openshell.v1.StoredProviderProfile + (*ProviderProfileResponse)(nil), // 65: openshell.v1.ProviderProfileResponse + (*ListProviderProfilesResponse)(nil), // 66: openshell.v1.ListProviderProfilesResponse + (*ImportProviderProfilesRequest)(nil), // 67: openshell.v1.ImportProviderProfilesRequest + (*ImportProviderProfilesResponse)(nil), // 68: openshell.v1.ImportProviderProfilesResponse + (*LintProviderProfilesRequest)(nil), // 69: openshell.v1.LintProviderProfilesRequest + (*LintProviderProfilesResponse)(nil), // 70: openshell.v1.LintProviderProfilesResponse + (*DeleteProviderResponse)(nil), // 71: openshell.v1.DeleteProviderResponse + (*DeleteProviderProfileRequest)(nil), // 72: openshell.v1.DeleteProviderProfileRequest + (*DeleteProviderProfileResponse)(nil), // 73: openshell.v1.DeleteProviderProfileResponse + (*GetSandboxProviderEnvironmentRequest)(nil), // 74: openshell.v1.GetSandboxProviderEnvironmentRequest + (*GetSandboxProviderEnvironmentResponse)(nil), // 75: openshell.v1.GetSandboxProviderEnvironmentResponse + (*UpdateConfigRequest)(nil), // 76: openshell.v1.UpdateConfigRequest + (*PolicyMergeOperation)(nil), // 77: openshell.v1.PolicyMergeOperation + (*AddNetworkRule)(nil), // 78: openshell.v1.AddNetworkRule + (*RemoveNetworkEndpoint)(nil), // 79: openshell.v1.RemoveNetworkEndpoint + (*RemoveNetworkRule)(nil), // 80: openshell.v1.RemoveNetworkRule + (*AddDenyRules)(nil), // 81: openshell.v1.AddDenyRules + (*AddAllowRules)(nil), // 82: openshell.v1.AddAllowRules + (*RemoveNetworkBinary)(nil), // 83: openshell.v1.RemoveNetworkBinary + (*UpdateConfigResponse)(nil), // 84: openshell.v1.UpdateConfigResponse + (*GetSandboxPolicyStatusRequest)(nil), // 85: openshell.v1.GetSandboxPolicyStatusRequest + (*GetSandboxPolicyStatusResponse)(nil), // 86: openshell.v1.GetSandboxPolicyStatusResponse + (*ListSandboxPoliciesRequest)(nil), // 87: openshell.v1.ListSandboxPoliciesRequest + (*ListSandboxPoliciesResponse)(nil), // 88: openshell.v1.ListSandboxPoliciesResponse + (*ReportPolicyStatusRequest)(nil), // 89: openshell.v1.ReportPolicyStatusRequest + (*ReportPolicyStatusResponse)(nil), // 90: openshell.v1.ReportPolicyStatusResponse + (*SandboxPolicyRevision)(nil), // 91: openshell.v1.SandboxPolicyRevision + (*GetSandboxLogsRequest)(nil), // 92: openshell.v1.GetSandboxLogsRequest + (*PushSandboxLogsRequest)(nil), // 93: openshell.v1.PushSandboxLogsRequest + (*PushSandboxLogsResponse)(nil), // 94: openshell.v1.PushSandboxLogsResponse + (*GetSandboxLogsResponse)(nil), // 95: openshell.v1.GetSandboxLogsResponse + (*SupervisorMessage)(nil), // 96: openshell.v1.SupervisorMessage + (*GatewayMessage)(nil), // 97: openshell.v1.GatewayMessage + (*SupervisorHello)(nil), // 98: openshell.v1.SupervisorHello + (*SessionAccepted)(nil), // 99: openshell.v1.SessionAccepted + (*SessionRejected)(nil), // 100: openshell.v1.SessionRejected + (*SupervisorHeartbeat)(nil), // 101: openshell.v1.SupervisorHeartbeat + (*GatewayHeartbeat)(nil), // 102: openshell.v1.GatewayHeartbeat + (*RelayOpen)(nil), // 103: openshell.v1.RelayOpen + (*SshRelayTarget)(nil), // 104: openshell.v1.SshRelayTarget + (*TcpRelayTarget)(nil), // 105: openshell.v1.TcpRelayTarget + (*RelayInit)(nil), // 106: openshell.v1.RelayInit + (*RelayFrame)(nil), // 107: openshell.v1.RelayFrame + (*RelayOpenResult)(nil), // 108: openshell.v1.RelayOpenResult + (*RelayClose)(nil), // 109: openshell.v1.RelayClose + (*L7RequestSample)(nil), // 110: openshell.v1.L7RequestSample + (*DenialSummary)(nil), // 111: openshell.v1.DenialSummary + (*PolicyChunk)(nil), // 112: openshell.v1.PolicyChunk + (*DraftPolicyUpdate)(nil), // 113: openshell.v1.DraftPolicyUpdate + (*SubmitPolicyAnalysisRequest)(nil), // 114: openshell.v1.SubmitPolicyAnalysisRequest + (*SubmitPolicyAnalysisResponse)(nil), // 115: openshell.v1.SubmitPolicyAnalysisResponse + (*GetDraftPolicyRequest)(nil), // 116: openshell.v1.GetDraftPolicyRequest + (*GetDraftPolicyResponse)(nil), // 117: openshell.v1.GetDraftPolicyResponse + (*ApproveDraftChunkRequest)(nil), // 118: openshell.v1.ApproveDraftChunkRequest + (*ApproveDraftChunkResponse)(nil), // 119: openshell.v1.ApproveDraftChunkResponse + (*RejectDraftChunkRequest)(nil), // 120: openshell.v1.RejectDraftChunkRequest + (*RejectDraftChunkResponse)(nil), // 121: openshell.v1.RejectDraftChunkResponse + (*ApproveAllDraftChunksRequest)(nil), // 122: openshell.v1.ApproveAllDraftChunksRequest + (*ApproveAllDraftChunksResponse)(nil), // 123: openshell.v1.ApproveAllDraftChunksResponse + (*EditDraftChunkRequest)(nil), // 124: openshell.v1.EditDraftChunkRequest + (*EditDraftChunkResponse)(nil), // 125: openshell.v1.EditDraftChunkResponse + (*UndoDraftChunkRequest)(nil), // 126: openshell.v1.UndoDraftChunkRequest + (*UndoDraftChunkResponse)(nil), // 127: openshell.v1.UndoDraftChunkResponse + (*ClearDraftChunksRequest)(nil), // 128: openshell.v1.ClearDraftChunksRequest + (*ClearDraftChunksResponse)(nil), // 129: openshell.v1.ClearDraftChunksResponse + (*GetDraftHistoryRequest)(nil), // 130: openshell.v1.GetDraftHistoryRequest + (*DraftHistoryEntry)(nil), // 131: openshell.v1.DraftHistoryEntry + (*GetDraftHistoryResponse)(nil), // 132: openshell.v1.GetDraftHistoryResponse + (*PolicyRevisionPayload)(nil), // 133: openshell.v1.PolicyRevisionPayload + (*DraftChunkPayload)(nil), // 134: openshell.v1.DraftChunkPayload + (*StoredPolicyRevision)(nil), // 135: openshell.v1.StoredPolicyRevision + (*StoredDraftChunk)(nil), // 136: openshell.v1.StoredDraftChunk + nil, // 137: openshell.v1.SandboxSpec.EnvironmentEntry + nil, // 138: openshell.v1.SandboxTemplate.LabelsEntry + nil, // 139: openshell.v1.SandboxTemplate.AnnotationsEntry + nil, // 140: openshell.v1.SandboxTemplate.EnvironmentEntry + nil, // 141: openshell.v1.PlatformEvent.MetadataEntry + nil, // 142: openshell.v1.CreateSandboxRequest.LabelsEntry + nil, // 143: openshell.v1.ExecSandboxRequest.EnvironmentEntry + nil, // 144: openshell.v1.SandboxLogLine.FieldsEntry + nil, // 145: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + (*datamodelv1.ObjectMeta)(nil), // 146: openshell.datamodel.v1.ObjectMeta + (*sandboxv1.SandboxPolicy)(nil), // 147: openshell.sandbox.v1.SandboxPolicy + (*structpb.Struct)(nil), // 148: google.protobuf.Struct + (*datamodelv1.Provider)(nil), // 149: openshell.datamodel.v1.Provider + (*sandboxv1.NetworkEndpoint)(nil), // 150: openshell.sandbox.v1.NetworkEndpoint + (*sandboxv1.NetworkBinary)(nil), // 151: openshell.sandbox.v1.NetworkBinary + (*sandboxv1.SettingValue)(nil), // 152: openshell.sandbox.v1.SettingValue + (*sandboxv1.NetworkPolicyRule)(nil), // 153: openshell.sandbox.v1.NetworkPolicyRule + (*sandboxv1.L7DenyRule)(nil), // 154: openshell.sandbox.v1.L7DenyRule + (*sandboxv1.L7Rule)(nil), // 155: openshell.sandbox.v1.L7Rule + (*sandboxv1.GetSandboxConfigRequest)(nil), // 156: openshell.sandbox.v1.GetSandboxConfigRequest + (*sandboxv1.GetGatewayConfigRequest)(nil), // 157: openshell.sandbox.v1.GetGatewayConfigRequest + (*sandboxv1.GetSandboxConfigResponse)(nil), // 158: openshell.sandbox.v1.GetSandboxConfigResponse + (*sandboxv1.GetGatewayConfigResponse)(nil), // 159: openshell.sandbox.v1.GetGatewayConfigResponse } var file_openshell_proto_depIdxs = []int32{ - 2, // 0: openshell.v1.HealthResponse.status:type_name -> openshell.v1.ServiceStatus - 110, // 1: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 6, // 2: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec - 8, // 3: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus + 3, // 0: openshell.v1.HealthResponse.status:type_name -> openshell.v1.ServiceStatus + 146, // 1: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 7, // 2: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec + 9, // 3: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus 0, // 4: openshell.v1.Sandbox.phase:type_name -> openshell.v1.SandboxPhase - 101, // 5: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry - 7, // 6: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate - 111, // 7: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 102, // 8: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry - 103, // 9: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry - 104, // 10: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry - 112, // 11: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct - 112, // 12: openshell.v1.SandboxTemplate.volume_claim_templates:type_name -> google.protobuf.Struct - 9, // 13: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition - 105, // 14: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry - 6, // 15: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec - 106, // 16: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry - 5, // 17: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox - 5, // 18: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox - 107, // 19: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry - 23, // 20: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout - 24, // 21: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr - 25, // 22: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit - 110, // 23: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 5, // 24: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox - 30, // 25: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine - 10, // 26: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent - 31, // 27: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning - 77, // 28: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate - 108, // 29: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry - 113, // 30: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 113, // 31: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 113, // 32: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider - 113, // 33: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 109, // 34: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - 111, // 35: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 114, // 36: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue - 43, // 37: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation - 44, // 38: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule - 45, // 39: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint - 46, // 40: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule - 47, // 41: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules - 48, // 42: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules - 49, // 43: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary - 115, // 44: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 116, // 45: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 117, // 46: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule - 57, // 47: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision - 57, // 48: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision - 1, // 49: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus - 1, // 50: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus - 111, // 51: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 30, // 52: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine - 30, // 53: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine - 64, // 54: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello - 67, // 55: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat - 72, // 56: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult - 73, // 57: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose - 65, // 58: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted - 66, // 59: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected - 68, // 60: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat - 69, // 61: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen - 73, // 62: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose - 70, // 63: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit - 74, // 64: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample - 115, // 65: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 75, // 66: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary - 76, // 67: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk - 76, // 68: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk - 115, // 69: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 95, // 70: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry - 111, // 71: openshell.v1.PolicyRevisionPayload.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 115, // 72: openshell.v1.DraftChunkPayload.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 3, // 73: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest - 11, // 74: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest - 12, // 75: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest - 13, // 76: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest - 14, // 77: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest - 18, // 78: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest - 20, // 79: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest - 22, // 80: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest - 32, // 81: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest - 33, // 82: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest - 34, // 83: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest - 35, // 84: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest - 36, // 85: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest - 118, // 86: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest - 119, // 87: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest - 42, // 88: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest - 51, // 89: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest - 53, // 90: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest - 55, // 91: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest - 40, // 92: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest - 58, // 93: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest - 59, // 94: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest - 62, // 95: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage - 71, // 96: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame - 28, // 97: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest - 78, // 98: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest - 80, // 99: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest - 82, // 100: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest - 84, // 101: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest - 86, // 102: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest - 88, // 103: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest - 90, // 104: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest - 92, // 105: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest - 94, // 106: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest - 4, // 107: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse - 15, // 108: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse - 15, // 109: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse - 16, // 110: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse - 17, // 111: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse - 19, // 112: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse - 21, // 113: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse - 26, // 114: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent - 37, // 115: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse - 37, // 116: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse - 38, // 117: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse - 37, // 118: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse - 39, // 119: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse - 120, // 120: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse - 121, // 121: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse - 50, // 122: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse - 52, // 123: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse - 54, // 124: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse - 56, // 125: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse - 41, // 126: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse - 61, // 127: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse - 60, // 128: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse - 63, // 129: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage - 71, // 130: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame - 29, // 131: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent - 79, // 132: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse - 81, // 133: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse - 83, // 134: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse - 85, // 135: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse - 87, // 136: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse - 89, // 137: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse - 91, // 138: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse - 93, // 139: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse - 96, // 140: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse - 107, // [107:141] is the sub-list for method output_type - 73, // [73:107] is the sub-list for method input_type - 73, // [73:73] is the sub-list for extension type_name - 73, // [73:73] is the sub-list for extension extendee - 0, // [0:73] is the sub-list for field type_name + 137, // 5: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry + 8, // 6: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate + 147, // 7: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 138, // 8: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry + 139, // 9: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry + 140, // 10: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry + 148, // 11: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct + 148, // 12: openshell.v1.SandboxTemplate.volume_claim_templates:type_name -> google.protobuf.Struct + 10, // 13: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition + 141, // 14: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry + 7, // 15: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec + 142, // 16: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry + 6, // 17: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox + 6, // 18: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox + 149, // 19: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 6, // 20: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 6, // 21: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 34, // 22: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse + 146, // 23: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 33, // 24: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint + 143, // 25: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry + 38, // 26: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout + 39, // 27: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr + 40, // 28: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit + 104, // 29: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget + 105, // 30: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget + 42, // 31: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit + 37, // 32: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest + 45, // 33: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize + 146, // 34: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 6, // 35: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox + 49, // 36: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine + 11, // 37: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent + 50, // 38: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning + 113, // 39: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate + 144, // 40: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry + 149, // 41: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 149, // 42: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 149, // 43: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider + 149, // 44: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 63, // 45: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile + 1, // 46: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory + 62, // 47: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential + 150, // 48: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 151, // 49: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 146, // 50: openshell.v1.StoredProviderProfile.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 63, // 51: openshell.v1.StoredProviderProfile.profile:type_name -> openshell.v1.ProviderProfile + 63, // 52: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile + 63, // 53: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 60, // 54: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 61, // 55: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 63, // 56: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 60, // 57: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 61, // 58: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 145, // 59: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + 147, // 60: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 152, // 61: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue + 77, // 62: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation + 78, // 63: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule + 79, // 64: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint + 80, // 65: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule + 81, // 66: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules + 82, // 67: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules + 83, // 68: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary + 153, // 69: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 154, // 70: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 155, // 71: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule + 91, // 72: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision + 91, // 73: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision + 2, // 74: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus + 2, // 75: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus + 147, // 76: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 49, // 77: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine + 49, // 78: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine + 98, // 79: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello + 101, // 80: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat + 108, // 81: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult + 109, // 82: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose + 99, // 83: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted + 100, // 84: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected + 102, // 85: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat + 103, // 86: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen + 109, // 87: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose + 104, // 88: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget + 105, // 89: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget + 106, // 90: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit + 110, // 91: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample + 153, // 92: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 111, // 93: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary + 112, // 94: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk + 112, // 95: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk + 153, // 96: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 131, // 97: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry + 147, // 98: openshell.v1.PolicyRevisionPayload.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 153, // 99: openshell.v1.DraftChunkPayload.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 4, // 100: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest + 12, // 101: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest + 13, // 102: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest + 14, // 103: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest + 15, // 104: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest + 16, // 105: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest + 17, // 106: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest + 18, // 107: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest + 25, // 108: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest + 27, // 109: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest + 28, // 110: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest + 29, // 111: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest + 31, // 112: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest + 35, // 113: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest + 37, // 114: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest + 43, // 115: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame + 44, // 116: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput + 51, // 117: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest + 52, // 118: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest + 53, // 119: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest + 58, // 120: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest + 59, // 121: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest + 67, // 122: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest + 69, // 123: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest + 54, // 124: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest + 55, // 125: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest + 72, // 126: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest + 156, // 127: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest + 157, // 128: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest + 76, // 129: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest + 85, // 130: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest + 87, // 131: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest + 89, // 132: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest + 74, // 133: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest + 92, // 134: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest + 93, // 135: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest + 96, // 136: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage + 107, // 137: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame + 47, // 138: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest + 114, // 139: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest + 116, // 140: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest + 118, // 141: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest + 120, // 142: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest + 122, // 143: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest + 124, // 144: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest + 126, // 145: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest + 128, // 146: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest + 130, // 147: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest + 5, // 148: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse + 19, // 149: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse + 19, // 150: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse + 20, // 151: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse + 21, // 152: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse + 22, // 153: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse + 23, // 154: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse + 24, // 155: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse + 26, // 156: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse + 34, // 157: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse + 34, // 158: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse + 30, // 159: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse + 32, // 160: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse + 36, // 161: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse + 41, // 162: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent + 43, // 163: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame + 41, // 164: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent + 56, // 165: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse + 56, // 166: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse + 57, // 167: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse + 66, // 168: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse + 65, // 169: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse + 68, // 170: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse + 70, // 171: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse + 56, // 172: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse + 71, // 173: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse + 73, // 174: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse + 158, // 175: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse + 159, // 176: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse + 84, // 177: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse + 86, // 178: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse + 88, // 179: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse + 90, // 180: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse + 75, // 181: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse + 95, // 182: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse + 94, // 183: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse + 97, // 184: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage + 107, // 185: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame + 48, // 186: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent + 115, // 187: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse + 117, // 188: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse + 119, // 189: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse + 121, // 190: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse + 123, // 191: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse + 125, // 192: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse + 127, // 193: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse + 129, // 194: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse + 132, // 195: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse + 148, // [148:196] is the sub-list for method output_type + 100, // [100:148] is the sub-list for method input_type + 100, // [100:100] is the sub-list for extension type_name + 100, // [100:100] is the sub-list for extension extendee + 0, // [0:100] is the sub-list for field type_name } func init() { file_openshell_proto_init() } @@ -7775,19 +10418,33 @@ func file_openshell_proto_init() { if File_openshell_proto != nil { return } - file_openshell_proto_msgTypes[23].OneofWrappers = []any{ + file_openshell_proto_msgTypes[4].OneofWrappers = []any{} + file_openshell_proto_msgTypes[37].OneofWrappers = []any{ (*ExecSandboxEvent_Stdout)(nil), (*ExecSandboxEvent_Stderr)(nil), (*ExecSandboxEvent_Exit)(nil), } - file_openshell_proto_msgTypes[26].OneofWrappers = []any{ + file_openshell_proto_msgTypes[38].OneofWrappers = []any{ + (*TcpForwardInit_Ssh)(nil), + (*TcpForwardInit_Tcp)(nil), + } + file_openshell_proto_msgTypes[39].OneofWrappers = []any{ + (*TcpForwardFrame_Init)(nil), + (*TcpForwardFrame_Data)(nil), + } + file_openshell_proto_msgTypes[40].OneofWrappers = []any{ + (*ExecSandboxInput_Start)(nil), + (*ExecSandboxInput_Stdin)(nil), + (*ExecSandboxInput_Resize)(nil), + } + file_openshell_proto_msgTypes[44].OneofWrappers = []any{ (*SandboxStreamEvent_Sandbox)(nil), (*SandboxStreamEvent_Log)(nil), (*SandboxStreamEvent_Event)(nil), (*SandboxStreamEvent_Warning)(nil), (*SandboxStreamEvent_DraftPolicyUpdate)(nil), } - file_openshell_proto_msgTypes[40].OneofWrappers = []any{ + file_openshell_proto_msgTypes[73].OneofWrappers = []any{ (*PolicyMergeOperation_AddRule)(nil), (*PolicyMergeOperation_RemoveEndpoint)(nil), (*PolicyMergeOperation_RemoveRule)(nil), @@ -7795,32 +10452,36 @@ func file_openshell_proto_init() { (*PolicyMergeOperation_AddAllowRules)(nil), (*PolicyMergeOperation_RemoveBinary)(nil), } - file_openshell_proto_msgTypes[59].OneofWrappers = []any{ + file_openshell_proto_msgTypes[92].OneofWrappers = []any{ (*SupervisorMessage_Hello)(nil), (*SupervisorMessage_Heartbeat)(nil), (*SupervisorMessage_RelayOpenResult)(nil), (*SupervisorMessage_RelayClose)(nil), } - file_openshell_proto_msgTypes[60].OneofWrappers = []any{ + file_openshell_proto_msgTypes[93].OneofWrappers = []any{ (*GatewayMessage_SessionAccepted)(nil), (*GatewayMessage_SessionRejected)(nil), (*GatewayMessage_Heartbeat)(nil), (*GatewayMessage_RelayOpen)(nil), (*GatewayMessage_RelayClose)(nil), } - file_openshell_proto_msgTypes[68].OneofWrappers = []any{ + file_openshell_proto_msgTypes[99].OneofWrappers = []any{ + (*RelayOpen_Ssh)(nil), + (*RelayOpen_Tcp)(nil), + } + file_openshell_proto_msgTypes[103].OneofWrappers = []any{ (*RelayFrame_Init)(nil), (*RelayFrame_Data)(nil), } - file_openshell_proto_msgTypes[96].OneofWrappers = []any{} - file_openshell_proto_msgTypes[97].OneofWrappers = []any{} + file_openshell_proto_msgTypes[131].OneofWrappers = []any{} + file_openshell_proto_msgTypes[132].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_openshell_proto_rawDesc), len(file_openshell_proto_rawDesc)), - NumEnums: 3, - NumMessages: 107, + NumEnums: 4, + NumMessages: 142, NumExtensions: 0, NumServices: 1, }, diff --git a/go/api/openshell/gen/openshellv1/openshell_grpc.pb.go b/go/api/openshell/gen/openshellv1/openshell_grpc.pb.go index 45fe4ceb16..bafdbaf728 100644 --- a/go/api/openshell/gen/openshellv1/openshell_grpc.pb.go +++ b/go/api/openshell/gen/openshellv1/openshell_grpc.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v3.15.8 +// - protoc (unknown) // source: openshell.proto package openshellv1 @@ -27,15 +27,29 @@ const ( OpenShell_CreateSandbox_FullMethodName = "/openshell.v1.OpenShell/CreateSandbox" OpenShell_GetSandbox_FullMethodName = "/openshell.v1.OpenShell/GetSandbox" OpenShell_ListSandboxes_FullMethodName = "/openshell.v1.OpenShell/ListSandboxes" + OpenShell_ListSandboxProviders_FullMethodName = "/openshell.v1.OpenShell/ListSandboxProviders" + OpenShell_AttachSandboxProvider_FullMethodName = "/openshell.v1.OpenShell/AttachSandboxProvider" + OpenShell_DetachSandboxProvider_FullMethodName = "/openshell.v1.OpenShell/DetachSandboxProvider" OpenShell_DeleteSandbox_FullMethodName = "/openshell.v1.OpenShell/DeleteSandbox" OpenShell_CreateSshSession_FullMethodName = "/openshell.v1.OpenShell/CreateSshSession" + OpenShell_ExposeService_FullMethodName = "/openshell.v1.OpenShell/ExposeService" + OpenShell_GetService_FullMethodName = "/openshell.v1.OpenShell/GetService" + OpenShell_ListServices_FullMethodName = "/openshell.v1.OpenShell/ListServices" + OpenShell_DeleteService_FullMethodName = "/openshell.v1.OpenShell/DeleteService" OpenShell_RevokeSshSession_FullMethodName = "/openshell.v1.OpenShell/RevokeSshSession" OpenShell_ExecSandbox_FullMethodName = "/openshell.v1.OpenShell/ExecSandbox" + OpenShell_ForwardTcp_FullMethodName = "/openshell.v1.OpenShell/ForwardTcp" + OpenShell_ExecSandboxInteractive_FullMethodName = "/openshell.v1.OpenShell/ExecSandboxInteractive" OpenShell_CreateProvider_FullMethodName = "/openshell.v1.OpenShell/CreateProvider" OpenShell_GetProvider_FullMethodName = "/openshell.v1.OpenShell/GetProvider" OpenShell_ListProviders_FullMethodName = "/openshell.v1.OpenShell/ListProviders" + OpenShell_ListProviderProfiles_FullMethodName = "/openshell.v1.OpenShell/ListProviderProfiles" + OpenShell_GetProviderProfile_FullMethodName = "/openshell.v1.OpenShell/GetProviderProfile" + OpenShell_ImportProviderProfiles_FullMethodName = "/openshell.v1.OpenShell/ImportProviderProfiles" + OpenShell_LintProviderProfiles_FullMethodName = "/openshell.v1.OpenShell/LintProviderProfiles" OpenShell_UpdateProvider_FullMethodName = "/openshell.v1.OpenShell/UpdateProvider" OpenShell_DeleteProvider_FullMethodName = "/openshell.v1.OpenShell/DeleteProvider" + OpenShell_DeleteProviderProfile_FullMethodName = "/openshell.v1.OpenShell/DeleteProviderProfile" OpenShell_GetSandboxConfig_FullMethodName = "/openshell.v1.OpenShell/GetSandboxConfig" OpenShell_GetGatewayConfig_FullMethodName = "/openshell.v1.OpenShell/GetGatewayConfig" OpenShell_UpdateConfig_FullMethodName = "/openshell.v1.OpenShell/UpdateConfig" @@ -80,24 +94,54 @@ type OpenShellClient interface { GetSandbox(ctx context.Context, in *GetSandboxRequest, opts ...grpc.CallOption) (*SandboxResponse, error) // List sandboxes. ListSandboxes(ctx context.Context, in *ListSandboxesRequest, opts ...grpc.CallOption) (*ListSandboxesResponse, error) + // List provider records attached to a sandbox. + ListSandboxProviders(ctx context.Context, in *ListSandboxProvidersRequest, opts ...grpc.CallOption) (*ListSandboxProvidersResponse, error) + // Attach a provider record to an existing sandbox. + AttachSandboxProvider(ctx context.Context, in *AttachSandboxProviderRequest, opts ...grpc.CallOption) (*AttachSandboxProviderResponse, error) + // Detach a provider record from an existing sandbox. + DetachSandboxProvider(ctx context.Context, in *DetachSandboxProviderRequest, opts ...grpc.CallOption) (*DetachSandboxProviderResponse, error) // Delete a sandbox by name. DeleteSandbox(ctx context.Context, in *DeleteSandboxRequest, opts ...grpc.CallOption) (*DeleteSandboxResponse, error) // Create a short-lived SSH session for a sandbox. CreateSshSession(ctx context.Context, in *CreateSshSessionRequest, opts ...grpc.CallOption) (*CreateSshSessionResponse, error) + // Create or update a sandbox HTTP service endpoint for local routing. + ExposeService(ctx context.Context, in *ExposeServiceRequest, opts ...grpc.CallOption) (*ServiceEndpointResponse, error) + // Fetch one sandbox HTTP service endpoint. + GetService(ctx context.Context, in *GetServiceRequest, opts ...grpc.CallOption) (*ServiceEndpointResponse, error) + // List sandbox HTTP service endpoints. + ListServices(ctx context.Context, in *ListServicesRequest, opts ...grpc.CallOption) (*ListServicesResponse, error) + // Delete one sandbox HTTP service endpoint. + DeleteService(ctx context.Context, in *DeleteServiceRequest, opts ...grpc.CallOption) (*DeleteServiceResponse, error) // Revoke a previously issued SSH session. RevokeSshSession(ctx context.Context, in *RevokeSshSessionRequest, opts ...grpc.CallOption) (*RevokeSshSessionResponse, error) // Execute a command in a ready sandbox and stream output. ExecSandbox(ctx context.Context, in *ExecSandboxRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ExecSandboxEvent], error) + // Forward one CLI-side TCP connection to a loopback TCP target in a sandbox. + ForwardTcp(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[TcpForwardFrame, TcpForwardFrame], error) + // Execute an interactive command with bidirectional stdin/stdout streaming. + // The first client message MUST carry an ExecSandboxInput with the start + // variant. Subsequent messages carry stdin bytes or window resize events. + ExecSandboxInteractive(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExecSandboxInput, ExecSandboxEvent], error) // Create a provider. CreateProvider(ctx context.Context, in *CreateProviderRequest, opts ...grpc.CallOption) (*ProviderResponse, error) // Fetch a provider by name. GetProvider(ctx context.Context, in *GetProviderRequest, opts ...grpc.CallOption) (*ProviderResponse, error) // List providers. ListProviders(ctx context.Context, in *ListProvidersRequest, opts ...grpc.CallOption) (*ListProvidersResponse, error) + // List available provider type profiles. + ListProviderProfiles(ctx context.Context, in *ListProviderProfilesRequest, opts ...grpc.CallOption) (*ListProviderProfilesResponse, error) + // Fetch one provider type profile by id. + GetProviderProfile(ctx context.Context, in *GetProviderProfileRequest, opts ...grpc.CallOption) (*ProviderProfileResponse, error) + // Import custom provider type profiles. + ImportProviderProfiles(ctx context.Context, in *ImportProviderProfilesRequest, opts ...grpc.CallOption) (*ImportProviderProfilesResponse, error) + // Validate provider type profiles without registering them. + LintProviderProfiles(ctx context.Context, in *LintProviderProfilesRequest, opts ...grpc.CallOption) (*LintProviderProfilesResponse, error) // Update an existing provider by name. UpdateProvider(ctx context.Context, in *UpdateProviderRequest, opts ...grpc.CallOption) (*ProviderResponse, error) // Delete a provider by name. DeleteProvider(ctx context.Context, in *DeleteProviderRequest, opts ...grpc.CallOption) (*DeleteProviderResponse, error) + // Delete a custom provider type profile by id. + DeleteProviderProfile(ctx context.Context, in *DeleteProviderProfileRequest, opts ...grpc.CallOption) (*DeleteProviderProfileResponse, error) // Get sandbox settings by id (called by sandbox entrypoint and poll loop). GetSandboxConfig(ctx context.Context, in *sandboxv1.GetSandboxConfigRequest, opts ...grpc.CallOption) (*sandboxv1.GetSandboxConfigResponse, error) // Get gateway-global settings. @@ -120,8 +164,9 @@ type OpenShellClient interface { // // The supervisor opens this stream at startup and keeps it alive for the // sandbox lifetime. The gateway uses it to coordinate relay channels for - // SSH connect and ExecSandbox. Raw SSH bytes flow over RelayStream calls - // (separate HTTP/2 streams on the same connection), not over this stream. + // SSH connect, ExecSandbox, and targetable sandbox services. Raw service + // bytes flow over RelayStream calls (separate HTTP/2 streams on the same + // connection), not over this stream. ConnectSupervisor(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[SupervisorMessage, GatewayMessage], error) // Raw byte relay between supervisor and gateway. // @@ -129,8 +174,8 @@ type OpenShellClient interface { // on its ConnectSupervisor stream. The first RelayFrame carries a // RelayInit with the channel_id to associate the new HTTP/2 stream with // the pending relay slot on the gateway. Subsequent frames carry raw bytes in either - // direction between the gateway-side waiter (ssh_tunnel / exec handler) - // and the supervisor-side local SSH daemon bridge. + // direction between the gateway-side waiter (ForwardTcp / exec handler) + // and the supervisor-side target bridge. // // This rides the same TCP+TLS+HTTP/2 connection as ConnectSupervisor — // no new TLS handshake, no reverse HTTP CONNECT. @@ -210,6 +255,36 @@ func (c *openShellClient) ListSandboxes(ctx context.Context, in *ListSandboxesRe return out, nil } +func (c *openShellClient) ListSandboxProviders(ctx context.Context, in *ListSandboxProvidersRequest, opts ...grpc.CallOption) (*ListSandboxProvidersResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListSandboxProvidersResponse) + err := c.cc.Invoke(ctx, OpenShell_ListSandboxProviders_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) AttachSandboxProvider(ctx context.Context, in *AttachSandboxProviderRequest, opts ...grpc.CallOption) (*AttachSandboxProviderResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AttachSandboxProviderResponse) + err := c.cc.Invoke(ctx, OpenShell_AttachSandboxProvider_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) DetachSandboxProvider(ctx context.Context, in *DetachSandboxProviderRequest, opts ...grpc.CallOption) (*DetachSandboxProviderResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DetachSandboxProviderResponse) + err := c.cc.Invoke(ctx, OpenShell_DetachSandboxProvider_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *openShellClient) DeleteSandbox(ctx context.Context, in *DeleteSandboxRequest, opts ...grpc.CallOption) (*DeleteSandboxResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DeleteSandboxResponse) @@ -230,6 +305,46 @@ func (c *openShellClient) CreateSshSession(ctx context.Context, in *CreateSshSes return out, nil } +func (c *openShellClient) ExposeService(ctx context.Context, in *ExposeServiceRequest, opts ...grpc.CallOption) (*ServiceEndpointResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ServiceEndpointResponse) + err := c.cc.Invoke(ctx, OpenShell_ExposeService_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) GetService(ctx context.Context, in *GetServiceRequest, opts ...grpc.CallOption) (*ServiceEndpointResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ServiceEndpointResponse) + err := c.cc.Invoke(ctx, OpenShell_GetService_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ListServices(ctx context.Context, in *ListServicesRequest, opts ...grpc.CallOption) (*ListServicesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListServicesResponse) + err := c.cc.Invoke(ctx, OpenShell_ListServices_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) DeleteService(ctx context.Context, in *DeleteServiceRequest, opts ...grpc.CallOption) (*DeleteServiceResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteServiceResponse) + err := c.cc.Invoke(ctx, OpenShell_DeleteService_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *openShellClient) RevokeSshSession(ctx context.Context, in *RevokeSshSessionRequest, opts ...grpc.CallOption) (*RevokeSshSessionResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(RevokeSshSessionResponse) @@ -259,6 +374,32 @@ func (c *openShellClient) ExecSandbox(ctx context.Context, in *ExecSandboxReques // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type OpenShell_ExecSandboxClient = grpc.ServerStreamingClient[ExecSandboxEvent] +func (c *openShellClient) ForwardTcp(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[TcpForwardFrame, TcpForwardFrame], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &OpenShell_ServiceDesc.Streams[1], OpenShell_ForwardTcp_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[TcpForwardFrame, TcpForwardFrame]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type OpenShell_ForwardTcpClient = grpc.BidiStreamingClient[TcpForwardFrame, TcpForwardFrame] + +func (c *openShellClient) ExecSandboxInteractive(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExecSandboxInput, ExecSandboxEvent], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &OpenShell_ServiceDesc.Streams[2], OpenShell_ExecSandboxInteractive_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[ExecSandboxInput, ExecSandboxEvent]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type OpenShell_ExecSandboxInteractiveClient = grpc.BidiStreamingClient[ExecSandboxInput, ExecSandboxEvent] + func (c *openShellClient) CreateProvider(ctx context.Context, in *CreateProviderRequest, opts ...grpc.CallOption) (*ProviderResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ProviderResponse) @@ -289,6 +430,46 @@ func (c *openShellClient) ListProviders(ctx context.Context, in *ListProvidersRe return out, nil } +func (c *openShellClient) ListProviderProfiles(ctx context.Context, in *ListProviderProfilesRequest, opts ...grpc.CallOption) (*ListProviderProfilesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListProviderProfilesResponse) + err := c.cc.Invoke(ctx, OpenShell_ListProviderProfiles_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) GetProviderProfile(ctx context.Context, in *GetProviderProfileRequest, opts ...grpc.CallOption) (*ProviderProfileResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ProviderProfileResponse) + err := c.cc.Invoke(ctx, OpenShell_GetProviderProfile_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ImportProviderProfiles(ctx context.Context, in *ImportProviderProfilesRequest, opts ...grpc.CallOption) (*ImportProviderProfilesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ImportProviderProfilesResponse) + err := c.cc.Invoke(ctx, OpenShell_ImportProviderProfiles_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) LintProviderProfiles(ctx context.Context, in *LintProviderProfilesRequest, opts ...grpc.CallOption) (*LintProviderProfilesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(LintProviderProfilesResponse) + err := c.cc.Invoke(ctx, OpenShell_LintProviderProfiles_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *openShellClient) UpdateProvider(ctx context.Context, in *UpdateProviderRequest, opts ...grpc.CallOption) (*ProviderResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ProviderResponse) @@ -309,6 +490,16 @@ func (c *openShellClient) DeleteProvider(ctx context.Context, in *DeleteProvider return out, nil } +func (c *openShellClient) DeleteProviderProfile(ctx context.Context, in *DeleteProviderProfileRequest, opts ...grpc.CallOption) (*DeleteProviderProfileResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteProviderProfileResponse) + err := c.cc.Invoke(ctx, OpenShell_DeleteProviderProfile_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *openShellClient) GetSandboxConfig(ctx context.Context, in *sandboxv1.GetSandboxConfigRequest, opts ...grpc.CallOption) (*sandboxv1.GetSandboxConfigResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(sandboxv1.GetSandboxConfigResponse) @@ -391,7 +582,7 @@ func (c *openShellClient) GetSandboxLogs(ctx context.Context, in *GetSandboxLogs func (c *openShellClient) PushSandboxLogs(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[PushSandboxLogsRequest, PushSandboxLogsResponse], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &OpenShell_ServiceDesc.Streams[1], OpenShell_PushSandboxLogs_FullMethodName, cOpts...) + stream, err := c.cc.NewStream(ctx, &OpenShell_ServiceDesc.Streams[3], OpenShell_PushSandboxLogs_FullMethodName, cOpts...) if err != nil { return nil, err } @@ -404,7 +595,7 @@ type OpenShell_PushSandboxLogsClient = grpc.ClientStreamingClient[PushSandboxLog func (c *openShellClient) ConnectSupervisor(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[SupervisorMessage, GatewayMessage], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &OpenShell_ServiceDesc.Streams[2], OpenShell_ConnectSupervisor_FullMethodName, cOpts...) + stream, err := c.cc.NewStream(ctx, &OpenShell_ServiceDesc.Streams[4], OpenShell_ConnectSupervisor_FullMethodName, cOpts...) if err != nil { return nil, err } @@ -417,7 +608,7 @@ type OpenShell_ConnectSupervisorClient = grpc.BidiStreamingClient[SupervisorMess func (c *openShellClient) RelayStream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[RelayFrame, RelayFrame], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &OpenShell_ServiceDesc.Streams[3], OpenShell_RelayStream_FullMethodName, cOpts...) + stream, err := c.cc.NewStream(ctx, &OpenShell_ServiceDesc.Streams[5], OpenShell_RelayStream_FullMethodName, cOpts...) if err != nil { return nil, err } @@ -430,7 +621,7 @@ type OpenShell_RelayStreamClient = grpc.BidiStreamingClient[RelayFrame, RelayFra func (c *openShellClient) WatchSandbox(ctx context.Context, in *WatchSandboxRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[SandboxStreamEvent], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &OpenShell_ServiceDesc.Streams[4], OpenShell_WatchSandbox_FullMethodName, cOpts...) + stream, err := c.cc.NewStream(ctx, &OpenShell_ServiceDesc.Streams[6], OpenShell_WatchSandbox_FullMethodName, cOpts...) if err != nil { return nil, err } @@ -558,24 +749,54 @@ type OpenShellServer interface { GetSandbox(context.Context, *GetSandboxRequest) (*SandboxResponse, error) // List sandboxes. ListSandboxes(context.Context, *ListSandboxesRequest) (*ListSandboxesResponse, error) + // List provider records attached to a sandbox. + ListSandboxProviders(context.Context, *ListSandboxProvidersRequest) (*ListSandboxProvidersResponse, error) + // Attach a provider record to an existing sandbox. + AttachSandboxProvider(context.Context, *AttachSandboxProviderRequest) (*AttachSandboxProviderResponse, error) + // Detach a provider record from an existing sandbox. + DetachSandboxProvider(context.Context, *DetachSandboxProviderRequest) (*DetachSandboxProviderResponse, error) // Delete a sandbox by name. DeleteSandbox(context.Context, *DeleteSandboxRequest) (*DeleteSandboxResponse, error) // Create a short-lived SSH session for a sandbox. CreateSshSession(context.Context, *CreateSshSessionRequest) (*CreateSshSessionResponse, error) + // Create or update a sandbox HTTP service endpoint for local routing. + ExposeService(context.Context, *ExposeServiceRequest) (*ServiceEndpointResponse, error) + // Fetch one sandbox HTTP service endpoint. + GetService(context.Context, *GetServiceRequest) (*ServiceEndpointResponse, error) + // List sandbox HTTP service endpoints. + ListServices(context.Context, *ListServicesRequest) (*ListServicesResponse, error) + // Delete one sandbox HTTP service endpoint. + DeleteService(context.Context, *DeleteServiceRequest) (*DeleteServiceResponse, error) // Revoke a previously issued SSH session. RevokeSshSession(context.Context, *RevokeSshSessionRequest) (*RevokeSshSessionResponse, error) // Execute a command in a ready sandbox and stream output. ExecSandbox(*ExecSandboxRequest, grpc.ServerStreamingServer[ExecSandboxEvent]) error + // Forward one CLI-side TCP connection to a loopback TCP target in a sandbox. + ForwardTcp(grpc.BidiStreamingServer[TcpForwardFrame, TcpForwardFrame]) error + // Execute an interactive command with bidirectional stdin/stdout streaming. + // The first client message MUST carry an ExecSandboxInput with the start + // variant. Subsequent messages carry stdin bytes or window resize events. + ExecSandboxInteractive(grpc.BidiStreamingServer[ExecSandboxInput, ExecSandboxEvent]) error // Create a provider. CreateProvider(context.Context, *CreateProviderRequest) (*ProviderResponse, error) // Fetch a provider by name. GetProvider(context.Context, *GetProviderRequest) (*ProviderResponse, error) // List providers. ListProviders(context.Context, *ListProvidersRequest) (*ListProvidersResponse, error) + // List available provider type profiles. + ListProviderProfiles(context.Context, *ListProviderProfilesRequest) (*ListProviderProfilesResponse, error) + // Fetch one provider type profile by id. + GetProviderProfile(context.Context, *GetProviderProfileRequest) (*ProviderProfileResponse, error) + // Import custom provider type profiles. + ImportProviderProfiles(context.Context, *ImportProviderProfilesRequest) (*ImportProviderProfilesResponse, error) + // Validate provider type profiles without registering them. + LintProviderProfiles(context.Context, *LintProviderProfilesRequest) (*LintProviderProfilesResponse, error) // Update an existing provider by name. UpdateProvider(context.Context, *UpdateProviderRequest) (*ProviderResponse, error) // Delete a provider by name. DeleteProvider(context.Context, *DeleteProviderRequest) (*DeleteProviderResponse, error) + // Delete a custom provider type profile by id. + DeleteProviderProfile(context.Context, *DeleteProviderProfileRequest) (*DeleteProviderProfileResponse, error) // Get sandbox settings by id (called by sandbox entrypoint and poll loop). GetSandboxConfig(context.Context, *sandboxv1.GetSandboxConfigRequest) (*sandboxv1.GetSandboxConfigResponse, error) // Get gateway-global settings. @@ -598,8 +819,9 @@ type OpenShellServer interface { // // The supervisor opens this stream at startup and keeps it alive for the // sandbox lifetime. The gateway uses it to coordinate relay channels for - // SSH connect and ExecSandbox. Raw SSH bytes flow over RelayStream calls - // (separate HTTP/2 streams on the same connection), not over this stream. + // SSH connect, ExecSandbox, and targetable sandbox services. Raw service + // bytes flow over RelayStream calls (separate HTTP/2 streams on the same + // connection), not over this stream. ConnectSupervisor(grpc.BidiStreamingServer[SupervisorMessage, GatewayMessage]) error // Raw byte relay between supervisor and gateway. // @@ -607,8 +829,8 @@ type OpenShellServer interface { // on its ConnectSupervisor stream. The first RelayFrame carries a // RelayInit with the channel_id to associate the new HTTP/2 stream with // the pending relay slot on the gateway. Subsequent frames carry raw bytes in either - // direction between the gateway-side waiter (ssh_tunnel / exec handler) - // and the supervisor-side local SSH daemon bridge. + // direction between the gateway-side waiter (ForwardTcp / exec handler) + // and the supervisor-side target bridge. // // This rides the same TCP+TLS+HTTP/2 connection as ConnectSupervisor — // no new TLS handshake, no reverse HTTP CONNECT. @@ -660,18 +882,45 @@ func (UnimplementedOpenShellServer) GetSandbox(context.Context, *GetSandboxReque func (UnimplementedOpenShellServer) ListSandboxes(context.Context, *ListSandboxesRequest) (*ListSandboxesResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ListSandboxes not implemented") } +func (UnimplementedOpenShellServer) ListSandboxProviders(context.Context, *ListSandboxProvidersRequest) (*ListSandboxProvidersResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListSandboxProviders not implemented") +} +func (UnimplementedOpenShellServer) AttachSandboxProvider(context.Context, *AttachSandboxProviderRequest) (*AttachSandboxProviderResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AttachSandboxProvider not implemented") +} +func (UnimplementedOpenShellServer) DetachSandboxProvider(context.Context, *DetachSandboxProviderRequest) (*DetachSandboxProviderResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DetachSandboxProvider not implemented") +} func (UnimplementedOpenShellServer) DeleteSandbox(context.Context, *DeleteSandboxRequest) (*DeleteSandboxResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method DeleteSandbox not implemented") } func (UnimplementedOpenShellServer) CreateSshSession(context.Context, *CreateSshSessionRequest) (*CreateSshSessionResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateSshSession not implemented") } +func (UnimplementedOpenShellServer) ExposeService(context.Context, *ExposeServiceRequest) (*ServiceEndpointResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ExposeService not implemented") +} +func (UnimplementedOpenShellServer) GetService(context.Context, *GetServiceRequest) (*ServiceEndpointResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetService not implemented") +} +func (UnimplementedOpenShellServer) ListServices(context.Context, *ListServicesRequest) (*ListServicesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListServices not implemented") +} +func (UnimplementedOpenShellServer) DeleteService(context.Context, *DeleteServiceRequest) (*DeleteServiceResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteService not implemented") +} func (UnimplementedOpenShellServer) RevokeSshSession(context.Context, *RevokeSshSessionRequest) (*RevokeSshSessionResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method RevokeSshSession not implemented") } func (UnimplementedOpenShellServer) ExecSandbox(*ExecSandboxRequest, grpc.ServerStreamingServer[ExecSandboxEvent]) error { return status.Errorf(codes.Unimplemented, "method ExecSandbox not implemented") } +func (UnimplementedOpenShellServer) ForwardTcp(grpc.BidiStreamingServer[TcpForwardFrame, TcpForwardFrame]) error { + return status.Errorf(codes.Unimplemented, "method ForwardTcp not implemented") +} +func (UnimplementedOpenShellServer) ExecSandboxInteractive(grpc.BidiStreamingServer[ExecSandboxInput, ExecSandboxEvent]) error { + return status.Errorf(codes.Unimplemented, "method ExecSandboxInteractive not implemented") +} func (UnimplementedOpenShellServer) CreateProvider(context.Context, *CreateProviderRequest) (*ProviderResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateProvider not implemented") } @@ -681,12 +930,27 @@ func (UnimplementedOpenShellServer) GetProvider(context.Context, *GetProviderReq func (UnimplementedOpenShellServer) ListProviders(context.Context, *ListProvidersRequest) (*ListProvidersResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ListProviders not implemented") } +func (UnimplementedOpenShellServer) ListProviderProfiles(context.Context, *ListProviderProfilesRequest) (*ListProviderProfilesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListProviderProfiles not implemented") +} +func (UnimplementedOpenShellServer) GetProviderProfile(context.Context, *GetProviderProfileRequest) (*ProviderProfileResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetProviderProfile not implemented") +} +func (UnimplementedOpenShellServer) ImportProviderProfiles(context.Context, *ImportProviderProfilesRequest) (*ImportProviderProfilesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ImportProviderProfiles not implemented") +} +func (UnimplementedOpenShellServer) LintProviderProfiles(context.Context, *LintProviderProfilesRequest) (*LintProviderProfilesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method LintProviderProfiles not implemented") +} func (UnimplementedOpenShellServer) UpdateProvider(context.Context, *UpdateProviderRequest) (*ProviderResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method UpdateProvider not implemented") } func (UnimplementedOpenShellServer) DeleteProvider(context.Context, *DeleteProviderRequest) (*DeleteProviderResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method DeleteProvider not implemented") } +func (UnimplementedOpenShellServer) DeleteProviderProfile(context.Context, *DeleteProviderProfileRequest) (*DeleteProviderProfileResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteProviderProfile not implemented") +} func (UnimplementedOpenShellServer) GetSandboxConfig(context.Context, *sandboxv1.GetSandboxConfigRequest) (*sandboxv1.GetSandboxConfigResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetSandboxConfig not implemented") } @@ -843,6 +1107,60 @@ func _OpenShell_ListSandboxes_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _OpenShell_ListSandboxProviders_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListSandboxProvidersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ListSandboxProviders(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ListSandboxProviders_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ListSandboxProviders(ctx, req.(*ListSandboxProvidersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_AttachSandboxProvider_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AttachSandboxProviderRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).AttachSandboxProvider(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_AttachSandboxProvider_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).AttachSandboxProvider(ctx, req.(*AttachSandboxProviderRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_DetachSandboxProvider_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DetachSandboxProviderRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).DetachSandboxProvider(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_DetachSandboxProvider_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).DetachSandboxProvider(ctx, req.(*DetachSandboxProviderRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _OpenShell_DeleteSandbox_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DeleteSandboxRequest) if err := dec(in); err != nil { @@ -879,6 +1197,78 @@ func _OpenShell_CreateSshSession_Handler(srv interface{}, ctx context.Context, d return interceptor(ctx, in, info, handler) } +func _OpenShell_ExposeService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ExposeServiceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ExposeService(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ExposeService_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ExposeService(ctx, req.(*ExposeServiceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_GetService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetServiceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetService(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetService_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetService(ctx, req.(*GetServiceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ListServices_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListServicesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ListServices(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ListServices_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ListServices(ctx, req.(*ListServicesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_DeleteService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteServiceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).DeleteService(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_DeleteService_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).DeleteService(ctx, req.(*DeleteServiceRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _OpenShell_RevokeSshSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(RevokeSshSessionRequest) if err := dec(in); err != nil { @@ -908,6 +1298,20 @@ func _OpenShell_ExecSandbox_Handler(srv interface{}, stream grpc.ServerStream) e // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type OpenShell_ExecSandboxServer = grpc.ServerStreamingServer[ExecSandboxEvent] +func _OpenShell_ForwardTcp_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(OpenShellServer).ForwardTcp(&grpc.GenericServerStream[TcpForwardFrame, TcpForwardFrame]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type OpenShell_ForwardTcpServer = grpc.BidiStreamingServer[TcpForwardFrame, TcpForwardFrame] + +func _OpenShell_ExecSandboxInteractive_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(OpenShellServer).ExecSandboxInteractive(&grpc.GenericServerStream[ExecSandboxInput, ExecSandboxEvent]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type OpenShell_ExecSandboxInteractiveServer = grpc.BidiStreamingServer[ExecSandboxInput, ExecSandboxEvent] + func _OpenShell_CreateProvider_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CreateProviderRequest) if err := dec(in); err != nil { @@ -962,6 +1366,78 @@ func _OpenShell_ListProviders_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _OpenShell_ListProviderProfiles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListProviderProfilesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ListProviderProfiles(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ListProviderProfiles_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ListProviderProfiles(ctx, req.(*ListProviderProfilesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_GetProviderProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetProviderProfileRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetProviderProfile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetProviderProfile_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetProviderProfile(ctx, req.(*GetProviderProfileRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ImportProviderProfiles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ImportProviderProfilesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ImportProviderProfiles(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ImportProviderProfiles_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ImportProviderProfiles(ctx, req.(*ImportProviderProfilesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_LintProviderProfiles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LintProviderProfilesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).LintProviderProfiles(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_LintProviderProfiles_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).LintProviderProfiles(ctx, req.(*LintProviderProfilesRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _OpenShell_UpdateProvider_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(UpdateProviderRequest) if err := dec(in); err != nil { @@ -998,6 +1474,24 @@ func _OpenShell_DeleteProvider_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _OpenShell_DeleteProviderProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteProviderProfileRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).DeleteProviderProfile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_DeleteProviderProfile_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).DeleteProviderProfile(ctx, req.(*DeleteProviderProfileRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _OpenShell_GetSandboxConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(sandboxv1.GetSandboxConfigRequest) if err := dec(in); err != nil { @@ -1359,6 +1853,18 @@ var OpenShell_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListSandboxes", Handler: _OpenShell_ListSandboxes_Handler, }, + { + MethodName: "ListSandboxProviders", + Handler: _OpenShell_ListSandboxProviders_Handler, + }, + { + MethodName: "AttachSandboxProvider", + Handler: _OpenShell_AttachSandboxProvider_Handler, + }, + { + MethodName: "DetachSandboxProvider", + Handler: _OpenShell_DetachSandboxProvider_Handler, + }, { MethodName: "DeleteSandbox", Handler: _OpenShell_DeleteSandbox_Handler, @@ -1367,6 +1873,22 @@ var OpenShell_ServiceDesc = grpc.ServiceDesc{ MethodName: "CreateSshSession", Handler: _OpenShell_CreateSshSession_Handler, }, + { + MethodName: "ExposeService", + Handler: _OpenShell_ExposeService_Handler, + }, + { + MethodName: "GetService", + Handler: _OpenShell_GetService_Handler, + }, + { + MethodName: "ListServices", + Handler: _OpenShell_ListServices_Handler, + }, + { + MethodName: "DeleteService", + Handler: _OpenShell_DeleteService_Handler, + }, { MethodName: "RevokeSshSession", Handler: _OpenShell_RevokeSshSession_Handler, @@ -1383,6 +1905,22 @@ var OpenShell_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListProviders", Handler: _OpenShell_ListProviders_Handler, }, + { + MethodName: "ListProviderProfiles", + Handler: _OpenShell_ListProviderProfiles_Handler, + }, + { + MethodName: "GetProviderProfile", + Handler: _OpenShell_GetProviderProfile_Handler, + }, + { + MethodName: "ImportProviderProfiles", + Handler: _OpenShell_ImportProviderProfiles_Handler, + }, + { + MethodName: "LintProviderProfiles", + Handler: _OpenShell_LintProviderProfiles_Handler, + }, { MethodName: "UpdateProvider", Handler: _OpenShell_UpdateProvider_Handler, @@ -1391,6 +1929,10 @@ var OpenShell_ServiceDesc = grpc.ServiceDesc{ MethodName: "DeleteProvider", Handler: _OpenShell_DeleteProvider_Handler, }, + { + MethodName: "DeleteProviderProfile", + Handler: _OpenShell_DeleteProviderProfile_Handler, + }, { MethodName: "GetSandboxConfig", Handler: _OpenShell_GetSandboxConfig_Handler, @@ -1466,6 +2008,18 @@ var OpenShell_ServiceDesc = grpc.ServiceDesc{ Handler: _OpenShell_ExecSandbox_Handler, ServerStreams: true, }, + { + StreamName: "ForwardTcp", + Handler: _OpenShell_ForwardTcp_Handler, + ServerStreams: true, + ClientStreams: true, + }, + { + StreamName: "ExecSandboxInteractive", + Handler: _OpenShell_ExecSandboxInteractive_Handler, + ServerStreams: true, + ClientStreams: true, + }, { StreamName: "PushSandboxLogs", Handler: _OpenShell_PushSandboxLogs_Handler, diff --git a/go/api/openshell/gen/sandboxv1/sandbox.pb.go b/go/api/openshell/gen/sandboxv1/sandbox.pb.go index 821715f31d..7a14bc5214 100644 --- a/go/api/openshell/gen/sandboxv1/sandbox.pb.go +++ b/go/api/openshell/gen/sandboxv1/sandbox.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v3.15.8 +// protoc (unknown) // source: sandbox.proto package sandboxv1 @@ -445,7 +445,7 @@ type NetworkEndpoint struct { // Single port (backwards compat). Use `ports` for multiple ports. // Mutually exclusive with `ports` — if both are set, `ports` takes precedence. Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` - // Application protocol for L7 inspection: "rest", "sql", or "" (L4-only). + // Application protocol for L7 inspection: "rest", "websocket", "graphql", "sql", or "" (L4-only). Protocol string `protobuf:"bytes,3,opt,name=protocol,proto3" json:"protocol,omitempty"` // TLS handling: "terminate" or "passthrough" (default). Tls string `protobuf:"bytes,4,opt,name=tls,proto3" json:"tls,omitempty"` @@ -478,8 +478,30 @@ type NetworkEndpoint struct { // upstreams like GitLab that embed %2F in namespaced resource paths. // Defaults to false (strict). AllowEncodedSlash bool `protobuf:"varint,11,opt,name=allow_encoded_slash,json=allowEncodedSlash,proto3" json:"allow_encoded_slash,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // GraphQL persisted-query behavior for hash-only/saved-query requests: + // "deny" (default) or "allow_registered". + PersistedQueries string `protobuf:"bytes,12,opt,name=persisted_queries,json=persistedQueries,proto3" json:"persisted_queries,omitempty"` + // Trusted GraphQL persisted-query registry keyed by hash or service-specific ID. + // Only used when persisted_queries is "allow_registered". + GraphqlPersistedQueries map[string]*GraphqlOperation `protobuf:"bytes,13,rep,name=graphql_persisted_queries,json=graphqlPersistedQueries,proto3" json:"graphql_persisted_queries,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Maximum GraphQL request body bytes to buffer for inspection. + // Defaults to 65536 when unset. + GraphqlMaxBodyBytes uint32 `protobuf:"varint,14,opt,name=graphql_max_body_bytes,json=graphqlMaxBodyBytes,proto3" json:"graphql_max_body_bytes,omitempty"` + // Optional HTTP path glob that scopes this L7 endpoint on shared host:port APIs. + // Example: use path "/graphql" for protocol "graphql" and "/repos/**" for + // protocol "rest" when both surfaces live under api.example.com:443. + // Empty means all paths. + Path string `protobuf:"bytes,15,opt,name=path,proto3" json:"path,omitempty"` + // When true on a "rest" endpoint, OpenShell rewrites credential placeholders + // inside client-to-server WebSocket text messages after an allowed HTTP 101 + // upgrade. Defaults to false. + WebsocketCredentialRewrite bool `protobuf:"varint,16,opt,name=websocket_credential_rewrite,json=websocketCredentialRewrite,proto3" json:"websocket_credential_rewrite,omitempty"` + // When true on a "rest" endpoint, OpenShell rewrites credential placeholders + // inside supported textual HTTP request bodies before forwarding upstream. + // Defaults to false. + RequestBodyCredentialRewrite bool `protobuf:"varint,17,opt,name=request_body_credential_rewrite,json=requestBodyCredentialRewrite,proto3" json:"request_body_credential_rewrite,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *NetworkEndpoint) Reset() { @@ -589,6 +611,112 @@ func (x *NetworkEndpoint) GetAllowEncodedSlash() bool { return false } +func (x *NetworkEndpoint) GetPersistedQueries() string { + if x != nil { + return x.PersistedQueries + } + return "" +} + +func (x *NetworkEndpoint) GetGraphqlPersistedQueries() map[string]*GraphqlOperation { + if x != nil { + return x.GraphqlPersistedQueries + } + return nil +} + +func (x *NetworkEndpoint) GetGraphqlMaxBodyBytes() uint32 { + if x != nil { + return x.GraphqlMaxBodyBytes + } + return 0 +} + +func (x *NetworkEndpoint) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *NetworkEndpoint) GetWebsocketCredentialRewrite() bool { + if x != nil { + return x.WebsocketCredentialRewrite + } + return false +} + +func (x *NetworkEndpoint) GetRequestBodyCredentialRewrite() bool { + if x != nil { + return x.RequestBodyCredentialRewrite + } + return false +} + +// Trusted GraphQL operation classification. +type GraphqlOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Operation type: "query", "mutation", or "subscription". + OperationType string `protobuf:"bytes,1,opt,name=operation_type,json=operationType,proto3" json:"operation_type,omitempty"` + // Operation name, if known. + OperationName string `protobuf:"bytes,2,opt,name=operation_name,json=operationName,proto3" json:"operation_name,omitempty"` + // Root field names selected by the operation. + Fields []string `protobuf:"bytes,3,rep,name=fields,proto3" json:"fields,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GraphqlOperation) Reset() { + *x = GraphqlOperation{} + mi := &file_sandbox_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GraphqlOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GraphqlOperation) ProtoMessage() {} + +func (x *GraphqlOperation) ProtoReflect() protoreflect.Message { + mi := &file_sandbox_proto_msgTypes[6] + 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 GraphqlOperation.ProtoReflect.Descriptor instead. +func (*GraphqlOperation) Descriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{6} +} + +func (x *GraphqlOperation) GetOperationType() string { + if x != nil { + return x.OperationType + } + return "" +} + +func (x *GraphqlOperation) GetOperationName() string { + if x != nil { + return x.OperationName + } + return "" +} + +func (x *GraphqlOperation) GetFields() []string { + if x != nil { + return x.Fields + } + return nil +} + // An L7 deny rule that blocks specific requests. // Mirrors L7Allow — same fields, same matching semantics, inverted effect. // Deny rules are evaluated after allow rules and take precedence. @@ -602,14 +730,21 @@ type L7DenyRule struct { Command string `protobuf:"bytes,3,opt,name=command,proto3" json:"command,omitempty"` // Query parameter matcher map (REST). // Same semantics as L7Allow.query. - Query map[string]*L7QueryMatcher `protobuf:"bytes,4,rep,name=query,proto3" json:"query,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Query map[string]*L7QueryMatcher `protobuf:"bytes,4,rep,name=query,proto3" json:"query,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // GraphQL operation type: "query", "mutation", "subscription", or "*" for any. + OperationType string `protobuf:"bytes,5,opt,name=operation_type,json=operationType,proto3" json:"operation_type,omitempty"` + // GraphQL operation name glob. "*" matches any operation name. + OperationName string `protobuf:"bytes,6,opt,name=operation_name,json=operationName,proto3" json:"operation_name,omitempty"` + // GraphQL root field globs. Deny rules match when any selected root field + // matches any configured glob. + Fields []string `protobuf:"bytes,7,rep,name=fields,proto3" json:"fields,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *L7DenyRule) Reset() { *x = L7DenyRule{} - mi := &file_sandbox_proto_msgTypes[6] + mi := &file_sandbox_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -621,7 +756,7 @@ func (x *L7DenyRule) String() string { func (*L7DenyRule) ProtoMessage() {} func (x *L7DenyRule) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[6] + mi := &file_sandbox_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -634,7 +769,7 @@ func (x *L7DenyRule) ProtoReflect() protoreflect.Message { // Deprecated: Use L7DenyRule.ProtoReflect.Descriptor instead. func (*L7DenyRule) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{6} + return file_sandbox_proto_rawDescGZIP(), []int{7} } func (x *L7DenyRule) GetMethod() string { @@ -665,6 +800,27 @@ func (x *L7DenyRule) GetQuery() map[string]*L7QueryMatcher { return nil } +func (x *L7DenyRule) GetOperationType() string { + if x != nil { + return x.OperationType + } + return "" +} + +func (x *L7DenyRule) GetOperationName() string { + if x != nil { + return x.OperationName + } + return "" +} + +func (x *L7DenyRule) GetFields() []string { + if x != nil { + return x.Fields + } + return nil +} + // An L7 policy rule (allow-only). type L7Rule struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -675,7 +831,7 @@ type L7Rule struct { func (x *L7Rule) Reset() { *x = L7Rule{} - mi := &file_sandbox_proto_msgTypes[7] + mi := &file_sandbox_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -687,7 +843,7 @@ func (x *L7Rule) String() string { func (*L7Rule) ProtoMessage() {} func (x *L7Rule) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[7] + mi := &file_sandbox_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -700,7 +856,7 @@ func (x *L7Rule) ProtoReflect() protoreflect.Message { // Deprecated: Use L7Rule.ProtoReflect.Descriptor instead. func (*L7Rule) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{7} + return file_sandbox_proto_rawDescGZIP(), []int{8} } func (x *L7Rule) GetAllow() *L7Allow { @@ -722,14 +878,21 @@ type L7Allow struct { // Query parameter matcher map (REST). // Key is the decoded query parameter name (case-sensitive). // Value supports either a single glob (`glob`) or a list (`any`). - Query map[string]*L7QueryMatcher `protobuf:"bytes,4,rep,name=query,proto3" json:"query,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Query map[string]*L7QueryMatcher `protobuf:"bytes,4,rep,name=query,proto3" json:"query,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // GraphQL operation type: "query", "mutation", "subscription", or "*" for any. + OperationType string `protobuf:"bytes,5,opt,name=operation_type,json=operationType,proto3" json:"operation_type,omitempty"` + // GraphQL operation name glob. "*" matches any operation name. + OperationName string `protobuf:"bytes,6,opt,name=operation_name,json=operationName,proto3" json:"operation_name,omitempty"` + // GraphQL root field globs. Allow rules match only when every selected root + // field matches one of the configured globs. Omit to match all fields. + Fields []string `protobuf:"bytes,7,rep,name=fields,proto3" json:"fields,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *L7Allow) Reset() { *x = L7Allow{} - mi := &file_sandbox_proto_msgTypes[8] + mi := &file_sandbox_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -741,7 +904,7 @@ func (x *L7Allow) String() string { func (*L7Allow) ProtoMessage() {} func (x *L7Allow) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[8] + mi := &file_sandbox_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -754,7 +917,7 @@ func (x *L7Allow) ProtoReflect() protoreflect.Message { // Deprecated: Use L7Allow.ProtoReflect.Descriptor instead. func (*L7Allow) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{8} + return file_sandbox_proto_rawDescGZIP(), []int{9} } func (x *L7Allow) GetMethod() string { @@ -785,6 +948,27 @@ func (x *L7Allow) GetQuery() map[string]*L7QueryMatcher { return nil } +func (x *L7Allow) GetOperationType() string { + if x != nil { + return x.OperationType + } + return "" +} + +func (x *L7Allow) GetOperationName() string { + if x != nil { + return x.OperationName + } + return "" +} + +func (x *L7Allow) GetFields() []string { + if x != nil { + return x.Fields + } + return nil +} + // Query value matcher for one query parameter key. type L7QueryMatcher struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -798,7 +982,7 @@ type L7QueryMatcher struct { func (x *L7QueryMatcher) Reset() { *x = L7QueryMatcher{} - mi := &file_sandbox_proto_msgTypes[9] + mi := &file_sandbox_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -810,7 +994,7 @@ func (x *L7QueryMatcher) String() string { func (*L7QueryMatcher) ProtoMessage() {} func (x *L7QueryMatcher) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[9] + mi := &file_sandbox_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -823,7 +1007,7 @@ func (x *L7QueryMatcher) ProtoReflect() protoreflect.Message { // Deprecated: Use L7QueryMatcher.ProtoReflect.Descriptor instead. func (*L7QueryMatcher) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{9} + return file_sandbox_proto_rawDescGZIP(), []int{10} } func (x *L7QueryMatcher) GetGlob() string { @@ -854,7 +1038,7 @@ type NetworkBinary struct { func (x *NetworkBinary) Reset() { *x = NetworkBinary{} - mi := &file_sandbox_proto_msgTypes[10] + mi := &file_sandbox_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -866,7 +1050,7 @@ func (x *NetworkBinary) String() string { func (*NetworkBinary) ProtoMessage() {} func (x *NetworkBinary) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[10] + mi := &file_sandbox_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -879,7 +1063,7 @@ func (x *NetworkBinary) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkBinary.ProtoReflect.Descriptor instead. func (*NetworkBinary) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{10} + return file_sandbox_proto_rawDescGZIP(), []int{11} } func (x *NetworkBinary) GetPath() string { @@ -908,7 +1092,7 @@ type GetSandboxConfigRequest struct { func (x *GetSandboxConfigRequest) Reset() { *x = GetSandboxConfigRequest{} - mi := &file_sandbox_proto_msgTypes[11] + mi := &file_sandbox_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -920,7 +1104,7 @@ func (x *GetSandboxConfigRequest) String() string { func (*GetSandboxConfigRequest) ProtoMessage() {} func (x *GetSandboxConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[11] + mi := &file_sandbox_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -933,7 +1117,7 @@ func (x *GetSandboxConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxConfigRequest.ProtoReflect.Descriptor instead. func (*GetSandboxConfigRequest) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{11} + return file_sandbox_proto_rawDescGZIP(), []int{12} } func (x *GetSandboxConfigRequest) GetSandboxId() string { @@ -952,7 +1136,7 @@ type GetGatewayConfigRequest struct { func (x *GetGatewayConfigRequest) Reset() { *x = GetGatewayConfigRequest{} - mi := &file_sandbox_proto_msgTypes[12] + mi := &file_sandbox_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -964,7 +1148,7 @@ func (x *GetGatewayConfigRequest) String() string { func (*GetGatewayConfigRequest) ProtoMessage() {} func (x *GetGatewayConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[12] + mi := &file_sandbox_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -977,7 +1161,7 @@ func (x *GetGatewayConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetGatewayConfigRequest.ProtoReflect.Descriptor instead. func (*GetGatewayConfigRequest) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{12} + return file_sandbox_proto_rawDescGZIP(), []int{13} } // Response containing gateway-global settings. @@ -994,7 +1178,7 @@ type GetGatewayConfigResponse struct { func (x *GetGatewayConfigResponse) Reset() { *x = GetGatewayConfigResponse{} - mi := &file_sandbox_proto_msgTypes[13] + mi := &file_sandbox_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1006,7 +1190,7 @@ func (x *GetGatewayConfigResponse) String() string { func (*GetGatewayConfigResponse) ProtoMessage() {} func (x *GetGatewayConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[13] + mi := &file_sandbox_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1019,7 +1203,7 @@ func (x *GetGatewayConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetGatewayConfigResponse.ProtoReflect.Descriptor instead. func (*GetGatewayConfigResponse) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{13} + return file_sandbox_proto_rawDescGZIP(), []int{14} } func (x *GetGatewayConfigResponse) GetSettings() map[string]*SettingValue { @@ -1052,7 +1236,7 @@ type SettingValue struct { func (x *SettingValue) Reset() { *x = SettingValue{} - mi := &file_sandbox_proto_msgTypes[14] + mi := &file_sandbox_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1064,7 +1248,7 @@ func (x *SettingValue) String() string { func (*SettingValue) ProtoMessage() {} func (x *SettingValue) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[14] + mi := &file_sandbox_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1077,7 +1261,7 @@ func (x *SettingValue) ProtoReflect() protoreflect.Message { // Deprecated: Use SettingValue.ProtoReflect.Descriptor instead. func (*SettingValue) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{14} + return file_sandbox_proto_rawDescGZIP(), []int{15} } func (x *SettingValue) GetValue() isSettingValue_Value { @@ -1162,7 +1346,7 @@ type EffectiveSetting struct { func (x *EffectiveSetting) Reset() { *x = EffectiveSetting{} - mi := &file_sandbox_proto_msgTypes[15] + mi := &file_sandbox_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1174,7 +1358,7 @@ func (x *EffectiveSetting) String() string { func (*EffectiveSetting) ProtoMessage() {} func (x *EffectiveSetting) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[15] + mi := &file_sandbox_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1187,7 +1371,7 @@ func (x *EffectiveSetting) ProtoReflect() protoreflect.Message { // Deprecated: Use EffectiveSetting.ProtoReflect.Descriptor instead. func (*EffectiveSetting) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{15} + return file_sandbox_proto_rawDescGZIP(), []int{16} } func (x *EffectiveSetting) GetValue() *SettingValue { @@ -1223,13 +1407,16 @@ type GetSandboxConfigResponse struct { // When policy_source is GLOBAL, the version of the global policy revision. // Zero when no global policy is active or when policy_source is SANDBOX. GlobalPolicyVersion uint32 `protobuf:"varint,7,opt,name=global_policy_version,json=globalPolicyVersion,proto3" json:"global_policy_version,omitempty"` + // Fingerprint for provider credential inputs attached to this sandbox. + // Changes when attached provider names or attached provider records change. + ProviderEnvRevision uint64 `protobuf:"varint,8,opt,name=provider_env_revision,json=providerEnvRevision,proto3" json:"provider_env_revision,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GetSandboxConfigResponse) Reset() { *x = GetSandboxConfigResponse{} - mi := &file_sandbox_proto_msgTypes[16] + mi := &file_sandbox_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1241,7 +1428,7 @@ func (x *GetSandboxConfigResponse) String() string { func (*GetSandboxConfigResponse) ProtoMessage() {} func (x *GetSandboxConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_sandbox_proto_msgTypes[16] + mi := &file_sandbox_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1254,7 +1441,7 @@ func (x *GetSandboxConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxConfigResponse.ProtoReflect.Descriptor instead. func (*GetSandboxConfigResponse) Descriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{16} + return file_sandbox_proto_rawDescGZIP(), []int{17} } func (x *GetSandboxConfigResponse) GetPolicy() *SandboxPolicy { @@ -1306,6 +1493,13 @@ func (x *GetSandboxConfigResponse) GetGlobalPolicyVersion() uint32 { return 0 } +func (x *GetSandboxConfigResponse) GetProviderEnvRevision() uint64 { + if x != nil { + return x.ProviderEnvRevision + } + return 0 +} + var File_sandbox_proto protoreflect.FileDescriptor const file_sandbox_proto_rawDesc = "" + @@ -1336,7 +1530,7 @@ const file_sandbox_proto_rawDesc = "" + "\x11NetworkPolicyRule\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12C\n" + "\tendpoints\x18\x02 \x03(\v2%.openshell.sandbox.v1.NetworkEndpointR\tendpoints\x12?\n" + - "\bbinaries\x18\x03 \x03(\v2#.openshell.sandbox.v1.NetworkBinaryR\bbinaries\"\xfd\x02\n" + + "\bbinaries\x18\x03 \x03(\v2#.openshell.sandbox.v1.NetworkBinaryR\bbinaries\"\xf0\x06\n" + "\x0fNetworkEndpoint\x12\x12\n" + "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" + "\x04port\x18\x02 \x01(\rR\x04port\x12\x1a\n" + @@ -1351,24 +1545,43 @@ const file_sandbox_proto_rawDesc = "" + "\n" + "deny_rules\x18\n" + " \x03(\v2 .openshell.sandbox.v1.L7DenyRuleR\tdenyRules\x12.\n" + - "\x13allow_encoded_slash\x18\v \x01(\bR\x11allowEncodedSlash\"\xf5\x01\n" + + "\x13allow_encoded_slash\x18\v \x01(\bR\x11allowEncodedSlash\x12+\n" + + "\x11persisted_queries\x18\f \x01(\tR\x10persistedQueries\x12~\n" + + "\x19graphql_persisted_queries\x18\r \x03(\v2B.openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntryR\x17graphqlPersistedQueries\x123\n" + + "\x16graphql_max_body_bytes\x18\x0e \x01(\rR\x13graphqlMaxBodyBytes\x12\x12\n" + + "\x04path\x18\x0f \x01(\tR\x04path\x12@\n" + + "\x1cwebsocket_credential_rewrite\x18\x10 \x01(\bR\x1awebsocketCredentialRewrite\x12E\n" + + "\x1frequest_body_credential_rewrite\x18\x11 \x01(\bR\x1crequestBodyCredentialRewrite\x1ar\n" + + "\x1cGraphqlPersistedQueriesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12<\n" + + "\x05value\x18\x02 \x01(\v2&.openshell.sandbox.v1.GraphqlOperationR\x05value:\x028\x01\"x\n" + + "\x10GraphqlOperation\x12%\n" + + "\x0eoperation_type\x18\x01 \x01(\tR\roperationType\x12%\n" + + "\x0eoperation_name\x18\x02 \x01(\tR\roperationName\x12\x16\n" + + "\x06fields\x18\x03 \x03(\tR\x06fields\"\xdb\x02\n" + "\n" + "L7DenyRule\x12\x16\n" + "\x06method\x18\x01 \x01(\tR\x06method\x12\x12\n" + "\x04path\x18\x02 \x01(\tR\x04path\x12\x18\n" + "\acommand\x18\x03 \x01(\tR\acommand\x12A\n" + - "\x05query\x18\x04 \x03(\v2+.openshell.sandbox.v1.L7DenyRule.QueryEntryR\x05query\x1a^\n" + + "\x05query\x18\x04 \x03(\v2+.openshell.sandbox.v1.L7DenyRule.QueryEntryR\x05query\x12%\n" + + "\x0eoperation_type\x18\x05 \x01(\tR\roperationType\x12%\n" + + "\x0eoperation_name\x18\x06 \x01(\tR\roperationName\x12\x16\n" + + "\x06fields\x18\a \x03(\tR\x06fields\x1a^\n" + "\n" + "QueryEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12:\n" + "\x05value\x18\x02 \x01(\v2$.openshell.sandbox.v1.L7QueryMatcherR\x05value:\x028\x01\"=\n" + "\x06L7Rule\x123\n" + - "\x05allow\x18\x01 \x01(\v2\x1d.openshell.sandbox.v1.L7AllowR\x05allow\"\xef\x01\n" + + "\x05allow\x18\x01 \x01(\v2\x1d.openshell.sandbox.v1.L7AllowR\x05allow\"\xd5\x02\n" + "\aL7Allow\x12\x16\n" + "\x06method\x18\x01 \x01(\tR\x06method\x12\x12\n" + "\x04path\x18\x02 \x01(\tR\x04path\x12\x18\n" + "\acommand\x18\x03 \x01(\tR\acommand\x12>\n" + - "\x05query\x18\x04 \x03(\v2(.openshell.sandbox.v1.L7Allow.QueryEntryR\x05query\x1a^\n" + + "\x05query\x18\x04 \x03(\v2(.openshell.sandbox.v1.L7Allow.QueryEntryR\x05query\x12%\n" + + "\x0eoperation_type\x18\x05 \x01(\tR\roperationType\x12%\n" + + "\x0eoperation_name\x18\x06 \x01(\tR\roperationName\x12\x16\n" + + "\x06fields\x18\a \x03(\tR\x06fields\x1a^\n" + "\n" + "QueryEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12:\n" + @@ -1399,7 +1612,7 @@ const file_sandbox_proto_rawDesc = "" + "\x05value\"\x86\x01\n" + "\x10EffectiveSetting\x128\n" + "\x05value\x18\x01 \x01(\v2\".openshell.sandbox.v1.SettingValueR\x05value\x128\n" + - "\x05scope\x18\x02 \x01(\x0e2\".openshell.sandbox.v1.SettingScopeR\x05scope\"\xf7\x03\n" + + "\x05scope\x18\x02 \x01(\x0e2\".openshell.sandbox.v1.SettingScopeR\x05scope\"\xab\x04\n" + "\x18GetSandboxConfigResponse\x12;\n" + "\x06policy\x18\x01 \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x06policy\x12\x18\n" + "\aversion\x18\x02 \x01(\rR\aversion\x12\x1f\n" + @@ -1408,7 +1621,8 @@ const file_sandbox_proto_rawDesc = "" + "\bsettings\x18\x04 \x03(\v2<.openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntryR\bsettings\x12'\n" + "\x0fconfig_revision\x18\x05 \x01(\x04R\x0econfigRevision\x12G\n" + "\rpolicy_source\x18\x06 \x01(\x0e2\".openshell.sandbox.v1.PolicySourceR\fpolicySource\x122\n" + - "\x15global_policy_version\x18\a \x01(\rR\x13globalPolicyVersion\x1ac\n" + + "\x15global_policy_version\x18\a \x01(\rR\x13globalPolicyVersion\x122\n" + + "\x15provider_env_revision\x18\b \x01(\x04R\x13providerEnvRevision\x1ac\n" + "\rSettingsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12<\n" + "\x05value\x18\x02 \x01(\v2&.openshell.sandbox.v1.EffectiveSettingR\x05value:\x028\x01*b\n" + @@ -1419,7 +1633,8 @@ const file_sandbox_proto_rawDesc = "" + "\fPolicySource\x12\x1d\n" + "\x19POLICY_SOURCE_UNSPECIFIED\x10\x00\x12\x19\n" + "\x15POLICY_SOURCE_SANDBOX\x10\x01\x12\x18\n" + - "\x14POLICY_SOURCE_GLOBAL\x10\x02b\x06proto3" + "\x14POLICY_SOURCE_GLOBAL\x10\x02B\xd7\x01\n" + + "\x18com.openshell.sandbox.v1B\fSandboxProtoP\x01Z;github.com/kagent-dev/kagent/go/api/openshell/gen/sandboxv1\xa2\x02\x03OSX\xaa\x02\x14Openshell.Sandbox.V1\xca\x02\x14Openshell\\Sandbox\\V1\xe2\x02 Openshell\\Sandbox\\V1\\GPBMetadata\xea\x02\x16Openshell::Sandbox::V1b\x06proto3" var ( file_sandbox_proto_rawDescOnce sync.Once @@ -1434,7 +1649,7 @@ func file_sandbox_proto_rawDescGZIP() []byte { } var file_sandbox_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_sandbox_proto_msgTypes = make([]protoimpl.MessageInfo, 22) +var file_sandbox_proto_msgTypes = make([]protoimpl.MessageInfo, 24) var file_sandbox_proto_goTypes = []any{ (SettingScope)(0), // 0: openshell.sandbox.v1.SettingScope (PolicySource)(0), // 1: openshell.sandbox.v1.PolicySource @@ -1444,51 +1659,55 @@ var file_sandbox_proto_goTypes = []any{ (*ProcessPolicy)(nil), // 5: openshell.sandbox.v1.ProcessPolicy (*NetworkPolicyRule)(nil), // 6: openshell.sandbox.v1.NetworkPolicyRule (*NetworkEndpoint)(nil), // 7: openshell.sandbox.v1.NetworkEndpoint - (*L7DenyRule)(nil), // 8: openshell.sandbox.v1.L7DenyRule - (*L7Rule)(nil), // 9: openshell.sandbox.v1.L7Rule - (*L7Allow)(nil), // 10: openshell.sandbox.v1.L7Allow - (*L7QueryMatcher)(nil), // 11: openshell.sandbox.v1.L7QueryMatcher - (*NetworkBinary)(nil), // 12: openshell.sandbox.v1.NetworkBinary - (*GetSandboxConfigRequest)(nil), // 13: openshell.sandbox.v1.GetSandboxConfigRequest - (*GetGatewayConfigRequest)(nil), // 14: openshell.sandbox.v1.GetGatewayConfigRequest - (*GetGatewayConfigResponse)(nil), // 15: openshell.sandbox.v1.GetGatewayConfigResponse - (*SettingValue)(nil), // 16: openshell.sandbox.v1.SettingValue - (*EffectiveSetting)(nil), // 17: openshell.sandbox.v1.EffectiveSetting - (*GetSandboxConfigResponse)(nil), // 18: openshell.sandbox.v1.GetSandboxConfigResponse - nil, // 19: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry - nil, // 20: openshell.sandbox.v1.L7DenyRule.QueryEntry - nil, // 21: openshell.sandbox.v1.L7Allow.QueryEntry - nil, // 22: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry - nil, // 23: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry + (*GraphqlOperation)(nil), // 8: openshell.sandbox.v1.GraphqlOperation + (*L7DenyRule)(nil), // 9: openshell.sandbox.v1.L7DenyRule + (*L7Rule)(nil), // 10: openshell.sandbox.v1.L7Rule + (*L7Allow)(nil), // 11: openshell.sandbox.v1.L7Allow + (*L7QueryMatcher)(nil), // 12: openshell.sandbox.v1.L7QueryMatcher + (*NetworkBinary)(nil), // 13: openshell.sandbox.v1.NetworkBinary + (*GetSandboxConfigRequest)(nil), // 14: openshell.sandbox.v1.GetSandboxConfigRequest + (*GetGatewayConfigRequest)(nil), // 15: openshell.sandbox.v1.GetGatewayConfigRequest + (*GetGatewayConfigResponse)(nil), // 16: openshell.sandbox.v1.GetGatewayConfigResponse + (*SettingValue)(nil), // 17: openshell.sandbox.v1.SettingValue + (*EffectiveSetting)(nil), // 18: openshell.sandbox.v1.EffectiveSetting + (*GetSandboxConfigResponse)(nil), // 19: openshell.sandbox.v1.GetSandboxConfigResponse + nil, // 20: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry + nil, // 21: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry + nil, // 22: openshell.sandbox.v1.L7DenyRule.QueryEntry + nil, // 23: openshell.sandbox.v1.L7Allow.QueryEntry + nil, // 24: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry + nil, // 25: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry } var file_sandbox_proto_depIdxs = []int32{ 3, // 0: openshell.sandbox.v1.SandboxPolicy.filesystem:type_name -> openshell.sandbox.v1.FilesystemPolicy 4, // 1: openshell.sandbox.v1.SandboxPolicy.landlock:type_name -> openshell.sandbox.v1.LandlockPolicy 5, // 2: openshell.sandbox.v1.SandboxPolicy.process:type_name -> openshell.sandbox.v1.ProcessPolicy - 19, // 3: openshell.sandbox.v1.SandboxPolicy.network_policies:type_name -> openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry + 20, // 3: openshell.sandbox.v1.SandboxPolicy.network_policies:type_name -> openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry 7, // 4: openshell.sandbox.v1.NetworkPolicyRule.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 12, // 5: openshell.sandbox.v1.NetworkPolicyRule.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 9, // 6: openshell.sandbox.v1.NetworkEndpoint.rules:type_name -> openshell.sandbox.v1.L7Rule - 8, // 7: openshell.sandbox.v1.NetworkEndpoint.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 20, // 8: openshell.sandbox.v1.L7DenyRule.query:type_name -> openshell.sandbox.v1.L7DenyRule.QueryEntry - 10, // 9: openshell.sandbox.v1.L7Rule.allow:type_name -> openshell.sandbox.v1.L7Allow - 21, // 10: openshell.sandbox.v1.L7Allow.query:type_name -> openshell.sandbox.v1.L7Allow.QueryEntry - 22, // 11: openshell.sandbox.v1.GetGatewayConfigResponse.settings:type_name -> openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry - 16, // 12: openshell.sandbox.v1.EffectiveSetting.value:type_name -> openshell.sandbox.v1.SettingValue - 0, // 13: openshell.sandbox.v1.EffectiveSetting.scope:type_name -> openshell.sandbox.v1.SettingScope - 2, // 14: openshell.sandbox.v1.GetSandboxConfigResponse.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 23, // 15: openshell.sandbox.v1.GetSandboxConfigResponse.settings:type_name -> openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry - 1, // 16: openshell.sandbox.v1.GetSandboxConfigResponse.policy_source:type_name -> openshell.sandbox.v1.PolicySource - 6, // 17: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry.value:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 11, // 18: openshell.sandbox.v1.L7DenyRule.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher - 11, // 19: openshell.sandbox.v1.L7Allow.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher - 16, // 20: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.SettingValue - 17, // 21: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.EffectiveSetting - 22, // [22:22] is the sub-list for method output_type - 22, // [22:22] is the sub-list for method input_type - 22, // [22:22] is the sub-list for extension type_name - 22, // [22:22] is the sub-list for extension extendee - 0, // [0:22] is the sub-list for field type_name + 13, // 5: openshell.sandbox.v1.NetworkPolicyRule.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 10, // 6: openshell.sandbox.v1.NetworkEndpoint.rules:type_name -> openshell.sandbox.v1.L7Rule + 9, // 7: openshell.sandbox.v1.NetworkEndpoint.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 21, // 8: openshell.sandbox.v1.NetworkEndpoint.graphql_persisted_queries:type_name -> openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry + 22, // 9: openshell.sandbox.v1.L7DenyRule.query:type_name -> openshell.sandbox.v1.L7DenyRule.QueryEntry + 11, // 10: openshell.sandbox.v1.L7Rule.allow:type_name -> openshell.sandbox.v1.L7Allow + 23, // 11: openshell.sandbox.v1.L7Allow.query:type_name -> openshell.sandbox.v1.L7Allow.QueryEntry + 24, // 12: openshell.sandbox.v1.GetGatewayConfigResponse.settings:type_name -> openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry + 17, // 13: openshell.sandbox.v1.EffectiveSetting.value:type_name -> openshell.sandbox.v1.SettingValue + 0, // 14: openshell.sandbox.v1.EffectiveSetting.scope:type_name -> openshell.sandbox.v1.SettingScope + 2, // 15: openshell.sandbox.v1.GetSandboxConfigResponse.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 25, // 16: openshell.sandbox.v1.GetSandboxConfigResponse.settings:type_name -> openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry + 1, // 17: openshell.sandbox.v1.GetSandboxConfigResponse.policy_source:type_name -> openshell.sandbox.v1.PolicySource + 6, // 18: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry.value:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 8, // 19: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry.value:type_name -> openshell.sandbox.v1.GraphqlOperation + 12, // 20: openshell.sandbox.v1.L7DenyRule.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 12, // 21: openshell.sandbox.v1.L7Allow.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 17, // 22: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.SettingValue + 18, // 23: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.EffectiveSetting + 24, // [24:24] is the sub-list for method output_type + 24, // [24:24] is the sub-list for method input_type + 24, // [24:24] is the sub-list for extension type_name + 24, // [24:24] is the sub-list for extension extendee + 0, // [0:24] is the sub-list for field type_name } func init() { file_sandbox_proto_init() } @@ -1496,7 +1715,7 @@ func file_sandbox_proto_init() { if File_sandbox_proto != nil { return } - file_sandbox_proto_msgTypes[14].OneofWrappers = []any{ + file_sandbox_proto_msgTypes[15].OneofWrappers = []any{ (*SettingValue_StringValue)(nil), (*SettingValue_BoolValue)(nil), (*SettingValue_IntValue)(nil), @@ -1508,7 +1727,7 @@ func file_sandbox_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_sandbox_proto_rawDesc), len(file_sandbox_proto_rawDesc)), NumEnums: 2, - NumMessages: 22, + NumMessages: 24, NumExtensions: 0, NumServices: 0, }, diff --git a/go/api/openshell/proto/compute_driver.proto b/go/api/openshell/proto/compute_driver.proto index 68af695e51..3c4308f3f0 100644 --- a/go/api/openshell/proto/compute_driver.proto +++ b/go/api/openshell/proto/compute_driver.proto @@ -53,6 +53,8 @@ message GetCapabilitiesResponse { string default_image = 3; // True when the driver can provision GPU-backed sandboxes. bool supports_gpu = 4; + // Number of GPUs available for sandbox assignment. + uint32 gpu_count = 5; } // Driver-owned sandbox model used for create requests and platform observations. @@ -84,6 +86,10 @@ message DriverSandboxSpec { DriverSandboxTemplate template = 6; // Request NVIDIA GPU resources for this sandbox. bool gpu = 9; + // Optional PCI BDF address (e.g. "0000:2d:00.0") or device index + // (e.g. "0", "1"). When empty with gpu=true, the driver assigns the + // first available GPU. + string gpu_device = 10; } // Driver-owned runtime template consumed by the compute platform. diff --git a/go/api/openshell/proto/datamodel.proto b/go/api/openshell/proto/datamodel.proto index 534b043ae8..4620881246 100644 --- a/go/api/openshell/proto/datamodel.proto +++ b/go/api/openshell/proto/datamodel.proto @@ -8,7 +8,7 @@ package openshell.datamodel.v1; // Kubernetes-style metadata shared by all top-level OpenShell domain objects. // // This structure provides consistent metadata (identity, labels, timestamps, -// versioning) across Sandbox, Provider, SshSession, and other resources. +// resource versioning) across Sandbox, Provider, SshSession, and other resources. message ObjectMeta { // Stable object ID generated by the gateway. string id = 1; @@ -22,6 +22,10 @@ message ObjectMeta { // Key-value labels for filtering and organization. // Labels must follow Kubernetes conventions: alphanumeric + `-._/`, max 63 chars per segment. map labels = 4; + + // Optimistic concurrency control version. + // Incremented by the gateway on each update. Clients can use this for compare-and-swap operations. + uint64 resource_version = 5; } // Provider model stored by OpenShell. diff --git a/go/api/openshell/proto/openshell.proto b/go/api/openshell/proto/openshell.proto index 1d7eba2186..990698d9b1 100644 --- a/go/api/openshell/proto/openshell.proto +++ b/go/api/openshell/proto/openshell.proto @@ -30,18 +30,50 @@ service OpenShell { // List sandboxes. rpc ListSandboxes(ListSandboxesRequest) returns (ListSandboxesResponse); + // List provider records attached to a sandbox. + rpc ListSandboxProviders(ListSandboxProvidersRequest) + returns (ListSandboxProvidersResponse); + + // Attach a provider record to an existing sandbox. + rpc AttachSandboxProvider(AttachSandboxProviderRequest) + returns (AttachSandboxProviderResponse); + + // Detach a provider record from an existing sandbox. + rpc DetachSandboxProvider(DetachSandboxProviderRequest) + returns (DetachSandboxProviderResponse); + // Delete a sandbox by name. rpc DeleteSandbox(DeleteSandboxRequest) returns (DeleteSandboxResponse); // Create a short-lived SSH session for a sandbox. rpc CreateSshSession(CreateSshSessionRequest) returns (CreateSshSessionResponse); + // Create or update a sandbox HTTP service endpoint for local routing. + rpc ExposeService(ExposeServiceRequest) returns (ServiceEndpointResponse); + + // Fetch one sandbox HTTP service endpoint. + rpc GetService(GetServiceRequest) returns (ServiceEndpointResponse); + + // List sandbox HTTP service endpoints. + rpc ListServices(ListServicesRequest) returns (ListServicesResponse); + + // Delete one sandbox HTTP service endpoint. + rpc DeleteService(DeleteServiceRequest) returns (DeleteServiceResponse); + // Revoke a previously issued SSH session. rpc RevokeSshSession(RevokeSshSessionRequest) returns (RevokeSshSessionResponse); // Execute a command in a ready sandbox and stream output. rpc ExecSandbox(ExecSandboxRequest) returns (stream ExecSandboxEvent); + // Forward one CLI-side TCP connection to a loopback TCP target in a sandbox. + rpc ForwardTcp(stream TcpForwardFrame) returns (stream TcpForwardFrame); + + // Execute an interactive command with bidirectional stdin/stdout streaming. + // The first client message MUST carry an ExecSandboxInput with the start + // variant. Subsequent messages carry stdin bytes or window resize events. + rpc ExecSandboxInteractive(stream ExecSandboxInput) returns (stream ExecSandboxEvent); + // Create a provider. rpc CreateProvider(CreateProviderRequest) returns (ProviderResponse); @@ -51,12 +83,32 @@ service OpenShell { // List providers. rpc ListProviders(ListProvidersRequest) returns (ListProvidersResponse); + // List available provider type profiles. + rpc ListProviderProfiles(ListProviderProfilesRequest) + returns (ListProviderProfilesResponse); + + // Fetch one provider type profile by id. + rpc GetProviderProfile(GetProviderProfileRequest) + returns (ProviderProfileResponse); + + // Import custom provider type profiles. + rpc ImportProviderProfiles(ImportProviderProfilesRequest) + returns (ImportProviderProfilesResponse); + + // Validate provider type profiles without registering them. + rpc LintProviderProfiles(LintProviderProfilesRequest) + returns (LintProviderProfilesResponse); + // Update an existing provider by name. rpc UpdateProvider(UpdateProviderRequest) returns (ProviderResponse); // Delete a provider by name. rpc DeleteProvider(DeleteProviderRequest) returns (DeleteProviderResponse); + // Delete a custom provider type profile by id. + rpc DeleteProviderProfile(DeleteProviderProfileRequest) + returns (DeleteProviderProfileResponse); + // Get sandbox settings by id (called by sandbox entrypoint and poll loop). rpc GetSandboxConfig(openshell.sandbox.v1.GetSandboxConfigRequest) returns (openshell.sandbox.v1.GetSandboxConfigResponse); @@ -95,8 +147,9 @@ service OpenShell { // // The supervisor opens this stream at startup and keeps it alive for the // sandbox lifetime. The gateway uses it to coordinate relay channels for - // SSH connect and ExecSandbox. Raw SSH bytes flow over RelayStream calls - // (separate HTTP/2 streams on the same connection), not over this stream. + // SSH connect, ExecSandbox, and targetable sandbox services. Raw service + // bytes flow over RelayStream calls (separate HTTP/2 streams on the same + // connection), not over this stream. rpc ConnectSupervisor(stream SupervisorMessage) returns (stream GatewayMessage); // Raw byte relay between supervisor and gateway. @@ -105,8 +158,8 @@ service OpenShell { // on its ConnectSupervisor stream. The first RelayFrame carries a // RelayInit with the channel_id to associate the new HTTP/2 stream with // the pending relay slot on the gateway. Subsequent frames carry raw bytes in either - // direction between the gateway-side waiter (ssh_tunnel / exec handler) - // and the supervisor-side local SSH daemon bridge. + // direction between the gateway-side waiter (ForwardTcp / exec handler) + // and the supervisor-side target bridge. // // This rides the same TCP+TLS+HTTP/2 connection as ConnectSupervisor — // no new TLS handshake, no reverse HTTP CONNECT. @@ -204,6 +257,10 @@ message SandboxSpec { repeated string providers = 8; // Request NVIDIA GPU resources for this sandbox. bool gpu = 9; + // Optional PCI BDF address (e.g. "0000:2d:00.0") or device index + // (e.g. "0", "1"). When empty with gpu=true, the driver assigns the + // first available GPU. + string gpu_device = 10; } // Public sandbox template mapped onto compute-driver template inputs. @@ -224,6 +281,12 @@ message SandboxTemplate { google.protobuf.Struct resources = 7; // Optional platform-specific volume claim templates. google.protobuf.Struct volume_claim_templates = 9; + // Enable Kubernetes user namespace isolation (hostUsers: false). + // When true, container UID 0 maps to a non-root host UID and capabilities + // become namespaced. Requires Kubernetes 1.33+ with user namespace support + // available (beta through 1.35, GA in 1.36+) and a supporting runtime. + // When unset, the cluster-wide default is used. + optional bool user_namespaces = 10; } // User-facing sandbox status derived by the gateway from compute-driver observations. @@ -309,6 +372,38 @@ message ListSandboxesRequest { string label_selector = 3; } +// List providers attached to a sandbox request. +message ListSandboxProvidersRequest { + // Sandbox name (canonical lookup key). + string sandbox_name = 1; +} + +// Attach provider to sandbox request. +message AttachSandboxProviderRequest { + // Sandbox name (canonical lookup key). + string sandbox_name = 1; + // Provider name to attach. + string provider_name = 2; + // Expected resource version for optimistic concurrency control. + // If 0, the server uses the current version (backward compatibility). + // If non-zero, the server validates that the sandbox's current resource_version + // matches this value before applying the mutation, returning ABORTED on mismatch. + uint64 expected_resource_version = 3; +} + +// Detach provider from sandbox request. +message DetachSandboxProviderRequest { + // Sandbox name (canonical lookup key). + string sandbox_name = 1; + // Provider name to detach. + string provider_name = 2; + // Expected resource version for optimistic concurrency control. + // If 0, the server uses the current version (backward compatibility). + // If non-zero, the server validates that the sandbox's current resource_version + // matches this value before applying the mutation, returning ABORTED on mismatch. + uint64 expected_resource_version = 3; +} + // Delete sandbox request. message DeleteSandboxRequest { // Sandbox name (canonical lookup key). @@ -325,6 +420,25 @@ message ListSandboxesResponse { repeated Sandbox sandboxes = 1; } +// List providers attached to a sandbox response. +message ListSandboxProvidersResponse { + repeated openshell.datamodel.v1.Provider providers = 1; +} + +// Attach provider to sandbox response. +message AttachSandboxProviderResponse { + Sandbox sandbox = 1; + // True when the provider was newly attached. False means it was already attached. + bool attached = 2; +} + +// Detach provider from sandbox response. +message DetachSandboxProviderResponse { + Sandbox sandbox = 1; + // True when the provider was removed. False means it was not attached. + bool detached = 2; +} + // Delete sandbox response. message DeleteSandboxResponse { bool deleted = 1; @@ -363,11 +477,6 @@ message CreateSshSessionResponse { // Gateway scheme. Must be exactly "http" or "https". string gateway_scheme = 5; - // HTTP path for the CONNECT/upgrade endpoint. Must begin with `/`. RFC - // 3986 path charset only ([A-Za-z0-9._~!$&'()*+,;=:@/-] plus %HH). - // Must not contain `?`, `#`, whitespace, backtick, or backslash. - string connect_path = 6; - // Optional host key fingerprint. If non-empty, [A-Za-z0-9:+/=-] only. string host_key_fingerprint = 7; @@ -375,6 +484,77 @@ message CreateSshSessionResponse { int64 expires_at_ms = 8; } +// Request to expose an HTTP service running inside a sandbox. +message ExposeServiceRequest { + // Sandbox name. + string sandbox = 1; + // Service name within the sandbox. + string service = 2; + // Loopback TCP port inside the sandbox. + uint32 target_port = 3; + // Whether to print/use the browser-facing service URL. + bool domain = 4; +} + +// Request to fetch an exposed sandbox service endpoint. +message GetServiceRequest { + // Sandbox name. + string sandbox = 1; + // Service name within the sandbox. Empty selects the unnamed endpoint. + string service = 2; +} + +// Request to list exposed sandbox service endpoints. +message ListServicesRequest { + // Optional sandbox name. Empty lists endpoints for all sandboxes. + string sandbox = 1; + // Page size. Zero uses the server default. + uint32 limit = 2; + // Page offset. + uint32 offset = 3; +} + +// Response containing exposed sandbox service endpoints. +message ListServicesResponse { + repeated ServiceEndpointResponse services = 1; +} + +// Request to delete an exposed sandbox service endpoint. +message DeleteServiceRequest { + // Sandbox name. + string sandbox = 1; + // Service name within the sandbox. Empty selects the unnamed endpoint. + string service = 2; +} + +// Response for deleting an exposed sandbox service endpoint. +message DeleteServiceResponse { + // True when an endpoint existed and was deleted. + bool deleted = 1; +} + +// Persisted sandbox service endpoint. +message ServiceEndpoint { + // Kubernetes-style metadata. + openshell.datamodel.v1.ObjectMeta metadata = 1; + // Sandbox object ID. + string sandbox_id = 2; + // Sandbox name. + string sandbox_name = 3; + // Service name within the sandbox. + string service_name = 4; + // Loopback TCP port inside the sandbox. + uint32 target_port = 5; + // Whether browser-facing service routing is enabled for this endpoint. + bool domain = 6; +} + +// Response containing a service endpoint and, when available, its local URL. +message ServiceEndpointResponse { + ServiceEndpoint endpoint = 1; + string url = 2; +} + // Revoke SSH session request. message RevokeSshSessionRequest { // Session token to revoke. @@ -409,6 +589,12 @@ message ExecSandboxRequest { // Request a pseudo-terminal for the remote command. bool tty = 7; + + // Initial terminal columns (used when tty=true, 0 = use default). + uint32 cols = 8; + + // Initial terminal rows (used when tty=true, 0 = use default). + uint32 rows = 9; } // One stdout chunk from a sandbox exec. @@ -435,6 +621,49 @@ message ExecSandboxEvent { } } +// Initial frame for one TCP forward stream. +message TcpForwardInit { + // Sandbox id. + string sandbox_id = 1; + // Optional service identifier for audit/correlation. + string service_id = 4; + // Target the gateway should request from the supervisor. + oneof target { + SshRelayTarget ssh = 5; + TcpRelayTarget tcp = 6; + } + // Optional target-specific authorization token. SSH targets use this as the + // short-lived SSH session token issued by CreateSshSession. + string authorization_token = 7; +} + +// A single frame on the CLI-to-gateway TCP forward stream. +message TcpForwardFrame { + oneof payload { + TcpForwardInit init = 1; + bytes data = 2; + } +} + +// Client-to-server message for interactive exec. +message ExecSandboxInput { + oneof payload { + // First message: exec request metadata. + ExecSandboxRequest start = 1; + // Subsequent messages: raw stdin bytes. + bytes stdin = 2; + // Terminal window size change. + ExecSandboxWindowResize resize = 3; + } +} + +// Terminal window resize event for interactive exec. +message ExecSandboxWindowResize { + uint32 cols = 1; + uint32 rows = 2; +} + + // SSH session record stored in persistence. message SshSession { // Kubernetes-style metadata (id, name, labels, timestamps, resource version). @@ -558,11 +787,121 @@ message ListProvidersResponse { repeated openshell.datamodel.v1.Provider providers = 1; } +// List provider type profiles request. +message ListProviderProfilesRequest { + uint32 limit = 1; + uint32 offset = 2; +} + +// Fetch provider type profile request. +message GetProviderProfileRequest { + string id = 1; +} + +// Provider profile payload with optional source metadata for diagnostics. +message ProviderProfileImportItem { + ProviderProfile profile = 1; + string source = 2; +} + +// Provider profile validation diagnostic. +message ProviderProfileDiagnostic { + string source = 1; + string profile_id = 2; + string field = 3; + string message = 4; + string severity = 5; +} + +// Provider credential declaration. +message ProviderProfileCredential { + string name = 1; + string description = 2; + repeated string env_vars = 3; + bool required = 4; + string auth_style = 5; + string header_name = 6; + string query_param = 7; +} + +// Stable provider profile categories used by clients for grouping and filtering. +enum ProviderProfileCategory { + PROVIDER_PROFILE_CATEGORY_UNSPECIFIED = 0; + PROVIDER_PROFILE_CATEGORY_OTHER = 1; + PROVIDER_PROFILE_CATEGORY_INFERENCE = 2; + PROVIDER_PROFILE_CATEGORY_AGENT = 3; + PROVIDER_PROFILE_CATEGORY_SOURCE_CONTROL = 4; + PROVIDER_PROFILE_CATEGORY_MESSAGING = 5; + PROVIDER_PROFILE_CATEGORY_DATA = 6; + PROVIDER_PROFILE_CATEGORY_KNOWLEDGE = 7; +} + +// Provider type profile metadata exposed to clients. +message ProviderProfile { + string id = 1; + string display_name = 2; + string description = 3; + ProviderProfileCategory category = 4; + repeated ProviderProfileCredential credentials = 5; + repeated openshell.sandbox.v1.NetworkEndpoint endpoints = 6; + repeated openshell.sandbox.v1.NetworkBinary binaries = 7; + bool inference_capable = 8; +} + +// Stored custom provider profile object. +message StoredProviderProfile { + openshell.datamodel.v1.ObjectMeta metadata = 1; + ProviderProfile profile = 2; +} + +// Provider profile response. +message ProviderProfileResponse { + ProviderProfile profile = 1; +} + +// List provider profiles response. +message ListProviderProfilesResponse { + repeated ProviderProfile profiles = 1; +} + +// Import custom provider profiles request. +message ImportProviderProfilesRequest { + repeated ProviderProfileImportItem profiles = 1; +} + +// Import custom provider profiles response. +message ImportProviderProfilesResponse { + repeated ProviderProfileDiagnostic diagnostics = 1; + repeated ProviderProfile profiles = 2; + bool imported = 3; +} + +// Lint provider profiles request. +message LintProviderProfilesRequest { + repeated ProviderProfileImportItem profiles = 1; +} + +// Lint provider profiles response. +message LintProviderProfilesResponse { + repeated ProviderProfileDiagnostic diagnostics = 1; + bool valid = 2; +} + // Delete provider response. message DeleteProviderResponse { bool deleted = 1; } +// Delete custom provider profile request. +message DeleteProviderProfileRequest { + string id = 1; +} + +// Delete custom provider profile response. +message DeleteProviderProfileResponse { + bool deleted = 1; +} + // Get sandbox provider environment request. message GetSandboxProviderEnvironmentRequest { // The sandbox ID. @@ -573,6 +912,8 @@ message GetSandboxProviderEnvironmentRequest { message GetSandboxProviderEnvironmentResponse { // Provider credential environment variables. map environment = 1; + // Fingerprint for the provider credential inputs that produced environment. + uint64 provider_env_revision = 2; } // --------------------------------------------------------------------------- @@ -604,6 +945,12 @@ message UpdateConfigRequest { bool global = 6; // Batched incremental policy merge operations. Sandbox-scoped only. repeated PolicyMergeOperation merge_operations = 7; + // Expected resource version for optimistic concurrency control (sandbox-scoped only). + // If 0, the server uses the current version (backward compatibility). + // If non-zero, the server validates that the sandbox's current resource_version + // matches this value before applying the mutation, returning ABORTED on mismatch. + // Ignored for global-scoped updates. + uint64 expected_resource_version = 8; } message PolicyMergeOperation { @@ -835,10 +1182,29 @@ message GatewayHeartbeat {} // On receiving this, the supervisor should initiate a RelayStream RPC to // the gateway, sending a RelayInit in the first RelayFrame to associate // the new HTTP/2 stream with the pending relay slot. The supervisor -// bridges that stream to the local SSH daemon. +// bridges that stream to the requested local target. message RelayOpen { // Gateway-allocated channel identifier (UUID). string channel_id = 1; + // Target the supervisor should dial inside the sandbox. + // If absent, supervisors treat the relay as SSH for compatibility. + oneof target { + SshRelayTarget ssh = 2; + TcpRelayTarget tcp = 3; + } + // Optional service identifier for audit/correlation. + string service_id = 5; +} + +// Built-in SSH relay target. +message SshRelayTarget {} + +// TCP target dialed by the supervisor from inside the sandbox. +message TcpRelayTarget { + // Phase 1 accepts loopback only: 127.0.0.1, ::1, or localhost. + string host = 1; + // Target port. Must fit in u16 and be non-zero. + uint32 port = 2; } // Initial RelayStream frame sent by the supervisor to claim a pending relay. @@ -976,6 +1342,15 @@ message PolicyChunk { int64 last_seen_ms = 15; // Binary path that triggered the denial (denormalized for display convenience). string binary = 16; + // Validation verdict from gateway-side static checks (prover output). + // Free-form summary string for human consumption in the inbox card. + // Empty until the prover has run for this chunk. + string validation_result = 17; + // Operator-supplied free-form text accompanying a rejection. Populated + // when the reviewer rejects via `RejectDraftChunkRequest.reason`; surfaced + // back to the in-sandbox agent so it can revise the proposal. + // Empty for non-rejected chunks. + string rejection_reason = 18; } // Notification that the draft policy was updated. @@ -996,7 +1371,13 @@ message SubmitPolicyAnalysisRequest { repeated DenialSummary summaries = 1; // Proposed policy chunks (validated by sandbox OPA engine). repeated PolicyChunk proposed_chunks = 2; - // Analysis mode: "mechanistic" or "llm". + // Analysis mode. `mechanistic` is the observation-driven path from the + // denial aggregator — chunks targeting the same host|port|binary fold + // into one row with hit_count incremented. `agent_authored` is an + // intentional proposal from an in-sandbox agent — each submission lands + // as its own chunk so the redraft-after-rejection loop has a stable id + // to watch. Other values are treated as agent-style (no dedup) so a new + // mode does not silently collapse proposals. string analysis_mode = 3; // Sandbox name. string name = 4; @@ -1009,6 +1390,10 @@ message SubmitPolicyAnalysisResponse { uint32 rejected_chunks = 2; // Reasons for each rejected chunk. repeated string rejection_reasons = 3; + // Server-assigned chunk IDs for the accepted chunks, in submission order. + // Agents use these to watch proposal state via policy.local's + // GET /v1/proposals/{id} and /wait endpoints. + repeated string accepted_chunk_ids = 4; } // Get draft policy for a sandbox. @@ -1171,6 +1556,12 @@ message DraftChunkPayload { string binary = 9; // Current draft version for the owning sandbox. int64 draft_version = 10; + // Gateway prover verdict for this chunk; empty until prover runs. + // Mirrors PolicyChunk.validation_result. + string validation_result = 11; + // Operator-supplied free-form rejection text; empty for non-rejected + // chunks. Mirrors PolicyChunk.rejection_reason. + string rejection_reason = 12; } // Internal stored policy revision row materialized from the generic objects table. @@ -1205,4 +1596,8 @@ message StoredDraftChunk { int32 hit_count = 15; int64 first_seen_ms = 16; int64 last_seen_ms = 17; + // Gateway prover verdict; empty until the prover runs. See PolicyChunk. + string validation_result = 18; + // Operator-supplied free-form rejection text. See PolicyChunk. + string rejection_reason = 19; } diff --git a/go/api/openshell/proto/sandbox.proto b/go/api/openshell/proto/sandbox.proto index 8a9abbce27..b40d95cb12 100644 --- a/go/api/openshell/proto/sandbox.proto +++ b/go/api/openshell/proto/sandbox.proto @@ -70,7 +70,7 @@ message NetworkEndpoint { // Single port (backwards compat). Use `ports` for multiple ports. // Mutually exclusive with `ports` — if both are set, `ports` takes precedence. uint32 port = 2; - // Application protocol for L7 inspection: "rest", "sql", or "" (L4-only). + // Application protocol for L7 inspection: "rest", "websocket", "graphql", "sql", or "" (L4-only). string protocol = 3; // TLS handling: "terminate" or "passthrough" (default). string tls = 4; @@ -102,6 +102,38 @@ message NetworkEndpoint { // upstreams like GitLab that embed %2F in namespaced resource paths. // Defaults to false (strict). bool allow_encoded_slash = 11; + // GraphQL persisted-query behavior for hash-only/saved-query requests: + // "deny" (default) or "allow_registered". + string persisted_queries = 12; + // Trusted GraphQL persisted-query registry keyed by hash or service-specific ID. + // Only used when persisted_queries is "allow_registered". + map graphql_persisted_queries = 13; + // Maximum GraphQL request body bytes to buffer for inspection. + // Defaults to 65536 when unset. + uint32 graphql_max_body_bytes = 14; + // Optional HTTP path glob that scopes this L7 endpoint on shared host:port APIs. + // Example: use path "/graphql" for protocol "graphql" and "/repos/**" for + // protocol "rest" when both surfaces live under api.example.com:443. + // Empty means all paths. + string path = 15; + // When true on a "rest" endpoint, OpenShell rewrites credential placeholders + // inside client-to-server WebSocket text messages after an allowed HTTP 101 + // upgrade. Defaults to false. + bool websocket_credential_rewrite = 16; + // When true on a "rest" endpoint, OpenShell rewrites credential placeholders + // inside supported textual HTTP request bodies before forwarding upstream. + // Defaults to false. + bool request_body_credential_rewrite = 17; +} + +// Trusted GraphQL operation classification. +message GraphqlOperation { + // Operation type: "query", "mutation", or "subscription". + string operation_type = 1; + // Operation name, if known. + string operation_name = 2; + // Root field names selected by the operation. + repeated string fields = 3; } // An L7 deny rule that blocks specific requests. @@ -117,6 +149,13 @@ message L7DenyRule { // Query parameter matcher map (REST). // Same semantics as L7Allow.query. map query = 4; + // GraphQL operation type: "query", "mutation", "subscription", or "*" for any. + string operation_type = 5; + // GraphQL operation name glob. "*" matches any operation name. + string operation_name = 6; + // GraphQL root field globs. Deny rules match when any selected root field + // matches any configured glob. + repeated string fields = 7; } // An L7 policy rule (allow-only). @@ -136,6 +175,13 @@ message L7Allow { // Key is the decoded query parameter name (case-sensitive). // Value supports either a single glob (`glob`) or a list (`any`). map query = 4; + // GraphQL operation type: "query", "mutation", "subscription", or "*" for any. + string operation_type = 5; + // GraphQL operation name glob. "*" matches any operation name. + string operation_name = 6; + // GraphQL root field globs. Allow rules match only when every selected root + // field matches one of the configured globs. Omit to match all fields. + repeated string fields = 7; } // Query value matcher for one query parameter key. @@ -219,4 +265,7 @@ message GetSandboxConfigResponse { // When policy_source is GLOBAL, the version of the global policy revision. // Zero when no global policy is active or when policy_source is SANDBOX. uint32 global_policy_version = 7; + // Fingerprint for provider credential inputs attached to this sandbox. + // Changes when attached provider names or attached provider records change. + uint64 provider_env_revision = 8; } From 66ec2da7c8f8e13e6d719bd8af31630eec3a4936 Mon Sep 17 00:00:00 2001 From: Peter Jausovec Date: Tue, 19 May 2026 16:00:59 -0700 Subject: [PATCH 2/8] add Hermes as a backend, update SSH functionality to new protos, add UI for hermes Signed-off-by: Peter Jausovec --- .../crd/bases/kagent.dev_agentharnesses.yaml | 64 ++- go/api/v1alpha2/agentharness_types.go | 40 +- go/api/v1alpha2/zz_generated.deepcopy.go | 10 + .../internal/httpserver/handlers/agents.go | 4 +- .../httpserver/handlers/agents_test.go | 38 ++ .../httpserver/handlers/sandbox_ssh.go | 366 ++++++++++-------- .../handlers/sandbox_ssh_forward_test.go | 105 +++++ .../httpserver/handlers/sandbox_ssh_test.go | 82 ---- go/core/pkg/app/app.go | 2 + .../openshell/agent_harness_ensure.go | 38 ++ .../openshell/channels/credentials.go | 101 +++++ .../openshell/channels/envkeys.go | 11 + .../openshell/channels/placeholders.go | 16 + .../openshell/channels/providers.go | 57 +++ .../openshell/channels/providers_test.go | 24 ++ .../openshell/channels/resolve.go | 140 +++++++ .../pkg/sandboxbackend/openshell/hermes.go | 186 +++++++++ .../openshell/hermes/bootstrap.go | 174 +++++++++ .../openshell/hermes/bootstrap_test.go | 98 +++++ .../openshell/hermes/channels.go | 65 ++++ .../openshell/hermes/constants.go | 21 + .../hermes/messaging_providers_test.go | 49 +++ .../sandboxbackend/openshell/hermes/policy.go | 306 +++++++++++++++ .../openshell/hermes/policy_test.go | 86 ++++ .../sandboxbackend/openshell/hermes/ssh.go | 8 + .../openshell/hermes/ssh_test.go | 12 + .../openshell/messaging_providers.go | 54 +++ .../pkg/sandboxbackend/openshell/openclaw.go | 20 +- .../openshell/openclaw/bootstrap_test.go | 6 +- .../openshell/openclaw/channels.go | 242 +++--------- .../openshell/openclaw/constants.go | 3 + .../openshell/openclaw/credentials.go | 58 --- .../openshell/openclaw/defaults.go | 25 ++ .../openshell/openclaw/policy.go | 31 +- .../openshell/openclaw/resolve.go | 19 - .../openshell/openclaw/ssh_test.go | 12 + .../pkg/sandboxbackend/openshell/openshell.go | 13 +- .../openshell/openshell_test.go | 105 ++++- .../pkg/sandboxbackend/openshell/policy.go | 21 +- .../pkg/sandboxbackend/openshell/providers.go | 88 +++++ .../sandboxbackend/openshell/ssh_terminal.go | 33 ++ .../openshell/ssh_terminal_test.go | 63 +++ .../pkg/sandboxbackend/openshell/translate.go | 34 +- .../openshell/translate_test.go | 69 +++- .../templates/kagent.dev_agentharnesses.yaml | 64 ++- ui/src/app/actions/agents.ts | 1 + ui/src/app/agents/new-harness/page.tsx | 47 ++- .../app/openshell/OpenshellTerminalPage.tsx | 32 +- ui/src/components/AgentCard.tsx | 11 +- ui/src/components/AgentListView.tsx | 11 +- ui/src/components/AgentsProvider.tsx | 6 +- .../agent-form/OpenClawSandboxFields.tsx | 147 +++++-- .../lib/__tests__/openClawSandboxForm.test.ts | 28 ++ ui/src/lib/agentHarness.ts | 37 +- ui/src/lib/openClawSandboxForm.ts | 54 ++- ui/src/lib/openshellSandboxAgents.ts | 12 +- 56 files changed, 2772 insertions(+), 677 deletions(-) create mode 100644 go/core/internal/httpserver/handlers/sandbox_ssh_forward_test.go create mode 100644 go/core/pkg/sandboxbackend/openshell/agent_harness_ensure.go create mode 100644 go/core/pkg/sandboxbackend/openshell/channels/credentials.go create mode 100644 go/core/pkg/sandboxbackend/openshell/channels/envkeys.go create mode 100644 go/core/pkg/sandboxbackend/openshell/channels/placeholders.go create mode 100644 go/core/pkg/sandboxbackend/openshell/channels/providers.go create mode 100644 go/core/pkg/sandboxbackend/openshell/channels/providers_test.go create mode 100644 go/core/pkg/sandboxbackend/openshell/channels/resolve.go create mode 100644 go/core/pkg/sandboxbackend/openshell/hermes.go create mode 100644 go/core/pkg/sandboxbackend/openshell/hermes/bootstrap.go create mode 100644 go/core/pkg/sandboxbackend/openshell/hermes/bootstrap_test.go create mode 100644 go/core/pkg/sandboxbackend/openshell/hermes/channels.go create mode 100644 go/core/pkg/sandboxbackend/openshell/hermes/constants.go create mode 100644 go/core/pkg/sandboxbackend/openshell/hermes/messaging_providers_test.go create mode 100644 go/core/pkg/sandboxbackend/openshell/hermes/policy.go create mode 100644 go/core/pkg/sandboxbackend/openshell/hermes/policy_test.go create mode 100644 go/core/pkg/sandboxbackend/openshell/hermes/ssh.go create mode 100644 go/core/pkg/sandboxbackend/openshell/hermes/ssh_test.go create mode 100644 go/core/pkg/sandboxbackend/openshell/messaging_providers.go delete mode 100644 go/core/pkg/sandboxbackend/openshell/openclaw/credentials.go create mode 100644 go/core/pkg/sandboxbackend/openshell/openclaw/defaults.go delete mode 100644 go/core/pkg/sandboxbackend/openshell/openclaw/resolve.go create mode 100644 go/core/pkg/sandboxbackend/openshell/openclaw/ssh_test.go create mode 100644 go/core/pkg/sandboxbackend/openshell/providers.go create mode 100644 go/core/pkg/sandboxbackend/openshell/ssh_terminal.go create mode 100644 go/core/pkg/sandboxbackend/openshell/ssh_terminal_test.go diff --git a/go/api/config/crd/bases/kagent.dev_agentharnesses.yaml b/go/api/config/crd/bases/kagent.dev_agentharnesses.yaml index c7748893de..8c0cf3d07a 100644 --- a/go/api/config/crd/bases/kagent.dev_agentharnesses.yaml +++ b/go/api/config/crd/bases/kagent.dev_agentharnesses.yaml @@ -69,6 +69,7 @@ spec: enum: - openclaw - nemoclaw + - hermes type: string channels: description: Channels configures Telegram and Slack integrations for @@ -83,10 +84,44 @@ spec: minLength: 1 type: string slack: - description: AgentHarnessSlackChannelSpec configures Slack when - AgentHarnessChannel.type is Slack. + description: |- + AgentHarnessSlackChannelSpec configures Slack when AgentHarnessChannel.type is Slack. + + OpenClaw and NemoClaw use channelAccess, allowlistChannels, and interactiveReplies. + Hermes uses allowedUserIDs (SLACK_ALLOWED_USERS), homeChannel (SLACK_HOME_CHANNEL), + and homeChannelName (SLACK_HOME_CHANNEL_NAME) in the sandbox .env. properties: + allowedUserIDs: + description: 'AllowedUserIDs restricts which Slack user + IDs may interact with the bot (Hermes: SLACK_ALLOWED_USERS).' + items: + type: string + type: array + allowedUserIDsFrom: + description: ValueSource defines a source for configuration + values from a Secret or ConfigMap + properties: + key: + description: The key of the ConfigMap or Secret. + maxLength: 253 + type: string + name: + description: The name of the ConfigMap or Secret. + maxLength: 253 + type: string + type: + enum: + - ConfigMap + - Secret + type: string + required: + - key + - name + - type + type: object allowlistChannels: + description: AllowlistChannels is required when channelAccess + is allowlist (OpenClaw / NemoClaw only). items: type: string type: array @@ -159,26 +194,37 @@ spec: rule: (has(self.value) && !has(self.valueFrom)) || (!has(self.value) && has(self.valueFrom)) channelAccess: - description: AgentHarnessChannelAccess controls whether - the bot listens broadly or only on an allowlist. + description: ChannelAccess controls OpenClaw routing (open, + allowlist, disabled). Omit for Hermes harnesses. enum: - allowlist - open - disabled type: string + homeChannel: + description: HomeChannel is the default Slack channel ID + for Hermes cron/scheduled messages (SLACK_HOME_CHANNEL). + type: string + homeChannelName: + description: HomeChannelName is a human-readable label for + HomeChannel (SLACK_HOME_CHANNEL_NAME). + type: string interactiveReplies: default: true type: boolean required: - appToken - botToken - - channelAccess type: object x-kubernetes-validations: - message: allowlistChannels is required when channelAccess is allowlist - rule: self.channelAccess != 'allowlist' || (has(self.allowlistChannels) - && size(self.allowlistChannels) > 0) + rule: '!has(self.channelAccess) || self.channelAccess != ''allowlist'' + || (has(self.allowlistChannels) && size(self.allowlistChannels) + > 0)' + - message: allowedUserIDs and allowedUserIDsFrom are mutually + exclusive + rule: '!(size(self.allowedUserIDs) > 0 && has(self.allowedUserIDsFrom))' telegram: description: AgentHarnessTelegramChannelSpec configures Telegram when AgentHarnessChannel.type is Telegram. @@ -435,7 +481,8 @@ spec: description: |- Image is the container image to run in the harness VM, if the backend supports per-resource images. Backends openclaw and nemoclaw pin the image - to the NemoClaw sandbox base when this field is empty. + to the NemoClaw sandbox base when this field is empty; backend hermes pins + to the Hermes sandbox base image when empty. type: string modelConfigRef: description: |- @@ -473,6 +520,7 @@ spec: enum: - openclaw - nemoclaw + - hermes type: string id: type: string diff --git a/go/api/v1alpha2/agentharness_types.go b/go/api/v1alpha2/agentharness_types.go index 4117fd1cb5..a8f1b990f8 100644 --- a/go/api/v1alpha2/agentharness_types.go +++ b/go/api/v1alpha2/agentharness_types.go @@ -18,14 +18,25 @@ import ( // AgentHarnessBackendType selects which sandbox control plane provisions the // environment. Additional backends may be added in the future. -// +kubebuilder:validation:Enum=openclaw;nemoclaw +// +kubebuilder:validation:Enum=openclaw;nemoclaw;hermes type AgentHarnessBackendType string const ( AgentHarnessBackendOpenClaw AgentHarnessBackendType = "openclaw" AgentHarnessBackendNemoClaw AgentHarnessBackendType = "nemoclaw" + AgentHarnessBackendHermes AgentHarnessBackendType = "hermes" ) +// IsKnownAgentHarnessBackend reports backends the OpenShell harness controller and API expose. +func IsKnownAgentHarnessBackend(b AgentHarnessBackendType) bool { + switch b { + case AgentHarnessBackendOpenClaw, AgentHarnessBackendNemoClaw, AgentHarnessBackendHermes: + return true + default: + return false + } +} + // AgentHarnessChannelType selects a messenger integration for OpenClaw harness VMs. // +kubebuilder:validation:Enum=telegram;slack type AgentHarnessChannelType string @@ -70,19 +81,37 @@ type AgentHarnessTelegramChannelSpec struct { // AgentHarnessSlackChannelSpec configures Slack when AgentHarnessChannel.type is Slack. // -// +kubebuilder:validation:XValidation:rule="self.channelAccess != 'allowlist' || (has(self.allowlistChannels) && size(self.allowlistChannels) > 0)",message="allowlistChannels is required when channelAccess is allowlist" +// OpenClaw and NemoClaw use channelAccess, allowlistChannels, and interactiveReplies. +// Hermes uses allowedUserIDs (SLACK_ALLOWED_USERS), homeChannel (SLACK_HOME_CHANNEL), +// and homeChannelName (SLACK_HOME_CHANNEL_NAME) in the sandbox .env. +// +// +kubebuilder:validation:XValidation:rule="!has(self.channelAccess) || self.channelAccess != 'allowlist' || (has(self.allowlistChannels) && size(self.allowlistChannels) > 0)",message="allowlistChannels is required when channelAccess is allowlist" +// +kubebuilder:validation:XValidation:rule="!(size(self.allowedUserIDs) > 0 && has(self.allowedUserIDsFrom))",message="allowedUserIDs and allowedUserIDsFrom are mutually exclusive" type AgentHarnessSlackChannelSpec struct { // +required BotToken AgentHarnessChannelCredential `json:"botToken"` // +required AppToken AgentHarnessChannelCredential `json:"appToken"` - // +required - ChannelAccess AgentHarnessChannelAccess `json:"channelAccess"` + // ChannelAccess controls OpenClaw routing (open, allowlist, disabled). Omit for Hermes harnesses. + // +optional + ChannelAccess AgentHarnessChannelAccess `json:"channelAccess,omitempty"` + // AllowlistChannels is required when channelAccess is allowlist (OpenClaw / NemoClaw only). // +optional AllowlistChannels []string `json:"allowlistChannels,omitempty"` // +optional // +kubebuilder:default=true InteractiveReplies *bool `json:"interactiveReplies,omitempty"` + // AllowedUserIDs restricts which Slack user IDs may interact with the bot (Hermes: SLACK_ALLOWED_USERS). + // +optional + AllowedUserIDs []string `json:"allowedUserIDs,omitempty"` + // +optional + AllowedUserIDsFrom *ValueSource `json:"allowedUserIDsFrom,omitempty"` + // HomeChannel is the default Slack channel ID for Hermes cron/scheduled messages (SLACK_HOME_CHANNEL). + // +optional + HomeChannel string `json:"homeChannel,omitempty"` + // HomeChannelName is a human-readable label for HomeChannel (SLACK_HOME_CHANNEL_NAME). + // +optional + HomeChannelName string `json:"homeChannelName,omitempty"` } // AgentHarnessChannel declares one messenger binding inside an OpenClaw/NemoClaw harness VM. @@ -118,7 +147,8 @@ type AgentHarnessSpec struct { // Image is the container image to run in the harness VM, if the backend // supports per-resource images. Backends openclaw and nemoclaw pin the image - // to the NemoClaw sandbox base when this field is empty. + // to the NemoClaw sandbox base when this field is empty; backend hermes pins + // to the Hermes sandbox base image when empty. // +optional Image string `json:"image,omitempty"` diff --git a/go/api/v1alpha2/zz_generated.deepcopy.go b/go/api/v1alpha2/zz_generated.deepcopy.go index 136058bced..cb1d70a875 100644 --- a/go/api/v1alpha2/zz_generated.deepcopy.go +++ b/go/api/v1alpha2/zz_generated.deepcopy.go @@ -230,6 +230,16 @@ func (in *AgentHarnessSlackChannelSpec) DeepCopyInto(out *AgentHarnessSlackChann *out = new(bool) **out = **in } + if in.AllowedUserIDs != nil { + in, out := &in.AllowedUserIDs, &out.AllowedUserIDs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AllowedUserIDsFrom != nil { + in, out := &in.AllowedUserIDsFrom, &out.AllowedUserIDsFrom + *out = new(ValueSource) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentHarnessSlackChannelSpec. diff --git a/go/core/internal/httpserver/handlers/agents.go b/go/core/internal/httpserver/handlers/agents.go index 3233204ba4..59c68ce27f 100644 --- a/go/core/internal/httpserver/handlers/agents.go +++ b/go/core/internal/httpserver/handlers/agents.go @@ -121,7 +121,7 @@ func (h *AgentsHandler) listAgentResponses(ctx context.Context, log logr.Logger, h.appendAgentResponses(ctx, log, agentObjects(agentList.Items), &result) for i := range harnessList.Items { sb := &harnessList.Items[i] - if sb.Spec.Backend != v1alpha2.AgentHarnessBackendOpenClaw && sb.Spec.Backend != v1alpha2.AgentHarnessBackendNemoClaw { + if !v1alpha2.IsKnownAgentHarnessBackend(sb.Spec.Backend) { continue } result = append(result, h.openshellAgentHarnessAgentResponse(ctx, log, sb)) @@ -684,7 +684,7 @@ func (h *AgentsHandler) HandleDeleteAgent(w ErrorResponseWriter, r *http.Request return } b := sb.Spec.Backend - if b != v1alpha2.AgentHarnessBackendOpenClaw && b != v1alpha2.AgentHarnessBackendNemoClaw { + if !v1alpha2.IsKnownAgentHarnessBackend(b) { w.RespondWithError(errors.NewNotFoundError("Agent not found", nil)) return } diff --git a/go/core/internal/httpserver/handlers/agents_test.go b/go/core/internal/httpserver/handlers/agents_test.go index 23a08aae90..efe2bf352c 100644 --- a/go/core/internal/httpserver/handlers/agents_test.go +++ b/go/core/internal/httpserver/handlers/agents_test.go @@ -948,4 +948,42 @@ func TestHandleCreateAgentHarness(t *testing.T) { require.NoError(t, handler.KubeClient.Get(context.Background(), types.NamespacedName{Namespace: "default", Name: "my-openclaw"}, &created)) require.Equal(t, v1alpha2.AgentHarnessBackendOpenClaw, created.Spec.Backend) }) + + t.Run("creates hermes AgentHarness", func(t *testing.T) { + modelConfig := createTestModelConfig() + handler, _ := setupTestHandler(t, modelConfig) + + body := map[string]any{ + "apiVersion": "kagent.dev/v1alpha2", + "kind": "AgentHarness", + "metadata": map[string]string{ + "name": "my-hermes", + "namespace": "default", + }, + "spec": map[string]any{ + "backend": "hermes", + "description": "hermes vm", + "modelConfigRef": "test-model-config", + }, + } + raw, err := json.Marshal(body) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/api/agentharnesses", bytes.NewReader(raw)) + req.Header.Set("Content-Type", "application/json") + req = setUser(req, "test-user") + w := httptest.NewRecorder() + + handler.HandleCreateAgentHarness(&testErrorResponseWriter{w}, req) + + require.Equal(t, http.StatusCreated, w.Code, w.Body.String()) + + var response api.StandardResponse[api.AgentResponse] + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &response)) + require.Equal(t, v1alpha2.AgentHarnessBackendHermes, response.Data.OpenshellAgentHarness.Backend) + + var created v1alpha2.AgentHarness + require.NoError(t, handler.KubeClient.Get(context.Background(), types.NamespacedName{Namespace: "default", Name: "my-hermes"}, &created)) + require.Equal(t, v1alpha2.AgentHarnessBackendHermes, created.Spec.Backend) + }) } diff --git a/go/core/internal/httpserver/handlers/sandbox_ssh.go b/go/core/internal/httpserver/handlers/sandbox_ssh.go index 397b71ede1..e0e0ddc2ca 100644 --- a/go/core/internal/httpserver/handlers/sandbox_ssh.go +++ b/go/core/internal/httpserver/handlers/sandbox_ssh.go @@ -1,9 +1,7 @@ package handlers import ( - "bufio" "context" - "crypto/tls" "encoding/json" "errors" "fmt" @@ -11,13 +9,13 @@ import ( "net" "net/http" "os" - "strconv" "strings" "sync" "time" "github.com/gorilla/websocket" openshellv1 "github.com/kagent-dev/kagent/go/api/openshell/gen/openshellv1" + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell" "golang.org/x/crypto/ssh" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -28,29 +26,28 @@ const ( // Default when OPENSHELL_GRPC_ADDR is unset and the WebSocket start frame omits grpc_address. // Short name "openshell:8080" only resolves if a Service "openshell" exists in the controller's // namespace; OpenShell is often installed in its own namespace (e.g. openshell). - defaultOpenshellGRPCAddr = "openshell.openshell.svc.cluster.local:8080" - openshellGRPCEnv = "OPENSHELL_GRPC_ADDR" - defaultSandboxSSHLaunchCmd = "openclaw tui" - - sandboxSSHWSReadBufSize = 4096 - sandboxSSHHandshakeTimeout = 90 * time.Second - sandboxSSHWSWriteDeadline = 15 * time.Second - sandboxSSHDefaultCols = 120 - sandboxSSHDefaultRows = 36 - sandboxSSHCopyBufSize = 32 * 1024 - sandboxSSHGatewayDialTimeout = 30 * time.Second - sandboxSSHClientConnTimeout = 60 * time.Second - sandboxSSHUser = "sandbox" - sandboxSSHPTYTerm = "xterm-256color" + defaultOpenshellGRPCAddr = "openshell.openshell.svc.cluster.local:8080" + openshellGRPCEnv = "OPENSHELL_GRPC_ADDR" + + sandboxSSHWSReadBufSize = 4096 + sandboxSSHHandshakeTimeout = 90 * time.Second + sandboxSSHWSWriteDeadline = 15 * time.Second + sandboxSSHDefaultCols = 120 + sandboxSSHDefaultRows = 36 + sandboxSSHCopyBufSize = 32 * 1024 + sandboxSSHClientConnTimeout = 60 * time.Second + sandboxSSHUser = "sandbox" + sandboxSSHPTYTerm = "xterm-256color" ) type sshStartMsg struct { - SandboxName string `json:"sandbox_name"` - GRPCAddress string `json:"grpc_address,omitempty"` - Cols int `json:"cols,omitempty"` - Rows int `json:"rows,omitempty"` - PlainShell bool `json:"plain_shell,omitempty"` - LaunchCommand string `json:"launch_command,omitempty"` + SandboxName string `json:"sandbox_name"` + GRPCAddress string `json:"grpc_address,omitempty"` + Cols int `json:"cols,omitempty"` + Rows int `json:"rows,omitempty"` + PlainShell bool `json:"plain_shell,omitempty"` + LaunchCommand string `json:"launch_command,omitempty"` + HarnessBackend string `json:"harness_backend,omitempty"` } type resizeMsg struct { @@ -65,7 +62,7 @@ type wsCtrlMsg struct { } // HandleSandboxSSHWebSocket upgrades to WebSocket, accepts one JSON start frame, mints an SSH -// session via OpenShell gRPC from inside the cluster, opens an HTTP CONNECT tunnel and SSH shell, +// session via OpenShell gRPC from inside the cluster, opens a ForwardTcp relay and SSH shell, // then proxies terminal I/O (same wire protocol as scripts/openshell-ssh-ws.mjs). func (h *Handlers) HandleSandboxSSHWebSocket(w ErrorResponseWriter, r *http.Request) { log := ctrllog.FromContext(r.Context()).WithName("sandbox-ssh-ws") @@ -96,16 +93,18 @@ func (h *Handlers) HandleSandboxSSHWebSocket(w ErrorResponseWriter, r *http.Requ log.Info("openshell gRPC target", "addr", grpcAddr) - ctx, cancel := context.WithTimeout(r.Context(), sandboxSSHHandshakeTimeout) - defer cancel() + handshakeCtx, handshakeCancel := context.WithTimeout(r.Context(), sandboxSSHHandshakeTimeout) + defer handshakeCancel() - sshClient, session, stdin, stdout, stderr, err := h.dialOpenshellShellSession( - ctx, grpcAddr, start.SandboxName, start.Rows, start.Cols, start.PlainShell, start.LaunchCommand) + // ForwardTcp must outlive the handshake; do not tie the relay stream to handshakeCtx. + grpcConn, sshClient, session, stdin, stdout, stderr, err := h.dialOpenshellShellSession( + handshakeCtx, r.Context(), grpcAddr, start.SandboxName, start.Rows, start.Cols, start.PlainShell, start.LaunchCommand, start.HarnessBackend) if err != nil { log.Info("openshell ssh session failed", "error", err) closeWSWithError(wsConn, err.Error()) return } + defer grpcConn.Close() defer func() { _ = session.Close() _ = sshClient.Close() @@ -128,19 +127,17 @@ func (h *Handlers) HandleSandboxSSHWebSocket(w ErrorResponseWriter, r *http.Requ runSandboxSSHWSReader(wsConn, session, stdin) }() - streamDone := make(chan struct{}) var streamWG sync.WaitGroup streamWG.Add(2) go copySSHStreamToWebSocket(stdout, writeWS, &streamWG) go copySSHStreamToWebSocket(stderr, writeWS, &streamWG) go func() { streamWG.Wait() - close(streamDone) + log.Info("ssh stdout/stderr copy finished") }() select { case <-copyDone: - case <-streamDone: case <-r.Context().Done(): } _ = wsConn.Close() @@ -250,24 +247,15 @@ func copySSHStreamToWebSocket(r io.Reader, writeWS func(messageType int, p []byt } } -func resolveSandboxSSHRemoteCommand(plainShell bool, launchCommandFromClient string) (plain bool, execCmd string) { - if plainShell { - return true, "" - } - cmd := strings.TrimSpace(launchCommandFromClient) - if cmd == "" { - cmd = defaultSandboxSSHLaunchCmd - } - return false, cmd -} - func (h *Handlers) dialOpenshellShellSession( - ctx context.Context, + handshakeCtx, streamCtx context.Context, grpcAddr, sandboxName string, rows, cols int, plainShell bool, launchCommandFromClient string, + harnessBackend string, ) ( + grpcConn *grpc.ClientConn, sshClient *ssh.Client, session *ssh.Session, stdin io.WriteCloser, @@ -275,24 +263,35 @@ func (h *Handlers) dialOpenshellShellSession( stderr io.Reader, err error, ) { - grpcConn, err := grpc.NewClient(grpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) + grpcConn, err = grpc.NewClient(grpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { - return nil, nil, nil, nil, nil, fmt.Errorf("grpc dial %q: %w", grpcAddr, err) + return nil, nil, nil, nil, nil, nil, fmt.Errorf("grpc dial %q: %w", grpcAddr, err) } - defer grpcConn.Close() cli := openshellv1.NewOpenShellClient(grpcConn) - sandboxID, sshRes, err := openshellCreateSSHSession(ctx, cli, sandboxName) + sandboxID, sshRes, err := openshellCreateSSHSession(handshakeCtx, cli, sandboxName) if err != nil { - return nil, nil, nil, nil, nil, err + _ = grpcConn.Close() + return nil, nil, nil, nil, nil, nil, err } - tunnelConn, dialHost, err := openshellDialHTTPConnectTunnel(ctx, grpcAddr, sshRes, sandboxID) + if sid := strings.TrimSpace(sshRes.GetSandboxId()); sid != "" { + sandboxID = sid + } + tunnelConn, err := openshellDialForwardTcp(streamCtx, cli, sandboxID, sshRes.GetToken()) if err != nil { - return nil, nil, nil, nil, nil, err + _ = grpcConn.Close() + return nil, nil, nil, nil, nil, nil, err } - return openSSHSessionOverTunnel(tunnelConn, dialHost, rows, cols, plainShell, launchCommandFromClient) + sshClient, session, stdin, stdout, stderr, err = openSSHSessionOverTunnel( + tunnelConn, "openshell", rows, cols, plainShell, launchCommandFromClient, harnessBackend, + ) + if err != nil { + _ = grpcConn.Close() + return nil, nil, nil, nil, nil, nil, err + } + return grpcConn, sshClient, session, stdin, stdout, stderr, nil } func sandboxIDForSSH(sb *openshellv1.Sandbox) string { @@ -325,62 +324,180 @@ func openshellCreateSSHSession( return "", nil, fmt.Errorf("CreateSshSession: %w", err) } - token := sshRes.GetToken() - gwHost := sshRes.GetGatewayHost() - gwPort := sshRes.GetGatewayPort() - scheme := strings.ToLower(strings.TrimSpace(sshRes.GetGatewayScheme())) - connectPath := sshRes.GetConnectPath() - if token == "" || gwHost == "" || gwPort == 0 || scheme == "" || connectPath == "" { - return "", nil, errors.New("CreateSshSession returned incomplete tunnel fields") + token := strings.TrimSpace(sshRes.GetToken()) + if token == "" { + return "", nil, errors.New("CreateSshSession returned empty session token") } return sandboxID, sshRes, nil } -func openshellDialHTTPConnectTunnel( +// openshellDialForwardTcp opens the OpenShell ForwardTcp bidi stream used by the CLI ssh-proxy. +func openshellDialForwardTcp( ctx context.Context, - grpcAddr string, - sshRes *openshellv1.CreateSshSessionResponse, - sandboxID string, -) (tunnelConn net.Conn, dialHost string, err error) { - token := sshRes.GetToken() - gwHost := sshRes.GetGatewayHost() - gwPort := sshRes.GetGatewayPort() - scheme := strings.ToLower(strings.TrimSpace(sshRes.GetGatewayScheme())) - connectPath := sshRes.GetConnectPath() - sid := sshRes.GetSandboxId() - if sid == "" { - sid = sandboxID - } - - dialHost, err = resolveGatewayDialHost(gwHost, grpcAddr) + cli openshellv1.OpenShellClient, + sandboxID, token string, +) (net.Conn, error) { + sandboxID = strings.TrimSpace(sandboxID) + token = strings.TrimSpace(token) + if sandboxID == "" || token == "" { + return nil, errors.New("sandbox id and session token are required for SSH forward") + } + + stream, err := cli.ForwardTcp(ctx) if err != nil { - return nil, "", err + return nil, fmt.Errorf("ForwardTcp: %w", err) } - if dialHost != gwHost { - log := ctrllog.FromContext(ctx) - log.Info("using cluster-reachable host for OpenShell gateway (CreateSshSession returned loopback)", - "gateway_host", gwHost, "dial_host", dialHost) + + if err := stream.Send(buildForwardTcpSSHInit(sandboxID, token)); err != nil { + _ = stream.CloseSend() + return nil, fmt.Errorf("ForwardTcp init: %w", err) } + return newTCPForwardConn(stream), nil +} - rawConn, err := dialGateway(scheme, dialHost, int(gwPort)) - if err != nil { - return nil, "", err +func buildForwardTcpSSHInit(sandboxID, token string) *openshellv1.TcpForwardFrame { + return &openshellv1.TcpForwardFrame{ + Payload: &openshellv1.TcpForwardFrame_Init{ + Init: &openshellv1.TcpForwardInit{ + SandboxId: sandboxID, + ServiceId: fmt.Sprintf("ssh-proxy:%s", sandboxID), + AuthorizationToken: token, + Target: &openshellv1.TcpForwardInit_Ssh{ + Ssh: &openshellv1.SshRelayTarget{}, + }, + }, + }, } +} - tunnelConn, err = completeHTTPConnect(ctx, rawConn, dialHost, connectPath, sid, token) - if err != nil { - _ = rawConn.Close() - return nil, "", err +// tcpForwardConn adapts OpenShell ForwardTcp to net.Conn for golang.org/x/crypto/ssh. +// A background reader pumps gateway data into rbuf so ssh handshakes can read and write +// concurrently (same pattern as openshell-cli ssh-proxy's split stdin/stdout tasks). +type tcpForwardConn struct { + stream openshellv1.OpenShell_ForwardTcpClient + + readMu sync.Mutex + readCond *sync.Cond + rbuf []byte + recvErr error + closed bool + + sendMu sync.Mutex +} + +func newTCPForwardConn(stream openshellv1.OpenShell_ForwardTcpClient) *tcpForwardConn { + c := &tcpForwardConn{stream: stream} + c.readCond = sync.NewCond(&c.readMu) + go c.pumpRecv() + return c +} + +func (c *tcpForwardConn) pumpRecv() { + for { + frame, err := c.stream.Recv() + c.readMu.Lock() + if c.closed { + c.readMu.Unlock() + return + } + if err != nil { + if c.recvErr == nil { + c.recvErr = err + } + c.readCond.Broadcast() + c.readMu.Unlock() + return + } + if frame != nil { + if data := frame.GetData(); len(data) > 0 { + c.rbuf = append(c.rbuf, data...) + c.readCond.Broadcast() + } + } + c.readMu.Unlock() + } +} + +func (c *tcpForwardConn) Read(p []byte) (int, error) { + c.readMu.Lock() + defer c.readMu.Unlock() + for len(c.rbuf) == 0 && c.recvErr == nil && !c.closed { + c.readCond.Wait() + } + if c.closed && len(c.rbuf) == 0 { + return 0, io.EOF + } + if len(c.rbuf) == 0 { + if c.recvErr != nil { + return 0, c.recvErr + } + return 0, io.EOF } - return tunnelConn, dialHost, nil + n := copy(p, c.rbuf) + c.rbuf = c.rbuf[n:] + return n, nil } +func (c *tcpForwardConn) Write(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + c.sendMu.Lock() + defer c.sendMu.Unlock() + c.readMu.Lock() + closed := c.closed + c.readMu.Unlock() + if closed { + return 0, io.ErrClosedPipe + } + if err := c.stream.Send(&openshellv1.TcpForwardFrame{ + Payload: &openshellv1.TcpForwardFrame_Data{Data: append([]byte(nil), p...)}, + }); err != nil { + return 0, err + } + return len(p), nil +} + +func (c *tcpForwardConn) Close() error { + c.readMu.Lock() + if c.closed { + c.readMu.Unlock() + return nil + } + c.closed = true + c.readMu.Unlock() + c.sendMu.Lock() + defer c.sendMu.Unlock() + return c.stream.CloseSend() +} + +func (c *tcpForwardConn) LocalAddr() net.Addr { return &tcpForwardAddr{} } +func (c *tcpForwardConn) RemoteAddr() net.Addr { return &tcpForwardAddr{} } + +func (c *tcpForwardConn) SetDeadline(t time.Time) error { + return nil +} + +func (c *tcpForwardConn) SetReadDeadline(t time.Time) error { + return nil +} + +func (c *tcpForwardConn) SetWriteDeadline(t time.Time) error { + return nil +} + +type tcpForwardAddr struct{} + +func (tcpForwardAddr) Network() string { return "tcp" } +func (tcpForwardAddr) String() string { return "openshell-forward-tcp" } + func openSSHSessionOverTunnel( tunnelConn net.Conn, dialHost string, rows, cols int, plainShell bool, launchCommandFromClient string, + harnessBackend string, ) ( sshClient *ssh.Client, session *ssh.Session, @@ -434,7 +551,7 @@ func openSSHSessionOverTunnel( _ = sshClient.Close() return nil, nil, nil, nil, nil, fmt.Errorf("ssh RequestPty: %w", err) } - useShell, remoteCmd := resolveSandboxSSHRemoteCommand(plainShell, launchCommandFromClient) + useShell, remoteCmd := openshell.ResolveSSHRemoteCommand(plainShell, launchCommandFromClient, harnessBackend) if useShell { if err := session.Shell(); err != nil { _ = session.Close() @@ -452,79 +569,6 @@ func openSSHSessionOverTunnel( return sshClient, session, stdin, stdout, stderr, nil } -func dialGateway(scheme, host string, port int) (net.Conn, error) { - addr := net.JoinHostPort(host, strconv.Itoa(port)) - d := net.Dialer{Timeout: sandboxSSHGatewayDialTimeout} - switch scheme { - case "https": - serverName := host - if strings.HasPrefix(host, "[") && strings.Contains(host, "]") { - serverName = strings.TrimSuffix(strings.TrimPrefix(host, "["), "]") - } - return tls.DialWithDialer(&d, "tcp", addr, &tls.Config{ - MinVersion: tls.VersionTLS12, - ServerName: serverName, - }) - case "http": - return d.Dial("tcp", addr) - default: - return nil, fmt.Errorf("unsupported gateway_scheme %q", scheme) - } -} - -func completeHTTPConnect(ctx context.Context, conn net.Conn, gatewayHost, connectPath, sandboxID, token string) (net.Conn, error) { - deadline, ok := ctx.Deadline() - if ok { - _ = conn.SetDeadline(deadline) - } - req := fmt.Sprintf( - "CONNECT %s HTTP/1.1\r\nHost: %s\r\nX-Sandbox-Id: %s\r\nX-Sandbox-Token: %s\r\n\r\n", - connectPath, - gatewayHost, - sandboxID, - token, - ) - if _, err := conn.Write([]byte(req)); err != nil { - return nil, fmt.Errorf("CONNECT write: %w", err) - } - - br := bufio.NewReader(conn) - statusLine, err := br.ReadString('\n') - if err != nil { - return nil, fmt.Errorf("CONNECT read status: %w", err) - } - if !strings.Contains(statusLine, " 200 ") { - return nil, fmt.Errorf("CONNECT failed: %s", strings.TrimSpace(statusLine)) - } - for { - line, rerr := br.ReadString('\n') - if rerr != nil { - return nil, fmt.Errorf("CONNECT read headers: %w", rerr) - } - if line == "\r\n" || line == "\n" { - break - } - } - _ = conn.SetDeadline(time.Time{}) - return &prefixReaderConn{Conn: conn, br: br}, nil -} - -type prefixReaderConn struct { - net.Conn - br *bufio.Reader -} - -func (p *prefixReaderConn) Read(b []byte) (int, error) { - if p.br != nil && p.br.Buffered() > 0 { - n, err := p.br.Read(b) - if p.br.Buffered() == 0 { - p.br = nil - } - return n, err - } - return p.Conn.Read(b) -} - func isLoopbackHost(h string) bool { switch strings.ToLower(strings.TrimSpace(h)) { case "127.0.0.1", "localhost", "::1", "[::1]": diff --git a/go/core/internal/httpserver/handlers/sandbox_ssh_forward_test.go b/go/core/internal/httpserver/handlers/sandbox_ssh_forward_test.go new file mode 100644 index 0000000000..d663c2703a --- /dev/null +++ b/go/core/internal/httpserver/handlers/sandbox_ssh_forward_test.go @@ -0,0 +1,105 @@ +package handlers + +import ( + "context" + "io" + "testing" + + openshellv1 "github.com/kagent-dev/kagent/go/api/openshell/gen/openshellv1" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/proto" +) + +func TestBuildForwardTcpSSHInit(t *testing.T) { + init := buildForwardTcpSSHInit("sb-uuid", "tok-abc") + require.NotNil(t, init.GetInit()) + require.Equal(t, "sb-uuid", init.GetInit().GetSandboxId()) + require.Equal(t, "ssh-proxy:sb-uuid", init.GetInit().GetServiceId()) + require.Equal(t, "tok-abc", init.GetInit().GetAuthorizationToken()) + require.NotNil(t, init.GetInit().GetSsh()) +} + +func TestTCPForwardConnReadWrite(t *testing.T) { + stream := newMockForwardTcpStream() + conn := newTCPForwardConn(stream) + + require.NoError(t, stream.pushRecv(&openshellv1.TcpForwardFrame{ + Payload: &openshellv1.TcpForwardFrame_Data{Data: []byte("SSH-2.0-test\r\n")}, + })) + + buf := make([]byte, 32) + n, err := conn.Read(buf) + require.NoError(t, err) + require.Equal(t, "SSH-2.0-test\r\n", string(buf[:n])) + + _, err = conn.Write([]byte("SSH-2.0-client\r\n")) + require.NoError(t, err) + + select { + case frame := <-stream.sent: + require.Equal(t, "SSH-2.0-client\r\n", string(frame.GetData())) + default: + t.Fatal("expected forwarded client data frame") + } + + require.NoError(t, conn.Close()) +} + +// mockForwardTcpStream is a minimal OpenShell_ForwardTcpClient for unit tests. +type mockForwardTcpStream struct { + recv chan *openshellv1.TcpForwardFrame + sent chan *openshellv1.TcpForwardFrame +} + +func newMockForwardTcpStream() *mockForwardTcpStream { + return &mockForwardTcpStream{ + recv: make(chan *openshellv1.TcpForwardFrame, 8), + sent: make(chan *openshellv1.TcpForwardFrame, 8), + } +} + +func (m *mockForwardTcpStream) pushRecv(frame *openshellv1.TcpForwardFrame) error { + m.recv <- frame + return nil +} + +func (m *mockForwardTcpStream) Send(frame *openshellv1.TcpForwardFrame) error { + m.sent <- frame + return nil +} + +func (m *mockForwardTcpStream) Recv() (*openshellv1.TcpForwardFrame, error) { + frame, ok := <-m.recv + if !ok { + return nil, io.EOF + } + return frame, nil +} + +func (m *mockForwardTcpStream) SendMsg(msg any) error { + frame, ok := msg.(*openshellv1.TcpForwardFrame) + if !ok { + return io.ErrUnexpectedEOF + } + return m.Send(frame) +} + +func (m *mockForwardTcpStream) RecvMsg(msg any) error { + frame, err := m.Recv() + if err != nil { + return err + } + out, ok := msg.(*openshellv1.TcpForwardFrame) + if !ok { + return io.ErrUnexpectedEOF + } + proto.Reset(out) + proto.Merge(out, frame) + return nil +} + +func (m *mockForwardTcpStream) Header() (metadata.MD, error) { return nil, nil } +func (m *mockForwardTcpStream) Trailer() metadata.MD { return nil } +func (m *mockForwardTcpStream) CloseSend() error { close(m.sent); return nil } +func (m *mockForwardTcpStream) Context() context.Context { return context.Background() } diff --git a/go/core/internal/httpserver/handlers/sandbox_ssh_test.go b/go/core/internal/httpserver/handlers/sandbox_ssh_test.go index faa1498e12..f26d5ed2c7 100644 --- a/go/core/internal/httpserver/handlers/sandbox_ssh_test.go +++ b/go/core/internal/httpserver/handlers/sandbox_ssh_test.go @@ -1,10 +1,7 @@ package handlers import ( - "bufio" "bytes" - "context" - "net" "net/http" "net/http/httptest" "strings" @@ -33,21 +30,6 @@ func TestIsLoopbackHost(t *testing.T) { } } -func TestResolveSandboxSSHRemoteCommand(t *testing.T) { - plain, cmd := resolveSandboxSSHRemoteCommand(false, "") - if plain || cmd != defaultSandboxSSHLaunchCmd { - t.Fatalf("default launch: plain=%v cmd=%q", plain, cmd) - } - plain, cmd = resolveSandboxSSHRemoteCommand(true, "") - if !plain || cmd != "" { - t.Fatalf("plain shell: plain=%v cmd=%q", plain, cmd) - } - plain, cmd = resolveSandboxSSHRemoteCommand(false, " custom ") - if plain || cmd != "custom" { - t.Fatalf("override: plain=%v cmd=%q", plain, cmd) - } -} - func TestResolveGatewayDialHost(t *testing.T) { got, err := resolveGatewayDialHost("ingress.example.com", "ignored:8080") if err != nil || got != "ingress.example.com" { @@ -259,67 +241,3 @@ func TestReadSandboxSSHStart(t *testing.T) { } }) } - -func TestCompleteHTTPConnect(t *testing.T) { - t.Run("ok and reads tunneled bytes", func(t *testing.T) { - client, server := net.Pipe() - done := make(chan struct{}) - go func() { - defer close(done) - defer server.Close() - br := bufio.NewReader(server) - for { - line, err := br.ReadString('\n') - if err != nil { - return - } - if line == "\r\n" || line == "\n" { - break - } - } - if _, err := server.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\npayload")); err != nil { - return - } - }() - - ctx := context.Background() - tunnel, err := completeHTTPConnect(ctx, client, "gw.example", "/connect-path", "sid", "tok") - if err != nil { - t.Fatal(err) - } - buf := make([]byte, 32) - n, err := tunnel.Read(buf) - if err != nil || string(buf[:n]) != "payload" { - t.Fatalf("read %q err %v", buf[:n], err) - } - _ = client.Close() - <-done - }) - - t.Run("non-200", func(t *testing.T) { - client, server := net.Pipe() - done := make(chan struct{}) - go func() { - defer close(done) - defer server.Close() - br := bufio.NewReader(server) - for { - line, err := br.ReadString('\n') - if err != nil { - return - } - if line == "\r\n" || line == "\n" { - break - } - } - _, _ = server.Write([]byte("HTTP/1.1 403 Forbidden\r\n\r\n")) - }() - - _, err := completeHTTPConnect(context.Background(), client, "gw.example", "/p", "sid", "tok") - _ = client.Close() - <-done - if err == nil || !strings.Contains(err.Error(), "CONNECT failed") { - t.Fatalf("got %v", err) - } - }) -} diff --git a/go/core/pkg/app/app.go b/go/core/pkg/app/app.go index d47ab55adf..c6a5fba6bd 100644 --- a/go/core/pkg/app/app.go +++ b/go/core/pkg/app/app.go @@ -739,9 +739,11 @@ func buildOpenshellSandboxBackends(ctx context.Context, cfg *Config, kubeClient } ocl := openshell.NewOpenClawBackend(kubeClient, clients, oc, nil) + hermesBackend := openshell.NewHermesBackend(kubeClient, clients, oc, nil) return map[v1alpha2.AgentHarnessBackendType]sandboxbackend.AsyncBackend{ v1alpha2.AgentHarnessBackendOpenClaw: ocl, v1alpha2.AgentHarnessBackendNemoClaw: ocl, + v1alpha2.AgentHarnessBackendHermes: hermesBackend, }, nil } diff --git a/go/core/pkg/sandboxbackend/openshell/agent_harness_ensure.go b/go/core/pkg/sandboxbackend/openshell/agent_harness_ensure.go new file mode 100644 index 0000000000..e79a4c9cfb --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/agent_harness_ensure.go @@ -0,0 +1,38 @@ +package openshell + +import ( + "context" + + openshellv1 "github.com/kagent-dev/kagent/go/api/openshell/gen/openshellv1" + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend" +) + +type createSandboxRequestBuilder func(*v1alpha2.AgentHarness, []string) (*openshellv1.CreateSandboxRequest, []string) + +// ensureAgentHarnessSandbox translates model config, upserts messaging providers, and creates the sandbox. +func (b *agentHarnessOpenShellBackend) ensureAgentHarnessSandbox( + ctx context.Context, + ah *v1alpha2.AgentHarness, + build createSandboxRequestBuilder, +) (sandboxbackend.EnsureResult, error) { + if err := translateModelConfig(ctx, ah, b.kubeClient, b.clients); err != nil { + return sandboxbackend.EnsureResult{}, err + } + providerNames, err := UpsertMessagingProviders(ctx, b.clients, b.kubeClient, ah) + if err != nil { + return sandboxbackend.EnsureResult{}, err + } + req, unsupported := build(ah, providerNames) + return b.CreateAgentHarnessSandbox(ctx, ah, req, unsupported) +} + +func attachMessagingProviders(req *openshellv1.CreateSandboxRequest, names []string) { + if req == nil || len(names) == 0 { + return + } + if req.Spec == nil { + req.Spec = &openshellv1.SandboxSpec{} + } + req.Spec.Providers = append(req.Spec.Providers, names...) +} diff --git a/go/core/pkg/sandboxbackend/openshell/channels/credentials.go b/go/core/pkg/sandboxbackend/openshell/channels/credentials.go new file mode 100644 index 0000000000..b203b82f8d --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/channels/credentials.go @@ -0,0 +1,101 @@ +package channels + +import ( + "context" + "fmt" + "strings" + + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// PutChannelCredential resolves a channel credential into env[envKey]. +func PutChannelCredential(ctx context.Context, kube client.Client, namespace string, cred v1alpha2.AgentHarnessChannelCredential, envKey string, env map[string]string) error { + if strings.TrimSpace(cred.Value) != "" { + env[envKey] = strings.TrimSpace(cred.Value) + return nil + } + if cred.ValueFrom == nil { + return fmt.Errorf("channel credential requires value or valueFrom") + } + v, err := cred.ValueFrom.Resolve(ctx, kube, namespace) + if err != nil { + return fmt.Errorf("resolve credential %s: %w", envKey, err) + } + env[envKey] = v + return nil +} + +// TelegramAllowFrom returns allowed Telegram user IDs from the channel spec. +func TelegramAllowFrom(ctx context.Context, kube client.Client, namespace string, spec *v1alpha2.AgentHarnessTelegramChannelSpec) ([]string, error) { + if len(spec.AllowedUserIDs) > 0 { + out := make([]string, 0, len(spec.AllowedUserIDs)) + for _, id := range spec.AllowedUserIDs { + s := strings.TrimSpace(id) + if s != "" { + out = append(out, s) + } + } + return out, nil + } + if spec.AllowedUserIDsFrom != nil { + raw, err := spec.AllowedUserIDsFrom.Resolve(ctx, kube, namespace) + if err != nil { + return nil, fmt.Errorf("resolve allowedUserIDsFrom: %w", err) + } + return SplitAllowedList(raw), nil + } + return nil, nil +} + +// SlackAllowedUsers returns allowed Slack user IDs from the channel spec (Hermes SLACK_ALLOWED_USERS). +func SlackAllowedUsers(ctx context.Context, kube client.Client, namespace string, spec *v1alpha2.AgentHarnessSlackChannelSpec) ([]string, error) { + if len(spec.AllowedUserIDs) > 0 { + out := make([]string, 0, len(spec.AllowedUserIDs)) + for _, id := range spec.AllowedUserIDs { + s := strings.TrimSpace(id) + if s != "" { + out = append(out, s) + } + } + return out, nil + } + if spec.AllowedUserIDsFrom != nil { + raw, err := spec.AllowedUserIDsFrom.Resolve(ctx, kube, namespace) + if err != nil { + return nil, fmt.Errorf("resolve allowedUserIDsFrom: %w", err) + } + return SplitAllowedList(raw), nil + } + return nil, nil +} + +// SplitAllowedList parses comma/newline/semicolon-separated ID lists. +func SplitAllowedList(raw string) []string { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil + } + var out []string + for _, part := range strings.FieldsFunc(raw, func(r rune) bool { + return r == ',' || r == '\n' || r == ';' + }) { + s := strings.TrimSpace(part) + if s != "" { + out = append(out, s) + } + } + return out +} + +// TrimNonEmptyStrings returns trimmed non-empty strings from ss. +func TrimNonEmptyStrings(ss []string) []string { + out := make([]string, 0, len(ss)) + for _, s := range ss { + s = strings.TrimSpace(s) + if s != "" { + out = append(out, s) + } + } + return out +} diff --git a/go/core/pkg/sandboxbackend/openshell/channels/envkeys.go b/go/core/pkg/sandboxbackend/openshell/channels/envkeys.go new file mode 100644 index 0000000000..db354228a1 --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/channels/envkeys.go @@ -0,0 +1,11 @@ +package channels + +// Standard OpenShell messaging credential environment variable names (NemoClaw contract). +const ( + EnvTelegramBotToken = "TELEGRAM_BOT_TOKEN" + EnvSlackBotToken = "SLACK_BOT_TOKEN" + EnvSlackAppToken = "SLACK_APP_TOKEN" + EnvSlackAllowedUsers = "SLACK_ALLOWED_USERS" + EnvSlackHomeChannel = "SLACK_HOME_CHANNEL" + EnvSlackHomeChannelName = "SLACK_HOME_CHANNEL_NAME" +) diff --git a/go/core/pkg/sandboxbackend/openshell/channels/placeholders.go b/go/core/pkg/sandboxbackend/openshell/channels/placeholders.go new file mode 100644 index 0000000000..96502438d7 --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/channels/placeholders.go @@ -0,0 +1,16 @@ +package channels + +// ResolveEnvPlaceholder is the in-sandbox placeholder OpenShell rewrites at L7 egress. +func ResolveEnvPlaceholder(envKey string) string { + return "openshell:resolve:env:" + envKey +} + +// SlackBotTokenPlaceholder matches NemoClaw Hermes/OpenClaw Slack bot token shape validation. +func SlackBotTokenPlaceholder() string { + return "xoxb-OPENSHELL-RESOLVE-ENV-SLACK_BOT_TOKEN" +} + +// SlackAppTokenPlaceholder matches NemoClaw Hermes/OpenClaw Slack app token shape validation. +func SlackAppTokenPlaceholder() string { + return "xapp-OPENSHELL-RESOLVE-ENV-SLACK_APP_TOKEN" +} diff --git a/go/core/pkg/sandboxbackend/openshell/channels/providers.go b/go/core/pkg/sandboxbackend/openshell/channels/providers.go new file mode 100644 index 0000000000..33e50ef000 --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/channels/providers.go @@ -0,0 +1,57 @@ +package channels + +// MessagingBridgeNames are OpenShell provider names for a sandbox (NemoClaw onboard). +func TelegramBridgeName(sandboxName string) string { + return sandboxName + "-telegram-bridge" +} + +func SlackBridgeName(sandboxName string) string { + return sandboxName + "-slack-bridge" +} + +func SlackAppBridgeName(sandboxName string) string { + return sandboxName + "-slack-app" +} + +// MessagingProviderDef is an OpenShell gateway provider for one messaging credential. +type MessagingProviderDef struct { + Name string + Credentials map[string]string +} + +// MessagingProviderDefs builds provider upsert records from resolved channel secrets. +func MessagingProviderDefs(sandboxName string, secrets map[string]string, resolved *Resolved) []MessagingProviderDef { + if sandboxName == "" || resolved == nil { + return nil + } + var defs []MessagingProviderDef + if resolved.HasTelegram { + if tok := secrets[EnvTelegramBotToken]; tok != "" { + defs = append(defs, MessagingProviderDef{ + Name: TelegramBridgeName(sandboxName), + Credentials: map[string]string{ + EnvTelegramBotToken: tok, + }, + }) + } + } + if resolved.HasSlack { + if tok := secrets[EnvSlackBotToken]; tok != "" { + defs = append(defs, MessagingProviderDef{ + Name: SlackBridgeName(sandboxName), + Credentials: map[string]string{ + EnvSlackBotToken: tok, + }, + }) + } + if tok := secrets[EnvSlackAppToken]; tok != "" { + defs = append(defs, MessagingProviderDef{ + Name: SlackAppBridgeName(sandboxName), + Credentials: map[string]string{ + EnvSlackAppToken: tok, + }, + }) + } + } + return defs +} diff --git a/go/core/pkg/sandboxbackend/openshell/channels/providers_test.go b/go/core/pkg/sandboxbackend/openshell/channels/providers_test.go new file mode 100644 index 0000000000..6f036ebb6d --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/channels/providers_test.go @@ -0,0 +1,24 @@ +package channels + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMessagingProviderDefs(t *testing.T) { + resolved := &Resolved{ + HasTelegram: true, + HasSlack: true, + Secrets: map[string]string{ + EnvTelegramBotToken: "tg", + EnvSlackBotToken: "xoxb", + EnvSlackAppToken: "xapp", + }, + } + defs := MessagingProviderDefs("ns-h", resolved.Secrets, resolved) + require.Len(t, defs, 3) + require.Equal(t, "ns-h-telegram-bridge", defs[0].Name) + require.Equal(t, "ns-h-slack-bridge", defs[1].Name) + require.Equal(t, "ns-h-slack-app", defs[2].Name) +} diff --git a/go/core/pkg/sandboxbackend/openshell/channels/resolve.go b/go/core/pkg/sandboxbackend/openshell/channels/resolve.go new file mode 100644 index 0000000000..7ae9c05de9 --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/channels/resolve.go @@ -0,0 +1,140 @@ +package channels + +import ( + "context" + "fmt" + "strings" + + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// TelegramAccount is one Telegram channel account in the harness. +type TelegramAccount struct { + Name string + AllowFrom []string +} + +// SlackAccount is one Slack channel account in the harness. +type SlackAccount struct { + Name string + ChannelAccess v1alpha2.AgentHarnessChannelAccess + AllowlistChannels []string + InteractiveReplies bool +} + +// Resolved holds channel credentials and per-backend configuration derived from AgentHarness.spec.channels. +type Resolved struct { + Secrets map[string]string + + HasTelegram bool + HasSlack bool + + TelegramAllow []string + SlackAllow []string + + // Hermes: first Slack channel with homeChannel / homeChannelName wins. + SlackHomeChannel string + SlackHomeChannelName string + + Telegram []TelegramAccount + Slack []SlackAccount + + slackRootPolicy v1alpha2.AgentHarnessChannelAccess + slackSeen bool +} + +// Resolve reads AgentHarness channels, populates standard credential env keys in Secrets, +// and returns structured account metadata for Hermes/OpenClaw bootstrap. +func Resolve(ctx context.Context, kube client.Client, namespace string, channels []v1alpha2.AgentHarnessChannel) (*Resolved, error) { + r := &Resolved{Secrets: map[string]string{}} + for _, ch := range channels { + switch ch.Type { + case v1alpha2.AgentHarnessChannelTypeTelegram: + if err := r.addTelegram(ctx, kube, namespace, ch); err != nil { + return nil, err + } + case v1alpha2.AgentHarnessChannelTypeSlack: + if err := r.addSlack(ctx, kube, namespace, ch); err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("channel %q: unsupported type %q", ch.Name, ch.Type) + } + } + return r, nil +} + +func (r *Resolved) addTelegram(ctx context.Context, kube client.Client, namespace string, ch v1alpha2.AgentHarnessChannel) error { + spec := ch.Telegram + if spec == nil { + return fmt.Errorf("channel %q: telegram spec is required", ch.Name) + } + if err := PutChannelCredential(ctx, kube, namespace, spec.BotToken, EnvTelegramBotToken, r.Secrets); err != nil { + return fmt.Errorf("channel %q telegram bot token: %w", ch.Name, err) + } + allow, err := TelegramAllowFrom(ctx, kube, namespace, spec) + if err != nil { + return fmt.Errorf("channel %q telegram allowlist: %w", ch.Name, err) + } + r.HasTelegram = true + if len(allow) > 0 { + r.TelegramAllow = allow + } + r.Telegram = append(r.Telegram, TelegramAccount{Name: ch.Name, AllowFrom: allow}) + return nil +} + +func (r *Resolved) addSlack(ctx context.Context, kube client.Client, namespace string, ch v1alpha2.AgentHarnessChannel) error { + spec := ch.Slack + if spec == nil { + return fmt.Errorf("channel %q: slack spec is required", ch.Name) + } + if err := PutChannelCredential(ctx, kube, namespace, spec.BotToken, EnvSlackBotToken, r.Secrets); err != nil { + return fmt.Errorf("channel %q slack bot token: %w", ch.Name, err) + } + if err := PutChannelCredential(ctx, kube, namespace, spec.AppToken, EnvSlackAppToken, r.Secrets); err != nil { + return fmt.Errorf("channel %q slack app token: %w", ch.Name, err) + } + allow, err := SlackAllowedUsers(ctx, kube, namespace, spec) + if err != nil { + return fmt.Errorf("channel %q slack allowed users: %w", ch.Name, err) + } + interactive := true + if spec.InteractiveReplies != nil { + interactive = *spec.InteractiveReplies + } + access := spec.ChannelAccess + if access == "" { + access = v1alpha2.AgentHarnessChannelAccessOpen + } + r.HasSlack = true + if len(allow) > 0 { + r.SlackAllow = append(r.SlackAllow, allow...) + } + r.Slack = append(r.Slack, SlackAccount{ + Name: ch.Name, + ChannelAccess: access, + AllowlistChannels: TrimNonEmptyStrings(spec.AllowlistChannels), + InteractiveReplies: interactive, + }) + if !r.slackSeen { + r.slackRootPolicy = access + r.slackSeen = true + } + if r.SlackHomeChannel == "" { + if home := strings.TrimSpace(spec.HomeChannel); home != "" { + r.SlackHomeChannel = home + r.SlackHomeChannelName = strings.TrimSpace(spec.HomeChannelName) + } + } + return nil +} + +// SlackRootGroupPolicy returns the group policy for the first Slack channel (OpenClaw bundle). +func (r *Resolved) SlackRootGroupPolicy() v1alpha2.AgentHarnessChannelAccess { + if r.slackSeen { + return r.slackRootPolicy + } + return v1alpha2.AgentHarnessChannelAccessOpen +} diff --git a/go/core/pkg/sandboxbackend/openshell/hermes.go b/go/core/pkg/sandboxbackend/openshell/hermes.go new file mode 100644 index 0000000000..c58490732a --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/hermes.go @@ -0,0 +1,186 @@ +package openshell + +import ( + "context" + "fmt" + "strings" + "time" + + openshellv1 "github.com/kagent-dev/kagent/go/api/openshell/gen/openshellv1" + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/core/internal/utils" + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend" + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/hermes" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" +) + +// HermesBackend implements AsyncBackend and PostReadyBackend for Hermes AgentHarness resources. +type HermesBackend struct { + *agentHarnessOpenShellBackend +} + +var _ sandboxbackend.AsyncBackend = (*HermesBackend)(nil) + +// NewHermesBackend returns the Hermes harness backend. +func NewHermesBackend(kubeClient client.Client, clients *OpenShellClients, cfg Config, recorder record.EventRecorder) *HermesBackend { + return &HermesBackend{ + agentHarnessOpenShellBackend: newAgentHarnessOpenShellBackend( + kubeClient, clients, cfg, recorder, + v1alpha2.AgentHarnessBackendHermes, + ), + } +} + +// EnsureAgentHarness syncs ModelConfig then creates the Hermes sandbox. +func (b *HermesBackend) EnsureAgentHarness(ctx context.Context, ah *v1alpha2.AgentHarness) (sandboxbackend.EnsureResult, error) { + if ah == nil { + return sandboxbackend.EnsureResult{}, fmt.Errorf("AgentHarness is required") + } + ctx, cancel := b.CallCtx(ctx) + defer cancel() + ctx = withAuth(ctx, b.cfg.Token) + + if res, found, err := b.findExistingSandbox(ctx, ah); err != nil || found { + return res, err + } + return b.ensureAgentHarnessSandbox(ctx, ah, buildHermesCreateRequest) +} + +// OnAgentHarnessReady writes ~/.hermes/config.yaml and .env, updates the config hash, and starts the gateway. +func (b *HermesBackend) OnAgentHarnessReady(ctx context.Context, ah *v1alpha2.AgentHarness, h sandboxbackend.Handle) error { + ref := strings.TrimSpace(ah.Spec.ModelConfigRef) + if ref == "" { + return nil + } + if h.ID == "" { + return fmt.Errorf("sandbox backend handle id is empty") + } + if b.kubeClient == nil { + return fmt.Errorf("kubernetes client is required for hermes bootstrap") + } + + modelConfigRef, err := utils.ParseRefString(ref, ah.Namespace) + if err != nil { + return fmt.Errorf("parse modelConfigRef: %w", err) + } + mc := &v1alpha2.ModelConfig{} + if err := b.kubeClient.Get(ctx, modelConfigRef, mc); err != nil { + return fmt.Errorf("get ModelConfig: %w", err) + } + + if _, err := UpsertMessagingProviders(ctx, b.clients, b.kubeClient, ah); err != nil { + return fmt.Errorf("upsert messaging providers: %w", err) + } + + configYAML, envFile, execEnv, err := hermes.BuildBootstrapArtifacts(ctx, b.kubeClient, ah.Namespace, ah, mc) + if err != nil { + return fmt.Errorf("build hermes config: %w", err) + } + + token := b.cfg.Token + idCtx, cancelID := b.CallCtx(ctx) + defer cancelID() + execID, err := b.ExecSandboxID(withAuth(idCtx, token), h.ID) + if err != nil { + return fmt.Errorf("resolve sandbox exec id: %w", err) + } + + mkdirScript := fmt.Sprintf(`mkdir -p %s`, hermes.HermesConfigDir) + installCtx, cancelInstall := context.WithTimeout(ctx, 120*time.Second+15*time.Second) + defer cancelInstall() + code, stderr, err := b.ExecSandbox(withAuth(installCtx, token), execID, []string{"sh", "-c", mkdirScript}, nil, execEnv, 30) + if err != nil { + return fmt.Errorf("mkdir hermes config dir: %w", err) + } + if code != 0 { + return fmt.Errorf("mkdir hermes config dir: exit %d: %s", code, strings.TrimSpace(stderr)) + } + + configInstall := []string{"sh", "-c", fmt.Sprintf(`cat > %s/config.yaml`, hermes.HermesConfigDir)} + code, stderr, err = b.ExecSandbox(withAuth(installCtx, token), execID, configInstall, configYAML, execEnv, 60) + if err != nil { + return fmt.Errorf("install hermes config.yaml: %w", err) + } + if code != 0 { + return fmt.Errorf("install hermes config.yaml: exit %d: %s", code, strings.TrimSpace(stderr)) + } + + envInstall := []string{"sh", "-c", fmt.Sprintf(`cat > %s/.env`, hermes.HermesConfigDir)} + code, stderr, err = b.ExecSandbox(withAuth(installCtx, token), execID, envInstall, envFile, execEnv, 60) + if err != nil { + return fmt.Errorf("install hermes .env: %w", err) + } + if code != 0 { + return fmt.Errorf("install hermes .env: exit %d: %s", code, strings.TrimSpace(stderr)) + } + + hashScript := fmt.Sprintf( + `mkdir -p /etc/nemoclaw && sha256sum %s/config.yaml %s/.env > %s && chmod 444 %s 2>/dev/null || true`, + hermes.HermesConfigDir, hermes.HermesConfigDir, hermes.HermesConfigHashFile, hermes.HermesConfigHashFile, + ) + hashCtx, cancelHash := context.WithTimeout(ctx, 30*time.Second) + defer cancelHash() + code, stderr, err = b.ExecSandbox(withAuth(hashCtx, token), execID, []string{"sh", "-c", hashScript}, nil, execEnv, 30) + if err != nil { + return fmt.Errorf("write hermes config hash: %w", err) + } + if code != 0 { + ctrllog.FromContext(ctx).Info("hermes config hash write skipped (non-fatal)", "stderr", strings.TrimSpace(stderr)) + } + + gwCtx, cancelGW := context.WithTimeout(ctx, 90*time.Second+15*time.Second) + defer cancelGW() + gatewayStart := fmt.Sprintf( + `HERMES_HOME=%s nohup hermes gateway run >>/tmp/gateway.log 2>&1 &`, + hermes.HermesConfigDir, + ) + code, stderr, err = b.ExecSandbox(withAuth(gwCtx, token), execID, []string{"sh", "-c", gatewayStart}, nil, execEnv, 30) + if err != nil { + return fmt.Errorf("start hermes gateway: %w", err) + } + if code != 0 { + return fmt.Errorf("start hermes gateway: exit %d: %s", code, strings.TrimSpace(stderr)) + } + + waitGateway := fmt.Sprintf( + `for i in $(seq 1 30); do ss -tln 2>/dev/null | grep -q "127.0.0.1:%d" && exit 0; sleep 1; done; exit 0`, + hermes.HermesInternalGatewayPort, + ) + code, stderr, err = b.ExecSandbox(withAuth(gwCtx, token), execID, []string{"sh", "-c", waitGateway}, nil, execEnv, 45) + if err != nil { + return fmt.Errorf("wait for hermes gateway listen: %w", err) + } + if code != 0 { + return fmt.Errorf("wait for hermes gateway listen: exit %d: %s", code, strings.TrimSpace(stderr)) + } + + socatStart := fmt.Sprintf( + `command -v socat >/dev/null 2>&1 && nohup socat TCP-LISTEN:%d,bind=0.0.0.0,fork,reuseaddr TCP:127.0.0.1:%d >>/tmp/socat.log 2>&1 &`, + hermes.HermesPublicGatewayPort, + hermes.HermesInternalGatewayPort, + ) + code, stderr, err = b.ExecSandbox(withAuth(gwCtx, token), execID, []string{"sh", "-c", socatStart}, nil, execEnv, 30) + if err != nil { + return fmt.Errorf("start hermes socat forwarder: %w", err) + } + if code != 0 { + return fmt.Errorf("start hermes socat forwarder: exit %d: %s", code, strings.TrimSpace(stderr)) + } + + ctrllog.FromContext(ctx).Info("hermes bootstrap completed", "agentHarness", ah.Namespace+"/"+ah.Name) + return nil +} + +func buildHermesCreateRequest(ah *v1alpha2.AgentHarness, messagingProviders []string) (*openshellv1.CreateSandboxRequest, []string) { + req, unsupported := buildAgentHarnessOpenshellCreateRequest(ah) + if req.GetSpec().GetTemplate() == nil { + req.Spec.Template = &openshellv1.SandboxTemplate{} + } + if ah.Spec.Image == "" { + req.Spec.Template.Image = hermes.HermesSandboxBaseImage + } + attachMessagingProviders(req, messagingProviders) + return req, unsupported +} diff --git a/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap.go b/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap.go new file mode 100644 index 0000000000..ea8928419a --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap.go @@ -0,0 +1,174 @@ +package hermes + +import ( + "context" + "fmt" + "maps" + "strings" + + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/channels" + "gopkg.in/yaml.v3" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// hermesConfig is the YAML shape written to ~/.hermes/config.yaml. +type hermesConfig struct { + ConfigVersion int `yaml:"_config_version"` + Model hermesModel `yaml:"model"` + Terminal hermesTerminal `yaml:"terminal"` + Agent hermesAgent `yaml:"agent"` + Memory hermesMemory `yaml:"memory"` + Skills hermesSkills `yaml:"skills"` + Display hermesDisplay `yaml:"display"` + Platforms hermesPlatforms `yaml:"platforms"` + Telegram *hermesTelegram `yaml:"telegram,omitempty"` +} + +type hermesModel struct { + Default string `yaml:"default"` + Provider string `yaml:"provider"` + BaseURL string `yaml:"base_url"` +} + +type hermesTerminal struct { + Backend string `yaml:"backend"` + Timeout int `yaml:"timeout"` +} + +type hermesAgent struct { + MaxTurns int `yaml:"max_turns"` + ReasoningEffort string `yaml:"reasoning_effort"` +} + +type hermesMemory struct { + MemoryEnabled bool `yaml:"memory_enabled"` + UserProfileEnabled bool `yaml:"user_profile_enabled"` +} + +type hermesSkills struct { + CreationNudgeInterval int `yaml:"creation_nudge_interval"` +} + +type hermesDisplay struct { + Compact bool `yaml:"compact"` + ToolProgress string `yaml:"tool_progress"` +} + +type hermesPlatforms struct { + APIServer hermesAPIServer `yaml:"api_server"` +} + +type hermesAPIServer struct { + Enabled bool `yaml:"enabled"` + Extra hermesAPIServerEx `yaml:"extra"` +} + +type hermesAPIServerEx struct { + Port int `yaml:"port"` + Host string `yaml:"host"` +} + +type hermesTelegram struct { + RequireMention bool `yaml:"require_mention"` +} + +// BuildHermesConfigYAML returns config.yaml bytes for the given ModelConfig. +func BuildHermesConfigYAML(mc *v1alpha2.ModelConfig, msg *messagingState) ([]byte, error) { + if mc == nil { + return nil, fmt.Errorf("ModelConfig is required") + } + modelID := strings.TrimSpace(mc.Spec.Model) + if modelID == "" { + return nil, fmt.Errorf("ModelConfig.spec.model is required for Hermes bootstrap") + } + + cfg := hermesConfig{ + ConfigVersion: 12, + Model: hermesModel{ + Default: modelID, + Provider: "custom", + BaseURL: DefaultInferenceBaseURL, + }, + Terminal: hermesTerminal{Backend: "local", Timeout: 180}, + Agent: hermesAgent{MaxTurns: 60, ReasoningEffort: "medium"}, + Memory: hermesMemory{ + MemoryEnabled: true, + UserProfileEnabled: true, + }, + Skills: hermesSkills{CreationNudgeInterval: 15}, + Display: hermesDisplay{Compact: false, ToolProgress: "all"}, + Platforms: hermesPlatforms{ + APIServer: hermesAPIServer{ + Enabled: true, + Extra: hermesAPIServerEx{ + Port: HermesInternalGatewayPort, + Host: "127.0.0.1", + }, + }, + }, + } + if msg != nil && msg.hasTelegram() { + cfg.Telegram = &hermesTelegram{RequireMention: true} + } + + raw, err := yaml.Marshal(&cfg) + if err != nil { + return nil, fmt.Errorf("marshal hermes config yaml: %w", err) + } + return raw, nil +} + +// BuildHermesEnvFile returns .env file bytes and populates execEnv with resolved channel secrets. +func BuildHermesEnvFile(msg *messagingState, execEnv map[string]string) []byte { + lines := []string{ + fmt.Sprintf("API_SERVER_PORT=%d", HermesInternalGatewayPort), + "API_SERVER_HOST=127.0.0.1", + } + if msg != nil { + if msg.hasTelegram() { + lines = append(lines, "TELEGRAM_BOT_TOKEN="+channels.ResolveEnvPlaceholder(channels.EnvTelegramBotToken)) + if allow := msg.telegramAllow(); len(allow) > 0 { + lines = append(lines, "TELEGRAM_ALLOWED_USERS="+strings.Join(allow, ",")) + } + } + if msg.hasSlack() { + lines = append(lines, + "SLACK_BOT_TOKEN="+channels.SlackBotTokenPlaceholder(), + "SLACK_APP_TOKEN="+channels.SlackAppTokenPlaceholder(), + ) + if allow := msg.slackAllow(); len(allow) > 0 { + lines = append(lines, channels.EnvSlackAllowedUsers+"="+strings.Join(allow, ",")) + } + if home := msg.slackHomeChannel(); home != "" { + lines = append(lines, channels.EnvSlackHomeChannel+"="+home) + if name := msg.slackHomeChannelName(); name != "" { + lines = append(lines, channels.EnvSlackHomeChannelName+"="+name) + } + } + } + } + if len(lines) == 0 { + return nil + } + return []byte(strings.Join(lines, "\n") + "\n") +} + +// BuildBootstrapArtifacts builds config.yaml, .env, and exec environment for Hermes bootstrap. +func BuildBootstrapArtifacts(ctx context.Context, kube client.Client, namespace string, ah *v1alpha2.AgentHarness, mc *v1alpha2.ModelConfig) (configYAML, envFile []byte, execEnv map[string]string, err error) { + execEnv = map[string]string{} + var msg *messagingState + if ah != nil && len(ah.Spec.Channels) > 0 { + msg, err = AccumulateMessagingChannels(ctx, kube, namespace, ah.Spec.Channels, nil) + if err != nil { + return nil, nil, nil, err + } + maps.Copy(execEnv, msg.secrets()) + } + configYAML, err = BuildHermesConfigYAML(mc, msg) + if err != nil { + return nil, nil, nil, err + } + envFile = BuildHermesEnvFile(msg, execEnv) + return configYAML, envFile, execEnv, nil +} diff --git a/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap_test.go b/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap_test.go new file mode 100644 index 0000000000..d9d77357b7 --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap_test.go @@ -0,0 +1,98 @@ +package hermes_test + +import ( + "context" + "testing" + + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/hermes" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestBuildHermesConfigYAML_ModelOnly(t *testing.T) { + mc := &v1alpha2.ModelConfig{ + Spec: v1alpha2.ModelConfigSpec{ + Model: "test-model", + Provider: v1alpha2.ModelProviderOpenAI, + }, + } + raw, err := hermes.BuildHermesConfigYAML(mc, nil) + require.NoError(t, err) + s := string(raw) + require.Contains(t, s, "default: test-model") + require.Contains(t, s, "provider: custom") + require.Contains(t, s, "base_url: https://inference.local/v1") + require.Contains(t, s, "port: 18642") +} + +func TestBuildBootstrapArtifacts_TelegramSlack(t *testing.T) { + scheme := runtime.NewScheme() + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(v1alpha2.AddToScheme(scheme)) + + ns := "default" + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "tg", Namespace: ns}, + Data: map[string][]byte{"token": []byte("tg-token")}, + } + mc := &v1alpha2.ModelConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "mc1", Namespace: ns}, + Spec: v1alpha2.ModelConfigSpec{ + Model: "gpt-4o", + Provider: v1alpha2.ModelProviderOpenAI, + }, + } + ah := &v1alpha2.AgentHarness{ + ObjectMeta: metav1.ObjectMeta{Name: "h1", Namespace: ns}, + Spec: v1alpha2.AgentHarnessSpec{ + Channels: []v1alpha2.AgentHarnessChannel{ + { + Name: "tg", + Type: v1alpha2.AgentHarnessChannelTypeTelegram, + Telegram: &v1alpha2.AgentHarnessTelegramChannelSpec{ + BotToken: v1alpha2.AgentHarnessChannelCredential{ + ValueFrom: &v1alpha2.ValueSource{ + Type: v1alpha2.SecretValueSource, + Name: "tg", + Key: "token", + }, + }, + AllowedUserIDs: []string{"123456789"}, + }, + }, + { + Name: "sl", + Type: v1alpha2.AgentHarnessChannelTypeSlack, + Slack: &v1alpha2.AgentHarnessSlackChannelSpec{ + BotToken: v1alpha2.AgentHarnessChannelCredential{Value: "xoxb-bot"}, + AppToken: v1alpha2.AgentHarnessChannelCredential{Value: "xapp-app"}, + AllowedUserIDs: []string{"U01234567", "U89ABCDEF"}, + HomeChannel: "C01234567890", + HomeChannelName: "general", + }, + }, + }, + }, + } + + kube := fake.NewClientBuilder().WithScheme(scheme).WithObjects(secret, mc).Build() + configYAML, envFile, execEnv, err := hermes.BuildBootstrapArtifacts(context.Background(), kube, ns, ah, mc) + require.NoError(t, err) + require.Contains(t, string(configYAML), "require_mention: true") + env := string(envFile) + require.Contains(t, env, "TELEGRAM_BOT_TOKEN=openshell:resolve:env:TELEGRAM_BOT_TOKEN") + require.Contains(t, env, "TELEGRAM_ALLOWED_USERS=123456789") + require.Contains(t, env, "SLACK_BOT_TOKEN=xoxb-OPENSHELL-RESOLVE-ENV-SLACK_BOT_TOKEN") + require.Contains(t, env, "SLACK_APP_TOKEN=xapp-OPENSHELL-RESOLVE-ENV-SLACK_APP_TOKEN") + require.Contains(t, env, "SLACK_ALLOWED_USERS=U01234567,U89ABCDEF") + require.Contains(t, env, "SLACK_HOME_CHANNEL=C01234567890") + require.Contains(t, env, "SLACK_HOME_CHANNEL_NAME=general") + require.Equal(t, "tg-token", execEnv["TELEGRAM_BOT_TOKEN"]) + require.Equal(t, "xoxb-bot", execEnv["SLACK_BOT_TOKEN"]) +} diff --git a/go/core/pkg/sandboxbackend/openshell/hermes/channels.go b/go/core/pkg/sandboxbackend/openshell/hermes/channels.go new file mode 100644 index 0000000000..568b23ea5f --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/hermes/channels.go @@ -0,0 +1,65 @@ +package hermes + +import ( + "context" + + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/channels" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type messagingState struct { + resolved *channels.Resolved +} + +// AccumulateMessagingChannels resolves channel credentials and returns messaging state for Hermes bootstrap. +func AccumulateMessagingChannels(ctx context.Context, kube client.Client, namespace string, specChannels []v1alpha2.AgentHarnessChannel, _ map[string]string) (*messagingState, error) { + resolved, err := channels.Resolve(ctx, kube, namespace, specChannels) + if err != nil { + return nil, err + } + return &messagingState{resolved: resolved}, nil +} + +func (st *messagingState) hasTelegram() bool { + return st != nil && st.resolved != nil && st.resolved.HasTelegram +} + +func (st *messagingState) hasSlack() bool { + return st != nil && st.resolved != nil && st.resolved.HasSlack +} + +func (st *messagingState) telegramAllow() []string { + if st == nil || st.resolved == nil { + return nil + } + return st.resolved.TelegramAllow +} + +func (st *messagingState) slackAllow() []string { + if st == nil || st.resolved == nil { + return nil + } + return st.resolved.SlackAllow +} + +func (st *messagingState) slackHomeChannel() string { + if st == nil || st.resolved == nil { + return "" + } + return st.resolved.SlackHomeChannel +} + +func (st *messagingState) slackHomeChannelName() string { + if st == nil || st.resolved == nil { + return "" + } + return st.resolved.SlackHomeChannelName +} + +func (st *messagingState) secrets() map[string]string { + if st == nil || st.resolved == nil { + return nil + } + return st.resolved.Secrets +} diff --git a/go/core/pkg/sandboxbackend/openshell/hermes/constants.go b/go/core/pkg/sandboxbackend/openshell/hermes/constants.go new file mode 100644 index 0000000000..62c83e707a --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/hermes/constants.go @@ -0,0 +1,21 @@ +package hermes + +const ( + // HermesSandboxBaseImage is the default OpenShell VM image for Hermes harnesses. + HermesSandboxBaseImage = "ghcr.io/nvidia/nemoclaw/hermes-sandbox-base:latest" + + // HermesConfigDir is the in-sandbox Hermes config root (HERMES_HOME). + HermesConfigDir = "/sandbox/.hermes" + + // HermesConfigHashFile is the root-owned integrity anchor written at bootstrap. + HermesConfigHashFile = "/etc/nemoclaw/hermes.config-hash" + + // HermesInternalGatewayPort is where Hermes binds (127.0.0.1 only). + HermesInternalGatewayPort = 18642 + + // HermesPublicGatewayPort is exposed via socat for OpenShell port forwarding. + HermesPublicGatewayPort = 8642 + + // DefaultInferenceBaseURL is the model base_url when routing through OpenShell inference.local. + DefaultInferenceBaseURL = "https://inference.local/v1" +) diff --git a/go/core/pkg/sandboxbackend/openshell/hermes/messaging_providers_test.go b/go/core/pkg/sandboxbackend/openshell/hermes/messaging_providers_test.go new file mode 100644 index 0000000000..e45047019a --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/hermes/messaging_providers_test.go @@ -0,0 +1,49 @@ +package hermes + +import ( + "context" + "testing" + + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/channels" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestMessagingProviderDefsFromChannels(t *testing.T) { + ns := "default" + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "tg", Namespace: ns}, + Data: map[string][]byte{"token": []byte("123:ABC")}, + } + kube := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(secret).Build() + ah := &v1alpha2.AgentHarness{ + ObjectMeta: metav1.ObjectMeta{Name: "mybot", Namespace: ns}, + Spec: v1alpha2.AgentHarnessSpec{ + Channels: []v1alpha2.AgentHarnessChannel{ + { + Name: "tg", + Type: v1alpha2.AgentHarnessChannelTypeTelegram, + Telegram: &v1alpha2.AgentHarnessTelegramChannelSpec{ + BotToken: v1alpha2.AgentHarnessChannelCredential{ + ValueFrom: &v1alpha2.ValueSource{ + Type: v1alpha2.SecretValueSource, + Name: "tg", + Key: "token", + }, + }, + }, + }, + }, + }, + } + resolved, err := channels.Resolve(context.Background(), kube, ns, ah.Spec.Channels) + require.NoError(t, err) + defs := channels.MessagingProviderDefs("default-mybot", resolved.Secrets, resolved) + require.Len(t, defs, 1) + require.Equal(t, "default-mybot-telegram-bridge", defs[0].Name) + require.Equal(t, "123:ABC", defs[0].Credentials[channels.EnvTelegramBotToken]) +} diff --git a/go/core/pkg/sandboxbackend/openshell/hermes/policy.go b/go/core/pkg/sandboxbackend/openshell/hermes/policy.go new file mode 100644 index 0000000000..de153d666c --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/hermes/policy.go @@ -0,0 +1,306 @@ +package hermes + +import ( + "maps" + + sandboxv1 "github.com/kagent-dev/kagent/go/api/openshell/gen/sandboxv1" + "github.com/kagent-dev/kagent/go/api/v1alpha2" +) + +// Network policy map keys for Hermes sandbox egress. Source of truth: +// NemoClaw agents/hermes/policy-additions.yaml +const ( + NetworkPolicyKeyNVIDIA = "nvidia" + NetworkPolicyKeyGitHub = "github" + NetworkPolicyKeyNousResearch = "nous_research" + NetworkPolicyKeyPyPI = "pypi" + NetworkPolicyKeyTelegram = "telegram" + NetworkPolicyKeySlack = "slack" +) + +const ( + endpointProtocolREST = "rest" + endpointEnforcement = "enforce" + endpointAccessFull = "full" + endpointTLSSkip = "skip" +) + +var hermesCoreBinaries = []*sandboxv1.NetworkBinary{ + {Path: "/usr/local/bin/hermes"}, + {Path: "/usr/bin/python3*"}, + {Path: "/opt/hermes/.venv/bin/python"}, +} + +var hermesMessagingBinaries = []*sandboxv1.NetworkBinary{ + {Path: "/usr/local/bin/node"}, + {Path: "/usr/bin/python3*"}, + {Path: "/opt/hermes/.venv/bin/python"}, +} + +var nvidiaInferenceRules = []*sandboxv1.L7Rule{ + {Allow: &sandboxv1.L7Allow{Method: "POST", Path: "/v1/chat/completions"}}, + {Allow: &sandboxv1.L7Allow{Method: "POST", Path: "/v1/completions"}}, + {Allow: &sandboxv1.L7Allow{Method: "POST", Path: "/v1/embeddings"}}, + {Allow: &sandboxv1.L7Allow{Method: "GET", Path: "/v1/models"}}, + {Allow: &sandboxv1.L7Allow{Method: "GET", Path: "/v1/models/**"}}, +} + +var nousResearchWildcardRules = []*sandboxv1.L7Rule{ + {Allow: &sandboxv1.L7Allow{Method: "GET", Path: "/**"}}, + {Allow: &sandboxv1.L7Allow{Method: "POST", Path: "/**"}}, +} + +var pypiGETRules = []*sandboxv1.L7Rule{ + {Allow: &sandboxv1.L7Allow{Method: "GET", Path: "/**"}}, +} + +var telegramHermesRules = []*sandboxv1.L7Rule{ + {Allow: &sandboxv1.L7Allow{Method: "GET", Path: "/bot*/**"}}, + {Allow: &sandboxv1.L7Allow{Method: "POST", Path: "/bot*/**"}}, + {Allow: &sandboxv1.L7Allow{Method: "GET", Path: "/file/bot*/**"}}, +} + +var slackRESTRules = []*sandboxv1.L7Rule{ + {Allow: &sandboxv1.L7Allow{Method: "GET", Path: "/**"}}, + {Allow: &sandboxv1.L7Allow{Method: "POST", Path: "/**"}}, +} + +var slackWssRules = []*sandboxv1.L7Rule{ + {Allow: &sandboxv1.L7Allow{Method: "GET", Path: "/**"}}, + {Allow: &sandboxv1.L7Allow{Method: "WEBSOCKET_TEXT", Path: "/**"}}, +} + +func restEndpoint(host string, rules []*sandboxv1.L7Rule) *sandboxv1.NetworkEndpoint { + return &sandboxv1.NetworkEndpoint{ + Host: host, + Ports: []uint32{443}, + Protocol: endpointProtocolREST, + Enforcement: endpointEnforcement, + Rules: rules, + } +} + +func restEndpointFullAccess(host string) *sandboxv1.NetworkEndpoint { + return &sandboxv1.NetworkEndpoint{ + Host: host, + Ports: []uint32{443}, + Protocol: endpointProtocolREST, + Enforcement: endpointEnforcement, + Access: endpointAccessFull, + } +} + +func restEndpointSlack(host string) *sandboxv1.NetworkEndpoint { + ep := restEndpoint(host, slackRESTRules) + ep.RequestBodyCredentialRewrite = true + return ep +} + +func slackWssEndpoint(host string) *sandboxv1.NetworkEndpoint { + return &sandboxv1.NetworkEndpoint{ + Host: host, + Ports: []uint32{443}, + Protocol: "websocket", + Enforcement: endpointEnforcement, + WebsocketCredentialRewrite: true, + Rules: slackWssRules, + } +} + +// IsHermesSandboxBackend reports backends that use the Hermes sandbox baseline. +func IsHermesSandboxBackend(b v1alpha2.AgentHarnessBackendType) bool { + return b == v1alpha2.AgentHarnessBackendHermes +} + +func defaultHermesFilesystemPolicy() *sandboxv1.FilesystemPolicy { + return &sandboxv1.FilesystemPolicy{ + IncludeWorkdir: true, + ReadOnly: []string{ + "/usr", + "/lib", + "/opt/hermes", + "/proc", + "/dev/urandom", + "/app", + "/etc", + "/var/log", + }, + ReadWrite: []string{ + "/sandbox", + "/tmp", + "/dev/null", + HermesConfigDir, + }, + } +} + +func defaultHermesLandlockPolicy() *sandboxv1.LandlockPolicy { + return &sandboxv1.LandlockPolicy{Compatibility: "best_effort"} +} + +func defaultHermesProcessPolicy() *sandboxv1.ProcessPolicy { + return &sandboxv1.ProcessPolicy{ + RunAsUser: "sandbox", + RunAsGroup: "sandbox", + } +} + +func defaultHermesNetworkPolicies() map[string]*sandboxv1.NetworkPolicyRule { + nousHosts := []string{ + "nousresearch.com", + "hermes-agent.nousresearch.com", + "inference-api.nousresearch.com", + "portal.nousresearch.com", + "browser-use-gateway.nousresearch.com", + "modal-gateway.nousresearch.com", + "openai-audio-gateway.nousresearch.com", + "fal-queue-gateway.nousresearch.com", + "firecrawl-gateway.nousresearch.com", + "tool-gateway.nousresearch.com", + } + nousEndpoints := make([]*sandboxv1.NetworkEndpoint, 0, len(nousHosts)) + for _, h := range nousHosts { + nousEndpoints = append(nousEndpoints, restEndpoint(h, nousResearchWildcardRules)) + } + + return map[string]*sandboxv1.NetworkPolicyRule{ + NetworkPolicyKeyNVIDIA: { + Name: "nvidia", + Endpoints: []*sandboxv1.NetworkEndpoint{ + restEndpoint("integrate.api.nvidia.com", nvidiaInferenceRules), + restEndpoint("inference-api.nvidia.com", nvidiaInferenceRules), + }, + Binaries: hermesCoreBinaries, + }, + NetworkPolicyKeyGitHub: { + Name: "github", + Endpoints: []*sandboxv1.NetworkEndpoint{ + restEndpointFullAccess("github.com"), + restEndpointFullAccess("api.github.com"), + }, + Binaries: []*sandboxv1.NetworkBinary{ + {Path: "/usr/bin/git"}, + {Path: "/opt/hermes/.venv/bin/python"}, + }, + }, + NetworkPolicyKeyNousResearch: { + Name: "nous_research", + Endpoints: nousEndpoints, + Binaries: hermesCoreBinaries, + }, + NetworkPolicyKeyPyPI: { + Name: "pypi", + Endpoints: []*sandboxv1.NetworkEndpoint{ + restEndpoint("pypi.org", pypiGETRules), + restEndpoint("files.pythonhosted.org", pypiGETRules), + }, + Binaries: []*sandboxv1.NetworkBinary{ + {Path: "/usr/local/bin/pip3"}, + {Path: "/usr/bin/python3*"}, + {Path: "/opt/hermes/.venv/bin/python"}, + }, + }, + } +} + +func hermesTelegramNetworkPolicyRule() *sandboxv1.NetworkPolicyRule { + return &sandboxv1.NetworkPolicyRule{ + Name: "telegram", + Endpoints: []*sandboxv1.NetworkEndpoint{restEndpoint("api.telegram.org", telegramHermesRules)}, + Binaries: hermesMessagingBinaries, + } +} + +func hermesSlackNetworkPolicyRule() *sandboxv1.NetworkPolicyRule { + return &sandboxv1.NetworkPolicyRule{ + Name: "slack", + Endpoints: []*sandboxv1.NetworkEndpoint{ + restEndpointSlack("slack.com"), + restEndpointSlack("api.slack.com"), + restEndpointSlack("hooks.slack.com"), + slackWssEndpoint("wss-primary.slack.com"), + slackWssEndpoint("wss-backup.slack.com"), + }, + Binaries: hermesCoreBinaries, + } +} + +func channelSpecPresent(ch v1alpha2.AgentHarnessChannel) bool { + switch ch.Type { + case v1alpha2.AgentHarnessChannelTypeTelegram: + return ch.Telegram != nil + case v1alpha2.AgentHarnessChannelTypeSlack: + return ch.Slack != nil + default: + return false + } +} + +func sandboxHasChannelType(ah *v1alpha2.AgentHarness, typ v1alpha2.AgentHarnessChannelType) bool { + if ah == nil { + return false + } + for _, ch := range ah.Spec.Channels { + if ch.Type == typ && channelSpecPresent(ch) { + return true + } + } + return false +} + +// ApplyHermesBaselinePolicies adds Hermes fixed network rules plus filesystem / landlock / process policies. +func ApplyHermesBaselinePolicies(net map[string]*sandboxv1.NetworkPolicyRule) (fs *sandboxv1.FilesystemPolicy, landlock *sandboxv1.LandlockPolicy, process *sandboxv1.ProcessPolicy) { + maps.Copy(net, defaultHermesNetworkPolicies()) + return defaultHermesFilesystemPolicy(), defaultHermesLandlockPolicy(), defaultHermesProcessPolicy() +} + +// ApplyChannelNetworkPolicies adds Telegram / Slack egress when channels are configured. +func ApplyChannelNetworkPolicies(ah *v1alpha2.AgentHarness, net map[string]*sandboxv1.NetworkPolicyRule) { + if sandboxHasChannelType(ah, v1alpha2.AgentHarnessChannelTypeTelegram) { + net[NetworkPolicyKeyTelegram] = hermesTelegramNetworkPolicyRule() + } + if sandboxHasChannelType(ah, v1alpha2.AgentHarnessChannelTypeSlack) { + net[NetworkPolicyKeySlack] = hermesSlackNetworkPolicyRule() + } +} + +// SandboxPolicyVersion is OpenShell SandboxPolicy.version for Hermes fragments. +const SandboxPolicyVersion = 1 + +// BaselineHermesSandboxPolicy returns the fixed Hermes baseline (nvidia, github, nous_research, pypi, filesystem, landlock, process). +func BaselineHermesSandboxPolicy() *sandboxv1.SandboxPolicy { + net := map[string]*sandboxv1.NetworkPolicyRule{} + fs, landlock, process := ApplyHermesBaselinePolicies(net) + return &sandboxv1.SandboxPolicy{ + Version: SandboxPolicyVersion, + NetworkPolicies: net, + Filesystem: fs, + Landlock: landlock, + Process: process, + } +} + +// ChannelNetworkPolicyFragment returns Telegram/Slack egress as a network-only policy fragment when channels are configured, or nil. +func ChannelNetworkPolicyFragment(ah *v1alpha2.AgentHarness) *sandboxv1.SandboxPolicy { + if ah == nil { + return nil + } + net := map[string]*sandboxv1.NetworkPolicyRule{} + ApplyChannelNetworkPolicies(ah, net) + if len(net) == 0 { + return nil + } + return &sandboxv1.SandboxPolicy{Version: SandboxPolicyVersion, NetworkPolicies: net} +} + +// AllowedDomainsBinaries returns executables allowed to use kagent_allowed_domains for Hermes harnesses. +func AllowedDomainsBinaries() []*sandboxv1.NetworkBinary { + return []*sandboxv1.NetworkBinary{ + {Path: "/usr/local/bin/hermes"}, + {Path: "/usr/bin/python3*"}, + {Path: "/opt/hermes/.venv/bin/python"}, + {Path: "/usr/bin/curl"}, + {Path: "/usr/bin/git"}, + {Path: "/sandbox/**"}, + } +} diff --git a/go/core/pkg/sandboxbackend/openshell/hermes/policy_test.go b/go/core/pkg/sandboxbackend/openshell/hermes/policy_test.go new file mode 100644 index 0000000000..0e7f504323 --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/hermes/policy_test.go @@ -0,0 +1,86 @@ +package hermes_test + +import ( + "testing" + + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/hermes" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestBaselineHermesSandboxPolicy(t *testing.T) { + pol := hermes.BaselineHermesSandboxPolicy() + require.NotNil(t, pol) + net := pol.GetNetworkPolicies() + require.Contains(t, net, hermes.NetworkPolicyKeyNVIDIA) + require.Contains(t, net, hermes.NetworkPolicyKeyNousResearch) + require.Contains(t, net, hermes.NetworkPolicyKeyPyPI) + require.NotContains(t, net, "clawhub") + + fs := pol.GetFilesystem() + require.True(t, fs.GetIncludeWorkdir()) + require.Contains(t, fs.GetReadWrite(), hermes.HermesConfigDir) + require.Contains(t, fs.GetReadOnly(), "/opt/hermes") +} + +func TestChannelNetworkPolicyFragment_Slack(t *testing.T) { + ah := &v1alpha2.AgentHarness{ + Spec: v1alpha2.AgentHarnessSpec{ + Channels: []v1alpha2.AgentHarnessChannel{ + { + Name: "sl", + Type: v1alpha2.AgentHarnessChannelTypeSlack, + Slack: &v1alpha2.AgentHarnessSlackChannelSpec{ + BotToken: v1alpha2.AgentHarnessChannelCredential{Value: "xoxb-bot"}, + AppToken: v1alpha2.AgentHarnessChannelCredential{Value: "xapp-app"}, + ChannelAccess: v1alpha2.AgentHarnessChannelAccessOpen, + }, + }, + }, + }, + } + frag := hermes.ChannelNetworkPolicyFragment(ah) + require.NotNil(t, frag) + slack := frag.GetNetworkPolicies()[hermes.NetworkPolicyKeySlack] + require.NotNil(t, slack) + var restHosts, wssHosts int + for _, ep := range slack.GetEndpoints() { + switch ep.GetHost() { + case "slack.com", "api.slack.com", "hooks.slack.com": + require.True(t, ep.GetRequestBodyCredentialRewrite()) + restHosts++ + case "wss-primary.slack.com", "wss-backup.slack.com": + require.Equal(t, "websocket", ep.GetProtocol()) + require.True(t, ep.GetWebsocketCredentialRewrite()) + wssHosts++ + } + } + require.Equal(t, 3, restHosts) + require.Equal(t, 2, wssHosts) +} + +func TestChannelNetworkPolicyFragment_Telegram(t *testing.T) { + ah := &v1alpha2.AgentHarness{ + Spec: v1alpha2.AgentHarnessSpec{ + Channels: []v1alpha2.AgentHarnessChannel{ + { + Name: "tg", + Type: v1alpha2.AgentHarnessChannelTypeTelegram, + Telegram: &v1alpha2.AgentHarnessTelegramChannelSpec{ + BotToken: v1alpha2.AgentHarnessChannelCredential{Value: "tok"}, + }, + }, + }, + }, + } + frag := hermes.ChannelNetworkPolicyFragment(ah) + require.NotNil(t, frag) + require.Contains(t, frag.GetNetworkPolicies(), hermes.NetworkPolicyKeyTelegram) +} + +func TestIsHermesSandboxBackend(t *testing.T) { + require.True(t, hermes.IsHermesSandboxBackend(v1alpha2.AgentHarnessBackendHermes)) + require.False(t, hermes.IsHermesSandboxBackend(v1alpha2.AgentHarnessBackendOpenClaw)) + _ = metav1.ObjectMeta{} +} diff --git a/go/core/pkg/sandboxbackend/openshell/hermes/ssh.go b/go/core/pkg/sandboxbackend/openshell/hermes/ssh.go new file mode 100644 index 0000000000..e031fe9b43 --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/hermes/ssh.go @@ -0,0 +1,8 @@ +package hermes + +// DefaultSSHLaunchCommand is the interactive CLI started when connecting to a +// Hermes harness sandbox via the UI terminal (unless plain shell is requested). +func DefaultSSHLaunchCommand() string { + // Hermes reads config from HERMES_HOME; bootstrap writes to /sandbox/.hermes. + return "cd " + HermesConfigDir + " && exec hermes" +} diff --git a/go/core/pkg/sandboxbackend/openshell/hermes/ssh_test.go b/go/core/pkg/sandboxbackend/openshell/hermes/ssh_test.go new file mode 100644 index 0000000000..80b05f1234 --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/hermes/ssh_test.go @@ -0,0 +1,12 @@ +package hermes_test + +import ( + "testing" + + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/hermes" + "github.com/stretchr/testify/require" +) + +func TestDefaultSSHLaunchCommand(t *testing.T) { + require.Equal(t, "cd /sandbox/.hermes && exec hermes", hermes.DefaultSSHLaunchCommand()) +} diff --git a/go/core/pkg/sandboxbackend/openshell/messaging_providers.go b/go/core/pkg/sandboxbackend/openshell/messaging_providers.go new file mode 100644 index 0000000000..cb2a2f64cc --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/messaging_providers.go @@ -0,0 +1,54 @@ +package openshell + +import ( + "context" + "fmt" + + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/channels" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" +) + +// UpsertMessagingProviders registers OpenShell gateway providers for harness channel credentials. +// Returns provider names to attach on CreateSandbox.spec.providers. +func UpsertMessagingProviders( + ctx context.Context, + oc *OpenShellClients, + kube client.Client, + ah *v1alpha2.AgentHarness, +) ([]string, error) { + if ah == nil || len(ah.Spec.Channels) == 0 { + return nil, nil + } + if oc == nil || oc.OpenShell == nil { + return nil, fmt.Errorf("openshell: OpenShell client is required for messaging providers") + } + if kube == nil { + return nil, fmt.Errorf("openshell: Kubernetes client is required for messaging providers") + } + + resolved, err := channels.Resolve(ctx, kube, ah.Namespace, ah.Spec.Channels) + if err != nil { + return nil, err + } + sandboxName := agentHarnessGatewayName(ah) + msgDefs := channels.MessagingProviderDefs(sandboxName, resolved.Secrets, resolved) + if len(msgDefs) == 0 { + return nil, nil + } + gwDefs := messagingDefsToGateway(msgDefs) + + names := make([]string, 0, len(gwDefs)) + if err := UpsertGatewayProviders(ctx, oc.OpenShell, gwDefs); err != nil { + return nil, err + } + for _, d := range gwDefs { + names = append(names, d.Name) + } + ctrllog.FromContext(ctx).Info("upserted messaging providers", + "agentHarness", ah.Namespace+"/"+ah.Name, + "providers", names, + ) + return names, nil +} diff --git a/go/core/pkg/sandboxbackend/openshell/openclaw.go b/go/core/pkg/sandboxbackend/openshell/openclaw.go index 1a4138977c..9f95a407a5 100644 --- a/go/core/pkg/sandboxbackend/openshell/openclaw.go +++ b/go/core/pkg/sandboxbackend/openshell/openclaw.go @@ -16,9 +16,6 @@ import ( ctrllog "sigs.k8s.io/controller-runtime/pkg/log" ) -// NemoclawSandboxBaseImage is the container image used for OpenClaw/NemoClaw harness sandboxes. -const NemoclawSandboxBaseImage = "ghcr.io/kagent-dev/nemoclaw/sandbox-base:2026.5.4" - // ClawBackend implements AsyncBackend and PostReadyBackend for OpenClaw- and // NemoClaw-typed AgentHarness resources: sync ModelConfig to the OpenShell control plane before create, // fixed sandbox image, and post-ready OpenClaw bootstrap when modelConfigRef is set. @@ -36,15 +33,12 @@ func NewOpenClawBackend(kubeClient client.Client, clients *OpenShellClients, cfg agentHarnessOpenShellBackend: newAgentHarnessOpenShellBackend( kubeClient, clients, cfg, recorder, v1alpha2.AgentHarnessBackendOpenClaw, - buildClawCreateRequest, ), } } // EnsureAgentHarness is the OpenClaw/NemoClaw EnsureAgentHarness flow: idempotent gateway lookup, // then translateModelConfig (apply ModelConfigRef onto the gateway) before CreateSandbox. -// Future backends (e.g. Hermes) follow the same pattern: findExistingSandbox + own translation step -// + createSandbox, with no callback fields. func (b *ClawBackend) EnsureAgentHarness(ctx context.Context, ah *v1alpha2.AgentHarness) (sandboxbackend.EnsureResult, error) { if ah == nil { return sandboxbackend.EnsureResult{}, fmt.Errorf("AgentHarness is required") @@ -56,10 +50,7 @@ func (b *ClawBackend) EnsureAgentHarness(ctx context.Context, ah *v1alpha2.Agent if res, found, err := b.findExistingSandbox(ctx, ah); err != nil || found { return res, err } - if err := translateModelConfig(ctx, ah, b.kubeClient, b.clients); err != nil { - return sandboxbackend.EnsureResult{}, err - } - return b.createSandbox(ctx, ah) + return b.ensureAgentHarnessSandbox(ctx, ah, buildClawCreateRequest) } const defaultOpenclawGatewayPort = 18800 @@ -88,6 +79,10 @@ func (b *ClawBackend) OnAgentHarnessReady(ctx context.Context, ah *v1alpha2.Agen return fmt.Errorf("get ModelConfig: %w", err) } + if _, err := UpsertMessagingProviders(ctx, b.clients, b.kubeClient, ah); err != nil { + return fmt.Errorf("upsert messaging providers: %w", err) + } + providerRecord := openclaw.GatewayProviderRecordName(mc.Spec.Provider) gwPort := defaultOpenclawGatewayPort token := b.cfg.Token @@ -135,14 +130,15 @@ func (b *ClawBackend) OnAgentHarnessReady(ctx context.Context, ah *v1alpha2.Agen return nil } -func buildClawCreateRequest(ah *v1alpha2.AgentHarness) (*openshellv1.CreateSandboxRequest, []string) { +func buildClawCreateRequest(ah *v1alpha2.AgentHarness, messagingProviders []string) (*openshellv1.CreateSandboxRequest, []string) { req, unsupported := buildAgentHarnessOpenshellCreateRequest(ah) if req.GetSpec().GetTemplate() == nil { req.Spec.Template = &openshellv1.SandboxTemplate{} } if ah.Spec.Image == "" { - req.Spec.Template.Image = NemoclawSandboxBaseImage + req.Spec.Template.Image = openclaw.NemoclawSandboxBaseImage } + attachMessagingProviders(req, messagingProviders) return req, unsupported } diff --git a/go/core/pkg/sandboxbackend/openshell/openclaw/bootstrap_test.go b/go/core/pkg/sandboxbackend/openshell/openclaw/bootstrap_test.go index 3ffd74b8de..297406d95f 100644 --- a/go/core/pkg/sandboxbackend/openshell/openclaw/bootstrap_test.go +++ b/go/core/pkg/sandboxbackend/openshell/openclaw/bootstrap_test.go @@ -95,7 +95,7 @@ func TestBuildBootstrapJSON_OpenAIAndTelegram(t *testing.T) { raw, env, err := openclaw.BuildBootstrapJSON(context.Background(), kube, ns, sbx, mc, 18800) require.NoError(t, err) require.Equal(t, "sk-test", env["OPENAI_API_KEY"]) - require.Equal(t, "telegram-bot-token", env["KAGENT_SB_CH_TG1_TELEGRAM_BOT"]) + require.Equal(t, "telegram-bot-token", env["TELEGRAM_BOT_TOKEN"]) var root map[string]any require.NoError(t, json.Unmarshal(raw, &root)) @@ -118,11 +118,11 @@ func TestBuildBootstrapJSON_OpenAIAndTelegram(t *testing.T) { kagent := secProvs["kagent"].(map[string]any) require.Equal(t, "env", kagent["source"]) al := kagent["allowlist"].([]any) - require.ElementsMatch(t, []any{"KAGENT_SB_CH_TG1_TELEGRAM_BOT", "OPENAI_API_KEY"}, al) + require.ElementsMatch(t, []any{"TELEGRAM_BOT_TOKEN", "OPENAI_API_KEY"}, al) ch := root["channels"].(map[string]any) require.Contains(t, ch, "telegram") tg := ch["telegram"].(map[string]any) tgAcc := tg["accounts"].(map[string]any) tg1 := tgAcc["tg1"].(map[string]any) - require.Equal(t, "telegram-bot-token", tg1["botToken"]) + require.Equal(t, "openshell:resolve:env:TELEGRAM_BOT_TOKEN", tg1["botToken"]) } diff --git a/go/core/pkg/sandboxbackend/openshell/openclaw/channels.go b/go/core/pkg/sandboxbackend/openshell/openclaw/channels.go index 00edaf82e3..f29835816b 100644 --- a/go/core/pkg/sandboxbackend/openshell/openclaw/channels.go +++ b/go/core/pkg/sandboxbackend/openshell/openclaw/channels.go @@ -2,209 +2,93 @@ package openclaw import ( "context" - "fmt" - "strings" + "maps" "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/channels" "sigs.k8s.io/controller-runtime/pkg/client" ) type harnessChannels struct { - telegram map[string]telegramAccount - tgDef string - - slack map[string]slackAccount - slDef string - - slackRootPolicy v1alpha2.AgentHarnessChannelAccess - slackSeen bool -} - -func newHarnessChannels() *harnessChannels { - return &harnessChannels{ - telegram: make(map[string]telegramAccount), - slack: make(map[string]slackAccount), - } + resolved *channels.Resolved } -func accumulateHarnessChannels(ctx context.Context, kube client.Client, namespace string, channels []v1alpha2.AgentHarnessChannel, env map[string]string) (*harnessChannels, error) { - a := newHarnessChannels() - for _, ch := range channels { - switch ch.Type { - case v1alpha2.AgentHarnessChannelTypeTelegram: - if err := a.addTelegram(ctx, kube, namespace, ch, env); err != nil { - return nil, err - } - case v1alpha2.AgentHarnessChannelTypeSlack: - if err := a.addSlack(ctx, kube, namespace, ch, env); err != nil { - return nil, err - } - default: - return nil, fmt.Errorf("channel %q: unsupported type %q", ch.Name, ch.Type) - } +func accumulateHarnessChannels(ctx context.Context, kube client.Client, namespace string, specChannels []v1alpha2.AgentHarnessChannel, env map[string]string) (*harnessChannels, error) { + resolved, err := channels.Resolve(ctx, kube, namespace, specChannels) + if err != nil { + return nil, err } - return a, nil + maps.Copy(env, resolved.Secrets) + return &harnessChannels{resolved: resolved}, nil } func (a *harnessChannels) channelsJSON() *channelsConfig { - if len(a.telegram) == 0 && len(a.slack) == 0 { + if a == nil || a.resolved == nil { + return nil + } + r := a.resolved + if len(r.Telegram) == 0 && len(r.Slack) == 0 { return nil } out := &channelsConfig{} - if len(a.telegram) > 0 { + if len(r.Telegram) > 0 { + accounts := make(map[string]telegramAccount, len(r.Telegram)) + var def string + for _, tg := range r.Telegram { + acc := telegramAccount{ + Name: tg.Name, + Enabled: true, + BotToken: openshellResolveEnv(channels.EnvTelegramBotToken), + } + if len(tg.AllowFrom) > 0 { + acc.DMPolicy = "allowlist" + acc.AllowFrom = tg.AllowFrom + } else { + acc.DMPolicy = "pairing" + } + accounts[tg.Name] = acc + if def == "" { + def = tg.Name + } + } out.Telegram = &telegramBundle{ Enabled: true, - Accounts: a.telegram, - DefaultAccount: a.tgDef, + Accounts: accounts, + DefaultAccount: def, } } - if len(a.slack) > 0 { + if len(r.Slack) > 0 { + accounts := make(map[string]slackAccount, len(r.Slack)) + var def string + for _, sl := range r.Slack { + acc := slackAccount{ + Name: sl.Name, + Enabled: true, + Mode: "socket", + BotToken: channels.SlackBotTokenPlaceholder(), + AppToken: channels.SlackAppTokenPlaceholder(), + UserTokenReadOnly: true, + GroupPolicy: string(sl.ChannelAccess), + Capabilities: slackCaps{ + InteractiveReplies: sl.InteractiveReplies, + }, + } + if chans := sl.AllowlistChannels; len(chans) > 0 { + acc.DM = &groupDM{GroupEnabled: true, GroupChannels: chans} + } + accounts[sl.Name] = acc + if def == "" { + def = sl.Name + } + } out.Slack = &slackBundle{ Enabled: true, Mode: "socket", WebhookPath: "/slack/events", UserTokenReadOnly: true, - GroupPolicy: string(a.slackRootPolicy), - Accounts: a.slack, - DefaultAccount: a.slDef, - } - } - return out -} - -func (a *harnessChannels) addTelegram(ctx context.Context, kube client.Client, namespace string, ch v1alpha2.AgentHarnessChannel, env map[string]string) error { - spec := ch.Telegram - if spec == nil { - return fmt.Errorf("channel %q: telegram spec is required", ch.Name) - } - botEnv := channelSecretEnvVar(ch.Name, "TELEGRAM_BOT") - if err := putChannelCredential(ctx, kube, namespace, spec.BotToken, botEnv, env); err != nil { - return fmt.Errorf("channel %q telegram bot token: %w", ch.Name, err) - } - botTok, err := resolvedChannelSecret(env, botEnv) - if err != nil { - return fmt.Errorf("channel %q telegram %w", ch.Name, err) - } - allowFrom, err := telegramAllowFrom(ctx, kube, namespace, spec) - if err != nil { - return fmt.Errorf("channel %q telegram allowlist: %w", ch.Name, err) - } - acc := telegramAccount{ - Name: ch.Name, - Enabled: true, - BotToken: botTok, - } - if len(allowFrom) > 0 { - acc.DMPolicy = "allowlist" - acc.AllowFrom = allowFrom - } else { - acc.DMPolicy = "pairing" - } - a.telegram[ch.Name] = acc - if a.tgDef == "" { - a.tgDef = ch.Name - } - return nil -} - -func (a *harnessChannels) addSlack(ctx context.Context, kube client.Client, namespace string, ch v1alpha2.AgentHarnessChannel, env map[string]string) error { - spec := ch.Slack - if spec == nil { - return fmt.Errorf("channel %q: slack spec is required", ch.Name) - } - botEnv := channelSecretEnvVar(ch.Name, "SLACK_BOT") - appEnv := channelSecretEnvVar(ch.Name, "SLACK_APP") - if err := putChannelCredential(ctx, kube, namespace, spec.BotToken, botEnv, env); err != nil { - return fmt.Errorf("channel %q slack bot token: %w", ch.Name, err) - } - if err := putChannelCredential(ctx, kube, namespace, spec.AppToken, appEnv, env); err != nil { - return fmt.Errorf("channel %q slack app token: %w", ch.Name, err) - } - slackBotTok, err := resolvedChannelSecret(env, botEnv) - if err != nil { - return fmt.Errorf("channel %q slack %w", ch.Name, err) - } - slackAppTok, err := resolvedChannelSecret(env, appEnv) - if err != nil { - return fmt.Errorf("channel %q slack %w", ch.Name, err) - } - acc := slackAccount{ - Name: ch.Name, - Enabled: true, - Mode: "socket", - BotToken: slackBotTok, - AppToken: slackAppTok, - UserTokenReadOnly: true, - GroupPolicy: string(spec.ChannelAccess), - Capabilities: slackCaps{ - InteractiveReplies: slackInteractiveReplies(spec), - }, - } - if chans := trimNonEmptyStrings(spec.AllowlistChannels); len(chans) > 0 { - acc.DM = &groupDM{GroupEnabled: true, GroupChannels: chans} - } - a.slack[ch.Name] = acc - if a.slDef == "" { - a.slDef = ch.Name - } - if !a.slackSeen { - a.slackRootPolicy = spec.ChannelAccess - a.slackSeen = true - } - return nil -} - -func slackInteractiveReplies(spec *v1alpha2.AgentHarnessSlackChannelSpec) bool { - if spec.InteractiveReplies == nil { - return true - } - return *spec.InteractiveReplies -} - -func splitAllowedList(raw string) []string { - raw = strings.TrimSpace(raw) - if raw == "" { - return nil - } - var out []string - for _, part := range strings.FieldsFunc(raw, func(r rune) bool { - return r == ',' || r == '\n' || r == ';' - }) { - s := strings.TrimSpace(part) - if s != "" { - out = append(out, s) - } - } - return out -} - -func telegramAllowFrom(ctx context.Context, kube client.Client, namespace string, spec *v1alpha2.AgentHarnessTelegramChannelSpec) ([]string, error) { - if len(spec.AllowedUserIDs) > 0 { - out := make([]string, 0, len(spec.AllowedUserIDs)) - for _, id := range spec.AllowedUserIDs { - s := strings.TrimSpace(id) - if s != "" { - out = append(out, s) - } - } - return out, nil - } - if spec.AllowedUserIDsFrom != nil { - raw, err := spec.AllowedUserIDsFrom.Resolve(ctx, kube, namespace) - if err != nil { - return nil, fmt.Errorf("resolve allowedUserIDsFrom: %w", err) - } - return splitAllowedList(raw), nil - } - return nil, nil -} - -func trimNonEmptyStrings(ss []string) []string { - out := make([]string, 0, len(ss)) - for _, s := range ss { - s = strings.TrimSpace(s) - if s != "" { - out = append(out, s) + GroupPolicy: string(r.SlackRootGroupPolicy()), + Accounts: accounts, + DefaultAccount: def, } } return out diff --git a/go/core/pkg/sandboxbackend/openshell/openclaw/constants.go b/go/core/pkg/sandboxbackend/openshell/openclaw/constants.go index 35a6abb998..dd0f98cdc8 100644 --- a/go/core/pkg/sandboxbackend/openshell/openclaw/constants.go +++ b/go/core/pkg/sandboxbackend/openshell/openclaw/constants.go @@ -1,6 +1,9 @@ package openclaw const ( + // NemoclawSandboxBaseImage is the default OpenShell VM image for OpenClaw/NemoClaw harnesses. + NemoclawSandboxBaseImage = "ghcr.io/kagent-dev/nemoclaw/sandbox-base:2026.5.4" + // bootstrapSecretProviderID is the secrets.providers key written into openclaw.json. bootstrapSecretProviderID = "kagent" diff --git a/go/core/pkg/sandboxbackend/openshell/openclaw/credentials.go b/go/core/pkg/sandboxbackend/openshell/openclaw/credentials.go deleted file mode 100644 index 02a47289e7..0000000000 --- a/go/core/pkg/sandboxbackend/openshell/openclaw/credentials.go +++ /dev/null @@ -1,58 +0,0 @@ -package openclaw - -import ( - "context" - "fmt" - "strings" - - "github.com/kagent-dev/kagent/go/api/v1alpha2" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func sandboxChannelEnvSuffix(name string) string { - var b strings.Builder - for _, r := range strings.ToUpper(strings.TrimSpace(name)) { - switch { - case r >= 'A' && r <= 'Z', r >= '0' && r <= '9': - b.WriteRune(r) - default: - b.WriteByte('_') - } - } - s := strings.Trim(b.String(), "_") - if s == "" { - return "CH" - } - return s -} - -func channelSecretEnvVar(channelName, tokenRole string) string { - return fmt.Sprintf("KAGENT_SB_CH_%s_%s", sandboxChannelEnvSuffix(channelName), tokenRole) -} - -func putChannelCredential(ctx context.Context, kube client.Client, namespace string, cred v1alpha2.AgentHarnessChannelCredential, envKey string, env map[string]string) error { - if strings.TrimSpace(cred.Value) != "" { - env[envKey] = strings.TrimSpace(cred.Value) - return nil - } - if cred.ValueFrom == nil { - return fmt.Errorf("channel credential requires value or valueFrom") - } - v, err := cred.ValueFrom.Resolve(ctx, kube, namespace) - if err != nil { - return fmt.Errorf("resolve credential %s: %w", envKey, err) - } - env[envKey] = v - return nil -} - -// resolvedChannelSecret returns the plaintext value putChannelCredential stored in env. -// Channel configs must use literals in openclaw.json: OpenClaw's Bot API clients build URLs from botToken before -// openshell:resolve:env: placeholders are expanded. -func resolvedChannelSecret(env map[string]string, envKey string) (string, error) { - v := strings.TrimSpace(env[envKey]) - if v == "" { - return "", fmt.Errorf("credential %s is missing or empty after resolve", envKey) - } - return v, nil -} diff --git a/go/core/pkg/sandboxbackend/openshell/openclaw/defaults.go b/go/core/pkg/sandboxbackend/openshell/openclaw/defaults.go new file mode 100644 index 0000000000..15384730ad --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/openclaw/defaults.go @@ -0,0 +1,25 @@ +package openclaw + +import ( + "fmt" + "strings" + + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/channels" +) + +// DefaultAPIKeyEnvVar is the environment variable name used for the model provider API key in the sandbox. +func DefaultAPIKeyEnvVar(provider v1alpha2.ModelProvider) string { + return fmt.Sprintf("%s_API_KEY", strings.ToUpper(string(provider))) +} + +// DefaultSSHLaunchCommand is the interactive CLI started when connecting to an +// OpenClaw or NemoClaw harness sandbox via the UI terminal (unless plain shell is requested). +func DefaultSSHLaunchCommand() string { + return "openclaw tui" +} + +// openshellResolveEnv matches OpenClaw onboard placeholders for OpenShell L7 credential rewrite. +func openshellResolveEnv(envVar string) string { + return channels.ResolveEnvPlaceholder(envVar) +} diff --git a/go/core/pkg/sandboxbackend/openshell/openclaw/policy.go b/go/core/pkg/sandboxbackend/openshell/openclaw/policy.go index 356359199b..b3d6632b04 100644 --- a/go/core/pkg/sandboxbackend/openshell/openclaw/policy.go +++ b/go/core/pkg/sandboxbackend/openshell/openclaw/policy.go @@ -60,6 +60,10 @@ var ( {Allow: &sandboxv1.L7Allow{Method: "GET", Path: "/**"}}, {Allow: &sandboxv1.L7Allow{Method: "POST", Path: "/**"}}, } + slackWssRules = []*sandboxv1.L7Rule{ + {Allow: &sandboxv1.L7Allow{Method: "GET", Path: "/**"}}, + {Allow: &sandboxv1.L7Allow{Method: "WEBSOCKET_TEXT", Path: "/**"}}, + } ) // restNetworkEndpoint is HTTPS:443 with protocol rest + enforce and explicit L7 rules (OpenShell policy schema). @@ -73,6 +77,23 @@ func restNetworkEndpoint(host string, rules []*sandboxv1.L7Rule) *sandboxv1.Netw } } +func restSlackNetworkEndpoint(host string) *sandboxv1.NetworkEndpoint { + ep := restNetworkEndpoint(host, slackRESTRules) + ep.RequestBodyCredentialRewrite = true + return ep +} + +func slackWssNetworkEndpoint(host string) *sandboxv1.NetworkEndpoint { + return &sandboxv1.NetworkEndpoint{ + Host: host, + Ports: []uint32{443}, + Protocol: "websocket", + Enforcement: endpointEnforcement, + WebsocketCredentialRewrite: true, + Rules: slackWssRules, + } +} + // messengerChannelNodeBinaries for Telegram / Slack OpenClaw channel egress (Node runtime). var messengerChannelNodeBinaries = []*sandboxv1.NetworkBinary{ {Path: "/usr/local/bin/node"}, @@ -152,11 +173,11 @@ func slackNetworkPolicyRule() *sandboxv1.NetworkPolicyRule { return &sandboxv1.NetworkPolicyRule{ Name: "slack", Endpoints: []*sandboxv1.NetworkEndpoint{ - restNetworkEndpoint("slack.com", slackRESTRules), - restNetworkEndpoint("api.slack.com", slackRESTRules), - restNetworkEndpoint("hooks.slack.com", slackRESTRules), - wssTunnelEndpoint("wss-primary.slack.com"), - wssTunnelEndpoint("wss-backup.slack.com"), + restSlackNetworkEndpoint("slack.com"), + restSlackNetworkEndpoint("api.slack.com"), + restSlackNetworkEndpoint("hooks.slack.com"), + slackWssNetworkEndpoint("wss-primary.slack.com"), + slackWssNetworkEndpoint("wss-backup.slack.com"), }, Binaries: messengerChannelNodeBinaries, } diff --git a/go/core/pkg/sandboxbackend/openshell/openclaw/resolve.go b/go/core/pkg/sandboxbackend/openshell/openclaw/resolve.go deleted file mode 100644 index d40b11d45d..0000000000 --- a/go/core/pkg/sandboxbackend/openshell/openclaw/resolve.go +++ /dev/null @@ -1,19 +0,0 @@ -package openclaw - -import ( - "fmt" - "strings" - - "github.com/kagent-dev/kagent/go/api/v1alpha2" -) - -// openshellResolveEnv matches OpenClaw onboard --custom-api-key: credentials resolve via -// OpenShell’s env path inside the sandbox (same as literal OPENAI_API_KEY etc. still injected by kagent on ExecSandbox). -func openshellResolveEnv(envVar string) string { - return "openshell:resolve:env:" + envVar -} - -// DefaultAPIKeyEnvVar is the environment variable name used for the model provider API key in the sandbox. -func DefaultAPIKeyEnvVar(provider v1alpha2.ModelProvider) string { - return fmt.Sprintf("%s_API_KEY", strings.ToUpper(string(provider))) -} diff --git a/go/core/pkg/sandboxbackend/openshell/openclaw/ssh_test.go b/go/core/pkg/sandboxbackend/openshell/openclaw/ssh_test.go new file mode 100644 index 0000000000..a2f5d32aef --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/openclaw/ssh_test.go @@ -0,0 +1,12 @@ +package openclaw_test + +import ( + "testing" + + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/openclaw" + "github.com/stretchr/testify/require" +) + +func TestDefaultSSHLaunchCommand(t *testing.T) { + require.Equal(t, "openclaw tui", openclaw.DefaultSSHLaunchCommand()) +} diff --git a/go/core/pkg/sandboxbackend/openshell/openshell.go b/go/core/pkg/sandboxbackend/openshell/openshell.go index 76411476e3..9802b1b9e7 100644 --- a/go/core/pkg/sandboxbackend/openshell/openshell.go +++ b/go/core/pkg/sandboxbackend/openshell/openshell.go @@ -4,7 +4,7 @@ // Use Dial to obtain OpenShellClients (shared connection for openshell.v1.OpenShell // and openshell.inference.v1.Inference). // -// • NewOpenClawBackend — pin the sandbox image to NemoclawSandboxBaseImage, translateModelConfig +// • NewOpenClawBackend — pin the sandbox image to openclaw.NemoclawSandboxBaseImage, translateModelConfig // when modelConfigRef is set, run OpenClaw bootstrap after Ready. The same instance // is registered for spec.backend=openclaw and nemoclaw (see app wiring). // @@ -30,7 +30,6 @@ type agentHarnessOpenShellBackend struct { *AgentHarnessOpenShellClient kubeClient client.Client backendName v1alpha2.AgentHarnessBackendType - buildCreate func(*v1alpha2.AgentHarness) (*openshellv1.CreateSandboxRequest, []string) } func newAgentHarnessOpenShellBackend( @@ -39,13 +38,11 @@ func newAgentHarnessOpenShellBackend( cfg Config, recorder record.EventRecorder, name v1alpha2.AgentHarnessBackendType, - build func(*v1alpha2.AgentHarness) (*openshellv1.CreateSandboxRequest, []string), ) *agentHarnessOpenShellBackend { return &agentHarnessOpenShellBackend{ AgentHarnessOpenShellClient: newAgentHarnessOpenShellClient(clients, cfg, recorder), kubeClient: kubeClient, backendName: name, - buildCreate: build, } } @@ -79,14 +76,6 @@ func (b *agentHarnessOpenShellBackend) findExistingSandbox(ctx context.Context, return sandboxbackend.EnsureResult{}, false, nil } -// createSandbox builds the gateway create request via b.buildCreate and submits it. Callers must -// already have applied CallCtx and withAuth to ctx and confirmed via findExistingSandbox that no -// sandbox exists yet. -func (b *agentHarnessOpenShellBackend) createSandbox(ctx context.Context, ah *v1alpha2.AgentHarness) (sandboxbackend.EnsureResult, error) { - req, unsupported := b.buildCreate(ah) - return b.CreateAgentHarnessSandbox(ctx, ah, req, unsupported) -} - // GetStatus implements AsyncBackend. func (b *agentHarnessOpenShellBackend) GetStatus(ctx context.Context, h sandboxbackend.Handle) (metav1.ConditionStatus, string, string) { return b.GetSandboxStatus(ctx, h) diff --git a/go/core/pkg/sandboxbackend/openshell/openshell_test.go b/go/core/pkg/sandboxbackend/openshell/openshell_test.go index 957a8bf3dc..4df8266635 100644 --- a/go/core/pkg/sandboxbackend/openshell/openshell_test.go +++ b/go/core/pkg/sandboxbackend/openshell/openshell_test.go @@ -13,6 +13,7 @@ import ( openshellv1 "github.com/kagent-dev/kagent/go/api/openshell/gen/openshellv1" "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend" + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/openclaw" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -30,6 +31,7 @@ type fakeGateway struct { mu sync.Mutex sandboxes map[string]*openshellv1.Sandbox + providers map[string]*datamodelv1.Provider createErr error getErr error deleteErr error @@ -92,25 +94,48 @@ func (f *fakeGateway) DeleteSandbox(_ context.Context, req *openshellv1.DeleteSa } func (f *fakeGateway) CreateProvider(_ context.Context, req *openshellv1.CreateProviderRequest) (*openshellv1.ProviderResponse, error) { - meta := req.GetProvider().GetMetadata() + f.mu.Lock() + defer f.mu.Unlock() + if f.providers == nil { + f.providers = map[string]*datamodelv1.Provider{} + } + p := req.GetProvider() name := "" - if meta != nil { - name = meta.GetName() + if p.GetMetadata() != nil { + name = p.GetMetadata().GetName() } - return &openshellv1.ProviderResponse{ - Provider: &datamodelv1.Provider{ - Metadata: &datamodelv1.ObjectMeta{Id: "fake-provider-id", Name: name}, - Type: req.GetProvider().GetType(), - }, - }, nil + stored := &datamodelv1.Provider{ + Metadata: &datamodelv1.ObjectMeta{Id: "fake-provider-id", Name: name}, + Type: p.GetType(), + Credentials: p.GetCredentials(), + } + f.providers[name] = stored + return &openshellv1.ProviderResponse{Provider: stored}, nil } -func (f *fakeGateway) GetProvider(_ context.Context, _ *openshellv1.GetProviderRequest) (*openshellv1.ProviderResponse, error) { - return nil, status.Error(codes.NotFound, "provider not found") +func (f *fakeGateway) GetProvider(_ context.Context, req *openshellv1.GetProviderRequest) (*openshellv1.ProviderResponse, error) { + f.mu.Lock() + defer f.mu.Unlock() + p, ok := f.providers[req.GetName()] + if !ok { + return nil, status.Error(codes.NotFound, "provider not found") + } + return &openshellv1.ProviderResponse{Provider: p}, nil } func (f *fakeGateway) UpdateProvider(_ context.Context, req *openshellv1.UpdateProviderRequest) (*openshellv1.ProviderResponse, error) { - return &openshellv1.ProviderResponse{Provider: req.GetProvider()}, nil + f.mu.Lock() + defer f.mu.Unlock() + if f.providers == nil { + f.providers = map[string]*datamodelv1.Provider{} + } + p := req.GetProvider() + name := "" + if p.GetMetadata() != nil { + name = p.GetMetadata().GetName() + } + f.providers[name] = p + return &openshellv1.ProviderResponse{Provider: p}, nil } func (f *fakeGateway) ExecSandbox(req *openshellv1.ExecSandboxRequest, stream grpc.ServerStreamingServer[openshellv1.ExecSandboxEvent]) error { @@ -422,5 +447,59 @@ func TestEnsureSandbox_Claw_PinsNemoclawBaseImage(t *testing.T) { defer fg.mu.Unlock() sb := fg.sandboxes["ns1-a1"] require.NotNil(t, sb) - require.Equal(t, NemoclawSandboxBaseImage, sb.GetSpec().GetTemplate().GetImage()) + require.Equal(t, openclaw.NemoclawSandboxBaseImage, sb.GetSpec().GetTemplate().GetImage()) +} + +func TestUpsertMessagingProviders(t *testing.T) { + ns := "default" + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "tg", Namespace: ns}, + Data: map[string][]byte{"token": []byte("tg-secret")}, + } + kube := fake.NewClientBuilder().WithScheme(testScheme(t)).WithObjects(secret).Build() + + ah := &v1alpha2.AgentHarness{ + ObjectMeta: metav1.ObjectMeta{Name: "hermes1", Namespace: ns}, + Spec: v1alpha2.AgentHarnessSpec{ + Channels: []v1alpha2.AgentHarnessChannel{ + { + Name: "tg", + Type: v1alpha2.AgentHarnessChannelTypeTelegram, + Telegram: &v1alpha2.AgentHarnessTelegramChannelSpec{ + BotToken: v1alpha2.AgentHarnessChannelCredential{ + ValueFrom: &v1alpha2.ValueSource{ + Type: v1alpha2.SecretValueSource, + Name: "tg", + Key: "token", + }, + }, + }, + }, + }, + }, + } + + c, _, fg, _, cleanup := startFake(t) + defer cleanup() + clients := &OpenShellClients{OpenShell: c} + + names, err := UpsertMessagingProviders(context.Background(), clients, kube, ah) + require.NoError(t, err) + require.Equal(t, []string{"default-hermes1-telegram-bridge"}, names) + + fg.mu.Lock() + defer fg.mu.Unlock() + p := fg.providers["default-hermes1-telegram-bridge"] + require.NotNil(t, p) + require.Equal(t, "tg-secret", p.GetCredentials()["TELEGRAM_BOT_TOKEN"]) + require.Equal(t, "generic", p.GetType()) +} + +func TestBuildHermesCreateRequest_AttachesMessagingProviders(t *testing.T) { + ah := &v1alpha2.AgentHarness{ + ObjectMeta: metav1.ObjectMeta{Name: "a1", Namespace: "ns1"}, + Spec: v1alpha2.AgentHarnessSpec{Backend: v1alpha2.AgentHarnessBackendHermes}, + } + req, _ := buildHermesCreateRequest(ah, []string{"ns1-a1-telegram-bridge"}) + require.Contains(t, req.GetSpec().GetProviders(), "ns1-a1-telegram-bridge") } diff --git a/go/core/pkg/sandboxbackend/openshell/policy.go b/go/core/pkg/sandboxbackend/openshell/policy.go index f06890eb3b..b01b3a20e5 100644 --- a/go/core/pkg/sandboxbackend/openshell/policy.go +++ b/go/core/pkg/sandboxbackend/openshell/policy.go @@ -6,6 +6,7 @@ import ( sandboxv1 "github.com/kagent-dev/kagent/go/api/openshell/gen/sandboxv1" "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend" + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/hermes" "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/openclaw" "google.golang.org/protobuf/proto" ) @@ -86,11 +87,22 @@ func applyAllowedDomainsPolicy(sbx *v1alpha2.AgentHarness, net map[string]*sandb if sbx != nil && openclaw.IsClawSandboxBackend(sbx.Spec.Backend) { domainList = openclaw.OmitNPMPresetRegistryHosts(domainList) } - if rule := allowedDomainsNetworkPolicyRule(domainList); rule != nil { + if rule := allowedDomainsNetworkPolicyRuleForHarness(sbx, domainList); rule != nil { net[kagentAllowedDomainsNetworkPolicyKey] = rule } } +func allowedDomainsNetworkPolicyRuleForHarness(ah *v1alpha2.AgentHarness, domains []string) *sandboxv1.NetworkPolicyRule { + rule := allowedDomainsNetworkPolicyRule(domains) + if rule == nil { + return nil + } + if ah != nil && hermes.IsHermesSandboxBackend(ah.Spec.Backend) { + rule.Binaries = hermes.AllowedDomainsBinaries() + } + return rule +} + // mergeOpenShellSandboxPolicies merges two OpenShell SandboxPolicy fragments for AgentHarness provisioning. // Network policy rules: overlay keys replace base keys. Filesystem, landlock, and process: overlay replaces // base only when the overlay sets a non-nil value (for future user-defined CRD policy). @@ -149,9 +161,12 @@ func openShellSandboxPolicyForAgentHarness(ah *v1alpha2.AgentHarness) *sandboxv1 return nil } var pol *sandboxv1.SandboxPolicy - if openclaw.IsClawSandboxBackend(ah.Spec.Backend) { + switch { + case openclaw.IsClawSandboxBackend(ah.Spec.Backend): pol = mergeOpenShellSandboxPolicies(openclaw.BaselineSandboxPolicy(), openclaw.ChannelNetworkPolicyFragment(ah)) - } else { + case hermes.IsHermesSandboxBackend(ah.Spec.Backend): + pol = mergeOpenShellSandboxPolicies(hermes.BaselineHermesSandboxPolicy(), hermes.ChannelNetworkPolicyFragment(ah)) + default: pol = openclaw.ChannelNetworkPolicyFragment(ah) } pol = mergeOpenShellSandboxPolicies(pol, sandboxPolicyFragmentFromNetwork(ah, applyAllowedDomainsPolicy)) diff --git a/go/core/pkg/sandboxbackend/openshell/providers.go b/go/core/pkg/sandboxbackend/openshell/providers.go new file mode 100644 index 0000000000..6969e5dc52 --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/providers.go @@ -0,0 +1,88 @@ +package openshell + +import ( + "context" + "fmt" + "strings" + + "github.com/kagent-dev/kagent/go/api/openshell/gen/datamodelv1" + openshellv1 "github.com/kagent-dev/kagent/go/api/openshell/gen/openshellv1" + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/channels" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const genericProviderType = "generic" + +// GatewayProviderDef describes an OpenShell gateway provider to create or update. +type GatewayProviderDef struct { + Name string + Type string + Credentials map[string]string +} + +// UpsertGatewayProvider creates or updates a single OpenShell gateway provider. +func UpsertGatewayProvider(ctx context.Context, osCli openshellv1.OpenShellClient, def GatewayProviderDef) error { + if osCli == nil { + return fmt.Errorf("openshell client is nil") + } + name := strings.TrimSpace(def.Name) + if name == "" { + return fmt.Errorf("provider name is empty") + } + providerType := strings.TrimSpace(def.Type) + if providerType == "" { + providerType = genericProviderType + } + + getResp, err := osCli.GetProvider(ctx, &openshellv1.GetProviderRequest{Name: name}) + exists := false + if err != nil { + if status.Code(err) != codes.NotFound { + return fmt.Errorf("GetProvider %s: %w", name, err) + } + } else if getResp.GetProvider() != nil { + exists = true + } + + providerProto := &datamodelv1.Provider{ + Metadata: &datamodelv1.ObjectMeta{Name: name}, + Type: providerType, + Credentials: def.Credentials, + } + + if exists { + _, err = osCli.UpdateProvider(ctx, &openshellv1.UpdateProviderRequest{Provider: providerProto}) + if err != nil { + return fmt.Errorf("UpdateProvider %s: %w", name, err) + } + return nil + } + _, err = osCli.CreateProvider(ctx, &openshellv1.CreateProviderRequest{Provider: providerProto}) + if err != nil { + return fmt.Errorf("CreateProvider %s: %w", name, err) + } + return nil +} + +// UpsertGatewayProviders upserts each provider definition. +func UpsertGatewayProviders(ctx context.Context, osCli openshellv1.OpenShellClient, defs []GatewayProviderDef) error { + for _, def := range defs { + if err := UpsertGatewayProvider(ctx, osCli, def); err != nil { + return err + } + } + return nil +} + +func messagingDefsToGateway(defs []channels.MessagingProviderDef) []GatewayProviderDef { + out := make([]GatewayProviderDef, 0, len(defs)) + for _, d := range defs { + out = append(out, GatewayProviderDef{ + Name: d.Name, + Type: genericProviderType, + Credentials: d.Credentials, + }) + } + return out +} diff --git a/go/core/pkg/sandboxbackend/openshell/ssh_terminal.go b/go/core/pkg/sandboxbackend/openshell/ssh_terminal.go new file mode 100644 index 0000000000..2299a76dc8 --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/ssh_terminal.go @@ -0,0 +1,33 @@ +package openshell + +import ( + "strings" + + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/hermes" + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/openclaw" +) + +// ResolveSSHRemoteCommand decides whether to run an interactive shell or a harness CLI. +// plainShell: client requested bash only. +// launchOverride: non-empty client launch_command wins. +// harnessBackend: wire string from the WebSocket start frame (e.g. "hermes"). +func ResolveSSHRemoteCommand(plainShell bool, launchOverride, harnessBackend string) (useShell bool, execCmd string) { + if plainShell { + return true, "" + } + cmd := strings.TrimSpace(launchOverride) + if cmd != "" { + return false, cmd + } + backend := v1alpha2.AgentHarnessBackendType(strings.TrimSpace(harnessBackend)) + if v1alpha2.IsKnownAgentHarnessBackend(backend) { + switch { + case hermes.IsHermesSandboxBackend(backend): + return false, hermes.DefaultSSHLaunchCommand() + case openclaw.IsClawSandboxBackend(backend): + return false, openclaw.DefaultSSHLaunchCommand() + } + } + return false, openclaw.DefaultSSHLaunchCommand() +} diff --git a/go/core/pkg/sandboxbackend/openshell/ssh_terminal_test.go b/go/core/pkg/sandboxbackend/openshell/ssh_terminal_test.go new file mode 100644 index 0000000000..d9d1451dea --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/ssh_terminal_test.go @@ -0,0 +1,63 @@ +package openshell_test + +import ( + "testing" + + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell" + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/hermes" + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/openclaw" +) + +func TestResolveSSHRemoteCommand(t *testing.T) { + tests := []struct { + name string + plainShell bool + launchOverride string + harnessBackend string + wantPlain bool + wantCmd string + }{ + { + name: "plain shell", + plainShell: true, + wantPlain: true, + }, + { + name: "client override", + launchOverride: " custom ", + wantPlain: false, + wantCmd: "custom", + }, + { + name: "unknown backend defaults to openclaw tui", + wantPlain: false, + wantCmd: openclaw.DefaultSSHLaunchCommand(), + }, + { + name: "hermes backend", + harnessBackend: "hermes", + wantPlain: false, + wantCmd: hermes.DefaultSSHLaunchCommand(), + }, + { + name: "openclaw backend", + harnessBackend: "openclaw", + wantPlain: false, + wantCmd: openclaw.DefaultSSHLaunchCommand(), + }, + { + name: "nemoclaw backend", + harnessBackend: "nemoclaw", + wantPlain: false, + wantCmd: openclaw.DefaultSSHLaunchCommand(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + plain, cmd := openshell.ResolveSSHRemoteCommand(tt.plainShell, tt.launchOverride, tt.harnessBackend) + if plain != tt.wantPlain || cmd != tt.wantCmd { + t.Fatalf("ResolveSSHRemoteCommand() = (%v, %q), want (%v, %q)", plain, cmd, tt.wantPlain, tt.wantCmd) + } + }) + } +} diff --git a/go/core/pkg/sandboxbackend/openshell/translate.go b/go/core/pkg/sandboxbackend/openshell/translate.go index 023bf8f9bb..c6ae5a0d0d 100644 --- a/go/core/pkg/sandboxbackend/openshell/translate.go +++ b/go/core/pkg/sandboxbackend/openshell/translate.go @@ -5,14 +5,11 @@ import ( "fmt" "strings" - "github.com/kagent-dev/kagent/go/api/openshell/gen/datamodelv1" inferencev1 "github.com/kagent-dev/kagent/go/api/openshell/gen/inferencev1" openshellv1 "github.com/kagent-dev/kagent/go/api/openshell/gen/openshellv1" "github.com/kagent-dev/kagent/go/api/v1alpha2" "github.com/kagent-dev/kagent/go/core/internal/utils" "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/openclaw" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" @@ -158,35 +155,16 @@ func translateModelConfig( providerRecordName := openclaw.GatewayProviderRecordName(modelConfig.Spec.Provider) model := modelConfig.Spec.Model - getProviderResp, err := osCli.GetProvider(ctx, &openshellv1.GetProviderRequest{Name: providerRecordName}) - exists := false - if err != nil { - if status.Code(err) != codes.NotFound { - return fmt.Errorf("GetProvider %s: %w", providerRecordName, err) - } - } else if getProviderResp.GetProvider() != nil { - exists = true - } - - providerProto := &datamodelv1.Provider{ - Metadata: &datamodelv1.ObjectMeta{Name: providerRecordName}, - Type: providerRecordName, + if err := UpsertGatewayProvider(ctx, osCli, GatewayProviderDef{ + Name: providerRecordName, + Type: providerRecordName, Credentials: map[string]string{ "apiKey": apiKey, }, + }); err != nil { + return fmt.Errorf("upsert inference provider %s: %w", providerRecordName, err) } - - if exists { - if _, err := osCli.UpdateProvider(ctx, &openshellv1.UpdateProviderRequest{Provider: providerProto}); err != nil { - return fmt.Errorf("UpdateProvider %s: %w", providerRecordName, err) - } - ctrllog.FromContext(ctx).Info("updated gateway provider", "name", providerRecordName) - } else { - if _, err := osCli.CreateProvider(ctx, &openshellv1.CreateProviderRequest{Provider: providerProto}); err != nil { - return fmt.Errorf("CreateProvider %s: %w", providerRecordName, err) - } - ctrllog.FromContext(ctx).Info("created gateway provider", "name", providerRecordName) - } + ctrllog.FromContext(ctx).Info("upserted gateway provider", "name", providerRecordName) if _, err := inference.SetClusterInference(ctx, &inferencev1.SetClusterInferenceRequest{ ProviderName: providerRecordName, diff --git a/go/core/pkg/sandboxbackend/openshell/translate_test.go b/go/core/pkg/sandboxbackend/openshell/translate_test.go index f917a93fde..332264719b 100644 --- a/go/core/pkg/sandboxbackend/openshell/translate_test.go +++ b/go/core/pkg/sandboxbackend/openshell/translate_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/hermes" "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/openclaw" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -80,6 +81,16 @@ func TestBuildOpenshellCreateRequest_AllowedDomainsPolicy(t *testing.T) { require.ElementsMatch(t, []string{"api.openai.com", "api.anthropic.com", "*.slack.com"}, hosts) } +func TestBuildClawCreateRequest_PinsBaseImage(t *testing.T) { + sbx := &v1alpha2.AgentHarness{ + ObjectMeta: metav1.ObjectMeta{Name: "s1", Namespace: "ns"}, + Spec: v1alpha2.AgentHarnessSpec{Backend: v1alpha2.AgentHarnessBackendOpenClaw}, + } + req, unsupported := buildClawCreateRequest(sbx, nil) + require.Empty(t, unsupported) + require.Equal(t, openclaw.NemoclawSandboxBaseImage, req.GetSpec().GetTemplate().GetImage()) +} + func TestBuildOpenshellCreateRequest_OpenClaw_NoAllowedDomains_HasRegistryPolicies(t *testing.T) { sbx := &v1alpha2.AgentHarness{ ObjectMeta: metav1.ObjectMeta{Name: "s1", Namespace: "ns"}, @@ -171,10 +182,15 @@ func TestBuildOpenshellCreateRequest_OpenClaw_Slack_HasSlackPolicy(t *testing.T) require.Equal(t, "slack.com", s.GetEndpoints()[0].GetHost()) require.Equal(t, "api.slack.com", s.GetEndpoints()[1].GetHost()) require.Equal(t, "hooks.slack.com", s.GetEndpoints()[2].GetHost()) - require.Equal(t, "wss-primary.slack.com", s.GetEndpoints()[3].GetHost()) - require.Equal(t, "skip", s.GetEndpoints()[3].GetTls()) - require.Equal(t, "wss-backup.slack.com", s.GetEndpoints()[4].GetHost()) - require.Equal(t, "skip", s.GetEndpoints()[4].GetTls()) + wssPrimary := s.GetEndpoints()[3] + require.Equal(t, "wss-primary.slack.com", wssPrimary.GetHost()) + require.Equal(t, "websocket", wssPrimary.GetProtocol()) + require.True(t, wssPrimary.GetWebsocketCredentialRewrite()) + wssBackup := s.GetEndpoints()[4] + require.Equal(t, "wss-backup.slack.com", wssBackup.GetHost()) + require.Equal(t, "websocket", wssBackup.GetProtocol()) + require.True(t, wssBackup.GetWebsocketCredentialRewrite()) + require.True(t, s.GetEndpoints()[0].GetRequestBodyCredentialRewrite()) } func TestBuildOpenshellCreateRequest_OpenClaw_AllowedDomains_OmitsNPMPresetHosts(t *testing.T) { @@ -203,3 +219,48 @@ func TestBuildOpenshellCreateRequest_OpenClaw_AllowedDomains_OmitsNPMPresetHosts } require.ElementsMatch(t, []string{"api.openai.com"}, hosts) } + +func TestBuildOpenshellCreateRequest_Hermes_BaselinePolicy(t *testing.T) { + sbx := &v1alpha2.AgentHarness{ + ObjectMeta: metav1.ObjectMeta{Name: "h1", Namespace: "ns"}, + Spec: v1alpha2.AgentHarnessSpec{Backend: v1alpha2.AgentHarnessBackendHermes}, + } + req, unsupported := buildHermesCreateRequest(sbx, nil) + require.Empty(t, unsupported) + require.Equal(t, hermes.HermesSandboxBaseImage, req.GetSpec().GetTemplate().GetImage()) + + pol := req.GetSpec().GetPolicy() + require.NotNil(t, pol) + net := pol.GetNetworkPolicies() + require.Contains(t, net, hermes.NetworkPolicyKeyNVIDIA) + require.Contains(t, net, hermes.NetworkPolicyKeyNousResearch) + require.Contains(t, net, hermes.NetworkPolicyKeyPyPI) + require.NotContains(t, net, openclaw.NetworkPolicyKeyClawhub) + require.NotContains(t, net, openclaw.NetworkPolicyKeyNPMYarn) + + fs := pol.GetFilesystem() + require.Contains(t, fs.GetReadWrite(), hermes.HermesConfigDir) + require.Contains(t, fs.GetReadOnly(), "/opt/hermes") +} + +func TestBuildOpenshellCreateRequest_Hermes_TelegramChannel(t *testing.T) { + sbx := &v1alpha2.AgentHarness{ + ObjectMeta: metav1.ObjectMeta{Name: "h1", Namespace: "ns"}, + Spec: v1alpha2.AgentHarnessSpec{ + Backend: v1alpha2.AgentHarnessBackendHermes, + Channels: []v1alpha2.AgentHarnessChannel{ + { + Name: "tg", + Type: v1alpha2.AgentHarnessChannelTypeTelegram, + Telegram: &v1alpha2.AgentHarnessTelegramChannelSpec{ + BotToken: v1alpha2.AgentHarnessChannelCredential{Value: "tok"}, + }, + }, + }, + }, + } + req, _ := buildAgentHarnessOpenshellCreateRequest(sbx) + net := req.GetSpec().GetPolicy().GetNetworkPolicies() + require.Contains(t, net, hermes.NetworkPolicyKeyTelegram) + require.Equal(t, "telegram", net[hermes.NetworkPolicyKeyTelegram].GetName()) +} diff --git a/helm/kagent-crds/templates/kagent.dev_agentharnesses.yaml b/helm/kagent-crds/templates/kagent.dev_agentharnesses.yaml index c7748893de..8c0cf3d07a 100644 --- a/helm/kagent-crds/templates/kagent.dev_agentharnesses.yaml +++ b/helm/kagent-crds/templates/kagent.dev_agentharnesses.yaml @@ -69,6 +69,7 @@ spec: enum: - openclaw - nemoclaw + - hermes type: string channels: description: Channels configures Telegram and Slack integrations for @@ -83,10 +84,44 @@ spec: minLength: 1 type: string slack: - description: AgentHarnessSlackChannelSpec configures Slack when - AgentHarnessChannel.type is Slack. + description: |- + AgentHarnessSlackChannelSpec configures Slack when AgentHarnessChannel.type is Slack. + + OpenClaw and NemoClaw use channelAccess, allowlistChannels, and interactiveReplies. + Hermes uses allowedUserIDs (SLACK_ALLOWED_USERS), homeChannel (SLACK_HOME_CHANNEL), + and homeChannelName (SLACK_HOME_CHANNEL_NAME) in the sandbox .env. properties: + allowedUserIDs: + description: 'AllowedUserIDs restricts which Slack user + IDs may interact with the bot (Hermes: SLACK_ALLOWED_USERS).' + items: + type: string + type: array + allowedUserIDsFrom: + description: ValueSource defines a source for configuration + values from a Secret or ConfigMap + properties: + key: + description: The key of the ConfigMap or Secret. + maxLength: 253 + type: string + name: + description: The name of the ConfigMap or Secret. + maxLength: 253 + type: string + type: + enum: + - ConfigMap + - Secret + type: string + required: + - key + - name + - type + type: object allowlistChannels: + description: AllowlistChannels is required when channelAccess + is allowlist (OpenClaw / NemoClaw only). items: type: string type: array @@ -159,26 +194,37 @@ spec: rule: (has(self.value) && !has(self.valueFrom)) || (!has(self.value) && has(self.valueFrom)) channelAccess: - description: AgentHarnessChannelAccess controls whether - the bot listens broadly or only on an allowlist. + description: ChannelAccess controls OpenClaw routing (open, + allowlist, disabled). Omit for Hermes harnesses. enum: - allowlist - open - disabled type: string + homeChannel: + description: HomeChannel is the default Slack channel ID + for Hermes cron/scheduled messages (SLACK_HOME_CHANNEL). + type: string + homeChannelName: + description: HomeChannelName is a human-readable label for + HomeChannel (SLACK_HOME_CHANNEL_NAME). + type: string interactiveReplies: default: true type: boolean required: - appToken - botToken - - channelAccess type: object x-kubernetes-validations: - message: allowlistChannels is required when channelAccess is allowlist - rule: self.channelAccess != 'allowlist' || (has(self.allowlistChannels) - && size(self.allowlistChannels) > 0) + rule: '!has(self.channelAccess) || self.channelAccess != ''allowlist'' + || (has(self.allowlistChannels) && size(self.allowlistChannels) + > 0)' + - message: allowedUserIDs and allowedUserIDsFrom are mutually + exclusive + rule: '!(size(self.allowedUserIDs) > 0 && has(self.allowedUserIDsFrom))' telegram: description: AgentHarnessTelegramChannelSpec configures Telegram when AgentHarnessChannel.type is Telegram. @@ -435,7 +481,8 @@ spec: description: |- Image is the container image to run in the harness VM, if the backend supports per-resource images. Backends openclaw and nemoclaw pin the image - to the NemoClaw sandbox base when this field is empty. + to the NemoClaw sandbox base when this field is empty; backend hermes pins + to the Hermes sandbox base image when empty. type: string modelConfigRef: description: |- @@ -473,6 +520,7 @@ spec: enum: - openclaw - nemoclaw + - hermes type: string id: type: string diff --git a/ui/src/app/actions/agents.ts b/ui/src/app/actions/agents.ts index bdf6209d9c..71375f2825 100644 --- a/ui/src/app/actions/agents.ts +++ b/ui/src/app/actions/agents.ts @@ -478,6 +478,7 @@ export async function createAgent(agentConfig: AgentFormData, update: boolean = description: agentConfig.description || "", modelRef: agentConfig.modelName || "", openClaw: agentConfig.openClawSandbox, + backend: agentConfig.harnessBackend, }); if ("error" in draft) { throw new Error(draft.error); diff --git a/ui/src/app/agents/new-harness/page.tsx b/ui/src/app/agents/new-harness/page.tsx index f57598cf4b..88bbff67b5 100644 --- a/ui/src/app/agents/new-harness/page.tsx +++ b/ui/src/app/agents/new-harness/page.tsx @@ -5,7 +5,11 @@ import { Loader2 } from "lucide-react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; -import { defaultOpenClawSandboxFormSlice, type OpenClawSandboxFormSlice } from "@/lib/openClawSandboxForm"; +import { + defaultOpenClawSandboxFormSlice, + type AgentHarnessSandboxBackend, + type OpenClawSandboxFormSlice, +} from "@/lib/openClawSandboxForm"; import type { ModelConfig } from "@/types"; import { ModelSelectionSection } from "@/components/create/ModelSelectionSection"; import { useRouter } from "next/navigation"; @@ -22,7 +26,17 @@ import type { AgentFormValidationErrors } from "@/components/agent-form/agent-fo import { focusFirstFormError } from "@/components/agent-form/focusFirstFormError"; import { PageHeader } from "@/components/layout/PageHeader"; -const HARNESS_OPTIONS = [{ value: "nemoclaw-openclaw", label: "NemoClaw (OpenClaw)" }] as const; +const HARNESS_OPTIONS = [ + { value: "nemoclaw-openclaw", label: "NemoClaw (OpenClaw)", backend: "openclaw" as const }, + { value: "hermes", label: "Hermes", backend: "hermes" as const }, +] as const; + +const HERMES_DEFAULT_IMAGE = "ghcr.io/nvidia/nemoclaw/hermes-sandbox-base:latest"; + +function harnessBackendForType(harnessType: (typeof HARNESS_OPTIONS)[number]["value"]): AgentHarnessSandboxBackend { + const opt = HARNESS_OPTIONS.find((o) => o.value === harnessType); + return opt?.backend ?? "openclaw"; +} function AgentHarnessPageContent() { const router = useRouter(); @@ -68,11 +82,6 @@ function AgentHarnessPageContent() { }, [formDirty]); const validateForm = () => { - if (state.harnessType !== "nemoclaw-openclaw") { - toast.error("Unsupported harness type."); - return false; - } - const formData: Partial = { name: state.name, namespace: state.namespace, @@ -80,6 +89,7 @@ function AgentHarnessPageContent() { type: "OpenClawSandbox", modelName: state.selectedModel?.ref || "", openClawSandbox: state.openClaw, + harnessBackend: harnessBackendForType(state.harnessType), }; const newErrors = validateAgentData(formData); @@ -136,11 +146,6 @@ function AgentHarnessPageContent() { return; } - if (state.harnessType !== "nemoclaw-openclaw") { - toast.error("Unsupported harness type."); - return; - } - try { setState((prev) => ({ ...prev, isSubmitting: true })); @@ -156,6 +161,7 @@ function AgentHarnessPageContent() { tools: [], modelName: state.selectedModel.ref, openClawSandbox: state.openClaw, + harnessBackend: harnessBackendForType(state.harnessType), }; const ocResult = await createNewAgent(ocPayload); if (ocResult.error) { @@ -250,12 +256,16 @@ function AgentHarnessPageContent() { Harness type { + const channels = value.channels.map((c) => + c.id === ch.id ? { ...c, allowedSlackUserIDs: e.target.value } : c, + ); + set({ channels }); + }} + placeholder="U01234567, U89ABCDEF (comma or space separated)" + disabled={disabled} + autoComplete="off" + /> + + + Home channel ID (optional) + + Default channel for cron/scheduled messages (SLACK_HOME_CHANNEL, e.g.{" "} + C01234567890). + + { + const channels = value.channels.map((c) => + c.id === ch.id ? { ...c, slackHomeChannel: e.target.value } : c, + ); + set({ channels }); + }} + placeholder="C01234567890" + disabled={disabled} + autoComplete="off" + /> + + + Home channel name (optional) + + Human-readable label (SLACK_HOME_CHANNEL_NAME, e.g. general). + + { + const channels = value.channels.map((c) => + c.id === ch.id ? { ...c, slackHomeChannelName: e.target.value } : c, + ); + set({ channels }); + }} + placeholder="general" + disabled={disabled} + autoComplete="off" + /> + + + )} + {ch.channelType === "telegram" && ( Allowed Telegram user IDs (optional) diff --git a/ui/src/lib/__tests__/openClawSandboxForm.test.ts b/ui/src/lib/__tests__/openClawSandboxForm.test.ts index be177138ea..8f18c143f8 100644 --- a/ui/src/lib/__tests__/openClawSandboxForm.test.ts +++ b/ui/src/lib/__tests__/openClawSandboxForm.test.ts @@ -149,5 +149,33 @@ describe("openClawSandboxForm allowedDomains", () => { expect(draft.kind).toBe("AgentHarness"); expect(draft.spec.backend).toBe("openclaw"); }); + + it("writes Hermes slack allowedUserIDs without OpenClaw channel access fields", () => { + const row = newOpenClawChannelRow(); + row.name = "slack-main"; + row.channelType = "slack"; + row.botToken = "xoxb-test"; + row.appToken = "xapp-test"; + row.allowedSlackUserIDs = "U01234567 U89ABCDEF"; + row.slackHomeChannel = "C01234567890"; + row.slackHomeChannelName = "general"; + const draft = buildSandboxCRDraft({ + name: "h1", + namespace: "ns", + description: "", + modelRef: "m1", + backend: "hermes", + openClaw: { ...defaultOpenClawSandboxFormSlice(), channels: [row] }, + }); + expect("error" in draft).toBe(false); + if ("error" in draft) return; + const channels = draft.spec.channels as { slack: Record }[]; + expect(channels[0].slack.allowedUserIDs).toEqual(["U01234567", "U89ABCDEF"]); + expect(channels[0].slack.homeChannel).toBe("C01234567890"); + expect(channels[0].slack.homeChannelName).toBe("general"); + expect(channels[0].slack.channelAccess).toBeUndefined(); + expect(channels[0].slack.allowlistChannels).toBeUndefined(); + expect(channels[0].slack.interactiveReplies).toBeUndefined(); + }); }); }); diff --git a/ui/src/lib/agentHarness.ts b/ui/src/lib/agentHarness.ts index e5b28ebd72..279ef70abd 100644 --- a/ui/src/lib/agentHarness.ts +++ b/ui/src/lib/agentHarness.ts @@ -7,7 +7,7 @@ import { isOpenshellSandboxRow } from "@/lib/openshellSandboxAgents"; * * Extend this union when new harness runtimes are added; pair with UI/server handling for each backend. */ -export const AGENT_HARNESS_BACKENDS = ["openclaw", "nemoclaw"] as const; +export const AGENT_HARNESS_BACKENDS = ["openclaw", "nemoclaw", "hermes"] as const; export type AgentHarnessBackend = (typeof AGENT_HARNESS_BACKENDS)[number]; @@ -32,6 +32,39 @@ export function isAgentHarness(item: AgentResponse): boolean { return getAgentHarnessBackend(item) !== undefined; } +/** + * Default interactive command when opening the OpenShell terminal for a harness backend. + * Keep in sync with Go: openclaw.DefaultSSHLaunchCommand / hermes.DefaultSSHLaunchCommand. + */ +export function defaultHarnessSSHLaunchCommand(backend: AgentHarnessBackend): string { + switch (backend) { + case "hermes": + return "cd /sandbox/.hermes && exec hermes"; + case "openclaw": + case "nemoclaw": + return "openclaw tui"; + default: { + const _exhaustive: never = backend; + return _exhaustive; + } + } +} + +/** Emoji shown beside harness agents in list/card views. */ +export function agentHarnessIcon(backend: AgentHarnessBackend): string { + switch (backend) { + case "hermes": + return "☤"; + case "openclaw": + case "nemoclaw": + return "🦞"; + default: { + const _exhaustive: never = backend; + return _exhaustive; + } + } +} + /** Short label for the agent list “type” column; harness-specific where known. */ export function agentHarnessTypeLabel(backend: AgentHarnessBackend): string { switch (backend) { @@ -39,6 +72,8 @@ export function agentHarnessTypeLabel(backend: AgentHarnessBackend): string { return "OpenClaw"; case "nemoclaw": return "NemoClaw"; + case "hermes": + return "Hermes"; default: { const _exhaustive: never = backend; return _exhaustive; diff --git a/ui/src/lib/openClawSandboxForm.ts b/ui/src/lib/openClawSandboxForm.ts index 92e8cced65..d900f934f0 100644 --- a/ui/src/lib/openClawSandboxForm.ts +++ b/ui/src/lib/openClawSandboxForm.ts @@ -1,9 +1,11 @@ import type { ValueSource } from "@/types"; import { k8sRefUtils } from "@/lib/k8sUtils"; -/** Sandbox CR backend; UI always uses openclaw for now. */ +/** Default Sandbox CR backend when the harness form does not specify one. */ const SANDBOX_BACKEND_OPENCLAW = "openclaw" as const; +export type AgentHarnessSandboxBackend = "openclaw" | "nemoclaw" | "hermes"; + export type SandboxChannelFormType = "telegram" | "slack"; export interface OpenClawChannelRow { @@ -20,7 +22,14 @@ export interface OpenClawChannelRow { appSecretKey: string; channelAccess: "allowlist" | "open" | "disabled"; allowlistChannels: string; + /** Telegram: maps to spec.channels[].telegram.allowedUserIDs */ allowedUserIDs: string; + /** Hermes Slack: maps to spec.channels[].slack.allowedUserIDs → SLACK_ALLOWED_USERS */ + allowedSlackUserIDs: string; + /** Hermes Slack: maps to spec.channels[].slack.homeChannel → SLACK_HOME_CHANNEL */ + slackHomeChannel: string; + /** Hermes Slack: maps to spec.channels[].slack.homeChannelName → SLACK_HOME_CHANNEL_NAME */ + slackHomeChannelName: string; interactiveReplies: boolean; } @@ -40,10 +49,17 @@ export function newOpenClawChannelRow(): OpenClawChannelRow { channelAccess: "open", allowlistChannels: "", allowedUserIDs: "", + allowedSlackUserIDs: "", + slackHomeChannel: "", + slackHomeChannelName: "", interactiveReplies: true, }; } +export function isClawHarnessBackend(backend: AgentHarnessSandboxBackend | undefined): boolean { + return backend === "openclaw" || backend === "nemoclaw"; +} + export interface OpenClawSandboxFormSlice { /** Optional override for Sandbox.spec.image (OpenShell VM template image). Empty → controller default. */ image: string; @@ -153,7 +169,9 @@ function credentialFromRow( export function validateOpenClawSandboxForm(args: { openClaw: OpenClawSandboxFormSlice; modelRef: string | undefined; + backend?: AgentHarnessSandboxBackend; }): OpenClawSandboxFormValidationError | undefined { + const clawBackend = isClawHarnessBackend(args.backend); const mr = (args.modelRef || "").trim(); if (!mr) { return openClawValidationFail("general", "Please select a model config for this sandbox."); @@ -206,7 +224,7 @@ export function validateOpenClawSandboxForm(args: { } } - if (ch.channelType === "slack") { + if (ch.channelType === "slack" && clawBackend) { if (ch.channelAccess === "allowlist") { const list = trimSplitList(ch.allowlistChannels); if (list.length === 0) { @@ -250,6 +268,7 @@ export function buildSandboxCRDraft(args: { description: string; modelRef: string; openClaw: OpenClawSandboxFormSlice; + backend?: AgentHarnessSandboxBackend; }): SandboxCRDraft | { error: string } { const modelConfigRef = modelConfigRefForSandbox(args.namespace.trim(), args.modelRef); @@ -297,13 +316,29 @@ export function buildSandboxCRDraft(args: { const slack: Record = { botToken: bot, appToken: app, - channelAccess: ch.channelAccess, - ...(ch.channelAccess === "allowlist" - ? { allowlistChannels: trimSplitList(ch.allowlistChannels) } - : {}), }; - if (!ch.interactiveReplies) { - slack.interactiveReplies = false; + const backend = args.backend ?? SANDBOX_BACKEND_OPENCLAW; + if (isClawHarnessBackend(backend)) { + slack.channelAccess = ch.channelAccess; + if (ch.channelAccess === "allowlist") { + slack.allowlistChannels = trimSplitList(ch.allowlistChannels); + } + if (!ch.interactiveReplies) { + slack.interactiveReplies = false; + } + } else { + const allowedSlack = trimSplitList(ch.allowedSlackUserIDs); + if (allowedSlack.length > 0) { + slack.allowedUserIDs = allowedSlack; + } + const homeChannel = ch.slackHomeChannel.trim(); + if (homeChannel) { + slack.homeChannel = homeChannel; + const homeName = ch.slackHomeChannelName.trim(); + if (homeName) { + slack.homeChannelName = homeName; + } + } } base.slack = slack; } @@ -311,8 +346,9 @@ export function buildSandboxCRDraft(args: { channels.push(base); } + const backend = args.backend ?? SANDBOX_BACKEND_OPENCLAW; const spec: Record = { - backend: SANDBOX_BACKEND_OPENCLAW, + backend, modelConfigRef, }; diff --git a/ui/src/lib/openshellSandboxAgents.ts b/ui/src/lib/openshellSandboxAgents.ts index e51bbeab9e..64c7e45d9d 100644 --- a/ui/src/lib/openshellSandboxAgents.ts +++ b/ui/src/lib/openshellSandboxAgents.ts @@ -1,4 +1,5 @@ import type { AgentResponse } from "@/types"; +import type { AgentHarnessBackend } from "@/lib/agentHarness"; export function isOpenshellSandboxRow(item: AgentResponse): boolean { return Boolean(item.openshellAgentHarness?.gatewaySandboxName); @@ -10,10 +11,8 @@ export type OpenshellTerminalLinkParams = { /** Sandbox CR name (Kubernetes metadata.name). */ crName?: string; modelConfigRef?: string; - /** - * OpenClaw / NemoClaw harness: terminal offers “Launch plain shell” vs default session (e.g. `openclaw tui`). - */ - clawHarness?: boolean; + /** AgentHarness.spec.backend (openclaw, nemoclaw, hermes). */ + harnessBackend?: AgentHarnessBackend; }; /** Opens `/openshell` with auto-connect when the page loads (`connect=1`). */ @@ -22,9 +21,12 @@ export function openshellTerminalHref(params: OpenshellTerminalLinkParams): stri sandbox: params.gatewaySandboxName, connect: "1", }); - if (params.clawHarness) { + if (params.harnessBackend === "openclaw" || params.harnessBackend === "nemoclaw") { q.set("clawHarness", "1"); } + if (params.harnessBackend) { + q.set("harnessBackend", params.harnessBackend); + } const ns = params.namespace?.trim(); const name = params.crName?.trim(); const mc = params.modelConfigRef?.trim(); From 67959f4eb5b367eb56be8b04823e2a8829a7464d Mon Sep 17 00:00:00 2001 From: Peter Jausovec Date: Tue, 19 May 2026 16:08:21 -0700 Subject: [PATCH 3/8] ui lint Signed-off-by: Peter Jausovec --- ui/src/components/AgentListView.tsx | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/ui/src/components/AgentListView.tsx b/ui/src/components/AgentListView.tsx index b42136941d..501d23169c 100644 --- a/ui/src/components/AgentListView.tsx +++ b/ui/src/components/AgentListView.tsx @@ -232,21 +232,26 @@ function AgentListRow({ item }: { item: AgentResponse }) { const nTools = countAgentToolBindings(item); const nSkills = countSkills(agent); - const chatPath = - sshSandbox && item.openshellAgentHarness - ? openshellTerminalHref({ - gatewaySandboxName: item.openshellAgentHarness.gatewaySandboxName, - namespace, - crName: name, - modelConfigRef: item.modelConfigRef, - harnessBackend: harnessBackend, - }) - : `/agents/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/chat`; - const goChat = useCallback(() => { + const gatewaySandboxName = item.openshellAgentHarness?.gatewaySandboxName; + const chatPath = useMemo( + () => + sshSandbox && gatewaySandboxName + ? openshellTerminalHref({ + gatewaySandboxName, + namespace, + crName: name, + modelConfigRef: item.modelConfigRef, + harnessBackend, + }) + : `/agents/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/chat`, + [sshSandbox, gatewaySandboxName, namespace, name, item.modelConfigRef, harnessBackend], + ); + + const goChat = () => { if (isReady) { router.push(chatPath); } - }, [chatPath, isReady, router]); + }; const handleEdit = (e: React.MouseEvent) => { e.preventDefault(); From b3022a25f4e76fd67b1d347d244632a1254bc931 Mon Sep 17 00:00:00 2001 From: Peter Jausovec Date: Tue, 19 May 2026 16:22:26 -0700 Subject: [PATCH 4/8] pr feedback Signed-off-by: Peter Jausovec --- .../httpserver/handlers/sandbox_ssh.go | 5 ++ .../handlers/sandbox_ssh_forward_test.go | 24 +++++++ .../openshell/channels/credentials.go | 18 +++-- .../openshell/channels/envkeys.go | 65 ++++++++++++++--- .../openshell/channels/envkeys_test.go | 13 ++++ .../openshell/channels/placeholders.go | 8 +-- .../openshell/channels/providers.go | 55 +++++++-------- .../openshell/channels/providers_test.go | 16 ++--- .../openshell/channels/resolve.go | 33 ++++++--- .../openshell/channels/resolve_test.go | 69 +++++++++++++++++++ .../pkg/sandboxbackend/openshell/hermes.go | 5 +- .../openshell/hermes/bootstrap.go | 38 +++++----- .../openshell/hermes/bootstrap_test.go | 18 ++--- .../openshell/hermes/channels.go | 32 --------- .../openshell/hermes/constants.go | 2 +- .../openshell/hermes/gateway_wait.go | 26 +++++++ .../openshell/hermes/gateway_wait_test.go | 27 ++++++++ .../hermes/messaging_providers_test.go | 4 +- .../openshell/openclaw/bootstrap_test.go | 6 +- .../openshell/openclaw/channels.go | 8 ++- .../openshell/openshell_test.go | 6 +- .../lib/__tests__/openClawSandboxForm.test.ts | 39 ++++++++++- ui/src/lib/openClawSandboxForm.ts | 19 +++-- 23 files changed, 390 insertions(+), 146 deletions(-) create mode 100644 go/core/pkg/sandboxbackend/openshell/channels/envkeys_test.go create mode 100644 go/core/pkg/sandboxbackend/openshell/channels/resolve_test.go create mode 100644 go/core/pkg/sandboxbackend/openshell/hermes/gateway_wait.go create mode 100644 go/core/pkg/sandboxbackend/openshell/hermes/gateway_wait_test.go diff --git a/go/core/internal/httpserver/handlers/sandbox_ssh.go b/go/core/internal/httpserver/handlers/sandbox_ssh.go index e0e0ddc2ca..58aa241efd 100644 --- a/go/core/internal/httpserver/handlers/sandbox_ssh.go +++ b/go/core/internal/httpserver/handlers/sandbox_ssh.go @@ -465,7 +465,12 @@ func (c *tcpForwardConn) Close() error { return nil } c.closed = true + if c.recvErr == nil { + c.recvErr = io.EOF + } + c.readCond.Broadcast() c.readMu.Unlock() + c.sendMu.Lock() defer c.sendMu.Unlock() return c.stream.CloseSend() diff --git a/go/core/internal/httpserver/handlers/sandbox_ssh_forward_test.go b/go/core/internal/httpserver/handlers/sandbox_ssh_forward_test.go index d663c2703a..6dc86dbef4 100644 --- a/go/core/internal/httpserver/handlers/sandbox_ssh_forward_test.go +++ b/go/core/internal/httpserver/handlers/sandbox_ssh_forward_test.go @@ -4,6 +4,7 @@ import ( "context" "io" "testing" + "time" openshellv1 "github.com/kagent-dev/kagent/go/api/openshell/gen/openshellv1" "github.com/stretchr/testify/require" @@ -46,6 +47,29 @@ func TestTCPForwardConnReadWrite(t *testing.T) { require.NoError(t, conn.Close()) } +func TestTCPForwardConnCloseUnblocksRead(t *testing.T) { + stream := newMockForwardTcpStream() + conn := newTCPForwardConn(stream) + + readDone := make(chan struct{}) + var readErr error + go func() { + _, readErr = conn.Read(make([]byte, 1)) + close(readDone) + }() + + time.Sleep(50 * time.Millisecond) + + require.NoError(t, conn.Close()) + + select { + case <-readDone: + case <-time.After(2 * time.Second): + t.Fatal("Read did not unblock after Close") + } + require.ErrorIs(t, readErr, io.EOF) +} + // mockForwardTcpStream is a minimal OpenShell_ForwardTcpClient for unit tests. type mockForwardTcpStream struct { recv chan *openshellv1.TcpForwardFrame diff --git a/go/core/pkg/sandboxbackend/openshell/channels/credentials.go b/go/core/pkg/sandboxbackend/openshell/channels/credentials.go index b203b82f8d..c52bb29981 100644 --- a/go/core/pkg/sandboxbackend/openshell/channels/credentials.go +++ b/go/core/pkg/sandboxbackend/openshell/channels/credentials.go @@ -11,16 +11,20 @@ import ( // PutChannelCredential resolves a channel credential into env[envKey]. func PutChannelCredential(ctx context.Context, kube client.Client, namespace string, cred v1alpha2.AgentHarnessChannelCredential, envKey string, env map[string]string) error { + var v string if strings.TrimSpace(cred.Value) != "" { - env[envKey] = strings.TrimSpace(cred.Value) - return nil - } - if cred.ValueFrom == nil { + v = strings.TrimSpace(cred.Value) + } else if cred.ValueFrom == nil { return fmt.Errorf("channel credential requires value or valueFrom") + } else { + var err error + v, err = cred.ValueFrom.Resolve(ctx, kube, namespace) + if err != nil { + return fmt.Errorf("resolve credential %s: %w", envKey, err) + } } - v, err := cred.ValueFrom.Resolve(ctx, kube, namespace) - if err != nil { - return fmt.Errorf("resolve credential %s: %w", envKey, err) + if prev, ok := env[envKey]; ok && prev != v { + return fmt.Errorf("env %s already set to a different value (duplicate channel binding?)", envKey) } env[envKey] = v return nil diff --git a/go/core/pkg/sandboxbackend/openshell/channels/envkeys.go b/go/core/pkg/sandboxbackend/openshell/channels/envkeys.go index db354228a1..a4cff6e756 100644 --- a/go/core/pkg/sandboxbackend/openshell/channels/envkeys.go +++ b/go/core/pkg/sandboxbackend/openshell/channels/envkeys.go @@ -1,11 +1,60 @@ package channels -// Standard OpenShell messaging credential environment variable names (NemoClaw contract). -const ( - EnvTelegramBotToken = "TELEGRAM_BOT_TOKEN" - EnvSlackBotToken = "SLACK_BOT_TOKEN" - EnvSlackAppToken = "SLACK_APP_TOKEN" - EnvSlackAllowedUsers = "SLACK_ALLOWED_USERS" - EnvSlackHomeChannel = "SLACK_HOME_CHANNEL" - EnvSlackHomeChannelName = "SLACK_HOME_CHANNEL_NAME" +import ( + "strings" + "unicode" ) + +// EnvKeySuffix sanitizes a channel binding name for use in environment variable names. +func EnvKeySuffix(channelName string) string { + var b strings.Builder + for _, r := range strings.TrimSpace(channelName) { + switch { + case unicode.IsLetter(r): + b.WriteRune(unicode.ToUpper(r)) + case unicode.IsDigit(r): + b.WriteRune(r) + case r == '-' || r == '_': + b.WriteRune('_') + } + } + if b.Len() == 0 { + return "CHANNEL" + } + return b.String() +} + +// TelegramBotTokenEnvKey is the per-channel Telegram bot token env var. +func TelegramBotTokenEnvKey(channelName string) string { + return "TELEGRAM_BOT_TOKEN_" + EnvKeySuffix(channelName) +} + +// TelegramAllowedUsersEnvKey is the per-channel Telegram allowlist env var (Hermes). +func TelegramAllowedUsersEnvKey(channelName string) string { + return "TELEGRAM_ALLOWED_USERS_" + EnvKeySuffix(channelName) +} + +// SlackBotTokenEnvKey is the per-channel Slack bot token env var. +func SlackBotTokenEnvKey(channelName string) string { + return "SLACK_BOT_TOKEN_" + EnvKeySuffix(channelName) +} + +// SlackAppTokenEnvKey is the per-channel Slack app token env var. +func SlackAppTokenEnvKey(channelName string) string { + return "SLACK_APP_TOKEN_" + EnvKeySuffix(channelName) +} + +// SlackAllowedUsersEnvKey is the per-channel Slack allowlist env var (Hermes). +func SlackAllowedUsersEnvKey(channelName string) string { + return "SLACK_ALLOWED_USERS_" + EnvKeySuffix(channelName) +} + +// SlackHomeChannelEnvKey is the per-channel Slack home channel env var (Hermes). +func SlackHomeChannelEnvKey(channelName string) string { + return "SLACK_HOME_CHANNEL_" + EnvKeySuffix(channelName) +} + +// SlackHomeChannelNameEnvKey is the per-channel Slack home channel display name env var (Hermes). +func SlackHomeChannelNameEnvKey(channelName string) string { + return "SLACK_HOME_CHANNEL_NAME_" + EnvKeySuffix(channelName) +} diff --git a/go/core/pkg/sandboxbackend/openshell/channels/envkeys_test.go b/go/core/pkg/sandboxbackend/openshell/channels/envkeys_test.go new file mode 100644 index 0000000000..95a844309b --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/channels/envkeys_test.go @@ -0,0 +1,13 @@ +package channels + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEnvKeySuffix(t *testing.T) { + require.Equal(t, "MY_BOT", EnvKeySuffix("my-bot")) + require.Equal(t, "CHANNEL", EnvKeySuffix(" ")) + require.Equal(t, "TELEGRAM_BOT_TOKEN_MY_BOT", TelegramBotTokenEnvKey("my-bot")) +} diff --git a/go/core/pkg/sandboxbackend/openshell/channels/placeholders.go b/go/core/pkg/sandboxbackend/openshell/channels/placeholders.go index 96502438d7..9fe130c442 100644 --- a/go/core/pkg/sandboxbackend/openshell/channels/placeholders.go +++ b/go/core/pkg/sandboxbackend/openshell/channels/placeholders.go @@ -6,11 +6,11 @@ func ResolveEnvPlaceholder(envKey string) string { } // SlackBotTokenPlaceholder matches NemoClaw Hermes/OpenClaw Slack bot token shape validation. -func SlackBotTokenPlaceholder() string { - return "xoxb-OPENSHELL-RESOLVE-ENV-SLACK_BOT_TOKEN" +func SlackBotTokenPlaceholder(envKey string) string { + return "xoxb-OPENSHELL-RESOLVE-ENV-" + envKey } // SlackAppTokenPlaceholder matches NemoClaw Hermes/OpenClaw Slack app token shape validation. -func SlackAppTokenPlaceholder() string { - return "xapp-OPENSHELL-RESOLVE-ENV-SLACK_APP_TOKEN" +func SlackAppTokenPlaceholder(envKey string) string { + return "xapp-OPENSHELL-RESOLVE-ENV-" + envKey } diff --git a/go/core/pkg/sandboxbackend/openshell/channels/providers.go b/go/core/pkg/sandboxbackend/openshell/channels/providers.go index 33e50ef000..c6334105ce 100644 --- a/go/core/pkg/sandboxbackend/openshell/channels/providers.go +++ b/go/core/pkg/sandboxbackend/openshell/channels/providers.go @@ -1,55 +1,52 @@ package channels -// MessagingBridgeNames are OpenShell provider names for a sandbox (NemoClaw onboard). -func TelegramBridgeName(sandboxName string) string { - return sandboxName + "-telegram-bridge" +// MessagingProviderDef is an OpenShell gateway provider for one messaging credential. +type MessagingProviderDef struct { + Name string + Credentials map[string]string } -func SlackBridgeName(sandboxName string) string { - return sandboxName + "-slack-bridge" +// TelegramBridgeName is the OpenShell provider name for a Telegram channel binding. +func TelegramBridgeName(sandboxName, channelName string) string { + return sandboxName + "-telegram-" + EnvKeySuffix(channelName) } -func SlackAppBridgeName(sandboxName string) string { - return sandboxName + "-slack-app" +// SlackBridgeName is the OpenShell provider name for a Slack bot token binding. +func SlackBridgeName(sandboxName, channelName string) string { + return sandboxName + "-slack-" + EnvKeySuffix(channelName) } -// MessagingProviderDef is an OpenShell gateway provider for one messaging credential. -type MessagingProviderDef struct { - Name string - Credentials map[string]string +// SlackAppBridgeName is the OpenShell provider name for a Slack app token binding. +func SlackAppBridgeName(sandboxName, channelName string) string { + return sandboxName + "-slack-app-" + EnvKeySuffix(channelName) } -// MessagingProviderDefs builds provider upsert records from resolved channel secrets. +// MessagingProviderDefs builds per-channel provider upsert records from resolved secrets. func MessagingProviderDefs(sandboxName string, secrets map[string]string, resolved *Resolved) []MessagingProviderDef { if sandboxName == "" || resolved == nil { return nil } var defs []MessagingProviderDef - if resolved.HasTelegram { - if tok := secrets[EnvTelegramBotToken]; tok != "" { + for _, tg := range resolved.Telegram { + envKey := TelegramBotTokenEnvKey(tg.Name) + if tok := secrets[envKey]; tok != "" { defs = append(defs, MessagingProviderDef{ - Name: TelegramBridgeName(sandboxName), - Credentials: map[string]string{ - EnvTelegramBotToken: tok, - }, + Name: TelegramBridgeName(sandboxName, tg.Name), + Credentials: map[string]string{envKey: tok}, }) } } - if resolved.HasSlack { - if tok := secrets[EnvSlackBotToken]; tok != "" { + for _, sl := range resolved.Slack { + if tok := secrets[SlackBotTokenEnvKey(sl.Name)]; tok != "" { defs = append(defs, MessagingProviderDef{ - Name: SlackBridgeName(sandboxName), - Credentials: map[string]string{ - EnvSlackBotToken: tok, - }, + Name: SlackBridgeName(sandboxName, sl.Name), + Credentials: map[string]string{SlackBotTokenEnvKey(sl.Name): tok}, }) } - if tok := secrets[EnvSlackAppToken]; tok != "" { + if tok := secrets[SlackAppTokenEnvKey(sl.Name)]; tok != "" { defs = append(defs, MessagingProviderDef{ - Name: SlackAppBridgeName(sandboxName), - Credentials: map[string]string{ - EnvSlackAppToken: tok, - }, + Name: SlackAppBridgeName(sandboxName, sl.Name), + Credentials: map[string]string{SlackAppTokenEnvKey(sl.Name): tok}, }) } } diff --git a/go/core/pkg/sandboxbackend/openshell/channels/providers_test.go b/go/core/pkg/sandboxbackend/openshell/channels/providers_test.go index 6f036ebb6d..72265058fa 100644 --- a/go/core/pkg/sandboxbackend/openshell/channels/providers_test.go +++ b/go/core/pkg/sandboxbackend/openshell/channels/providers_test.go @@ -8,17 +8,17 @@ import ( func TestMessagingProviderDefs(t *testing.T) { resolved := &Resolved{ - HasTelegram: true, - HasSlack: true, + Telegram: []TelegramAccount{{Name: "tg"}}, + Slack: []SlackAccount{{Name: "sl"}}, Secrets: map[string]string{ - EnvTelegramBotToken: "tg", - EnvSlackBotToken: "xoxb", - EnvSlackAppToken: "xapp", + TelegramBotTokenEnvKey("tg"): "tg-secret", + SlackBotTokenEnvKey("sl"): "xoxb", + SlackAppTokenEnvKey("sl"): "xapp", }, } defs := MessagingProviderDefs("ns-h", resolved.Secrets, resolved) require.Len(t, defs, 3) - require.Equal(t, "ns-h-telegram-bridge", defs[0].Name) - require.Equal(t, "ns-h-slack-bridge", defs[1].Name) - require.Equal(t, "ns-h-slack-app", defs[2].Name) + require.Equal(t, "ns-h-telegram-TG", defs[0].Name) + require.Equal(t, "ns-h-slack-SL", defs[1].Name) + require.Equal(t, "ns-h-slack-app-SL", defs[2].Name) } diff --git a/go/core/pkg/sandboxbackend/openshell/channels/resolve.go b/go/core/pkg/sandboxbackend/openshell/channels/resolve.go index 7ae9c05de9..8df3a284d5 100644 --- a/go/core/pkg/sandboxbackend/openshell/channels/resolve.go +++ b/go/core/pkg/sandboxbackend/openshell/channels/resolve.go @@ -20,6 +20,9 @@ type SlackAccount struct { Name string ChannelAccess v1alpha2.AgentHarnessChannelAccess AllowlistChannels []string + AllowedUserIDs []string + HomeChannel string + HomeChannelName string InteractiveReplies bool } @@ -44,11 +47,20 @@ type Resolved struct { slackSeen bool } -// Resolve reads AgentHarness channels, populates standard credential env keys in Secrets, +// Resolve reads AgentHarness channels, populates per-channel credential env keys in Secrets, // and returns structured account metadata for Hermes/OpenClaw bootstrap. func Resolve(ctx context.Context, kube client.Client, namespace string, channels []v1alpha2.AgentHarnessChannel) (*Resolved, error) { r := &Resolved{Secrets: map[string]string{}} + seenNames := make(map[string]struct{}, len(channels)) for _, ch := range channels { + name := strings.TrimSpace(ch.Name) + if name == "" { + continue + } + if _, dup := seenNames[name]; dup { + return nil, fmt.Errorf("channel %q: duplicate binding name", name) + } + seenNames[name] = struct{}{} switch ch.Type { case v1alpha2.AgentHarnessChannelTypeTelegram: if err := r.addTelegram(ctx, kube, namespace, ch); err != nil { @@ -70,7 +82,7 @@ func (r *Resolved) addTelegram(ctx context.Context, kube client.Client, namespac if spec == nil { return fmt.Errorf("channel %q: telegram spec is required", ch.Name) } - if err := PutChannelCredential(ctx, kube, namespace, spec.BotToken, EnvTelegramBotToken, r.Secrets); err != nil { + if err := PutChannelCredential(ctx, kube, namespace, spec.BotToken, TelegramBotTokenEnvKey(ch.Name), r.Secrets); err != nil { return fmt.Errorf("channel %q telegram bot token: %w", ch.Name, err) } allow, err := TelegramAllowFrom(ctx, kube, namespace, spec) @@ -90,10 +102,10 @@ func (r *Resolved) addSlack(ctx context.Context, kube client.Client, namespace s if spec == nil { return fmt.Errorf("channel %q: slack spec is required", ch.Name) } - if err := PutChannelCredential(ctx, kube, namespace, spec.BotToken, EnvSlackBotToken, r.Secrets); err != nil { + if err := PutChannelCredential(ctx, kube, namespace, spec.BotToken, SlackBotTokenEnvKey(ch.Name), r.Secrets); err != nil { return fmt.Errorf("channel %q slack bot token: %w", ch.Name, err) } - if err := PutChannelCredential(ctx, kube, namespace, spec.AppToken, EnvSlackAppToken, r.Secrets); err != nil { + if err := PutChannelCredential(ctx, kube, namespace, spec.AppToken, SlackAppTokenEnvKey(ch.Name), r.Secrets); err != nil { return fmt.Errorf("channel %q slack app token: %w", ch.Name, err) } allow, err := SlackAllowedUsers(ctx, kube, namespace, spec) @@ -112,21 +124,24 @@ func (r *Resolved) addSlack(ctx context.Context, kube client.Client, namespace s if len(allow) > 0 { r.SlackAllow = append(r.SlackAllow, allow...) } + homeChannel := strings.TrimSpace(spec.HomeChannel) + homeChannelName := strings.TrimSpace(spec.HomeChannelName) r.Slack = append(r.Slack, SlackAccount{ Name: ch.Name, ChannelAccess: access, AllowlistChannels: TrimNonEmptyStrings(spec.AllowlistChannels), + AllowedUserIDs: allow, + HomeChannel: homeChannel, + HomeChannelName: homeChannelName, InteractiveReplies: interactive, }) if !r.slackSeen { r.slackRootPolicy = access r.slackSeen = true } - if r.SlackHomeChannel == "" { - if home := strings.TrimSpace(spec.HomeChannel); home != "" { - r.SlackHomeChannel = home - r.SlackHomeChannelName = strings.TrimSpace(spec.HomeChannelName) - } + if r.SlackHomeChannel == "" && homeChannel != "" { + r.SlackHomeChannel = homeChannel + r.SlackHomeChannelName = homeChannelName } return nil } diff --git a/go/core/pkg/sandboxbackend/openshell/channels/resolve_test.go b/go/core/pkg/sandboxbackend/openshell/channels/resolve_test.go new file mode 100644 index 0000000000..19d1590d77 --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/channels/resolve_test.go @@ -0,0 +1,69 @@ +package channels + +import ( + "context" + "testing" + + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/stretchr/testify/require" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestResolve_perChannelTelegramSecrets(t *testing.T) { + ns := "default" + kube := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build() + channels := []v1alpha2.AgentHarnessChannel{ + { + Name: "bot-a", + Type: v1alpha2.AgentHarnessChannelTypeTelegram, + Telegram: &v1alpha2.AgentHarnessTelegramChannelSpec{ + BotToken: v1alpha2.AgentHarnessChannelCredential{Value: "token-a"}, + }, + }, + { + Name: "bot-b", + Type: v1alpha2.AgentHarnessChannelTypeTelegram, + Telegram: &v1alpha2.AgentHarnessTelegramChannelSpec{ + BotToken: v1alpha2.AgentHarnessChannelCredential{Value: "token-b"}, + }, + }, + } + resolved, err := Resolve(context.Background(), kube, ns, channels) + require.NoError(t, err) + require.Equal(t, "token-a", resolved.Secrets[TelegramBotTokenEnvKey("bot-a")]) + require.Equal(t, "token-b", resolved.Secrets[TelegramBotTokenEnvKey("bot-b")]) +} + +func TestResolve_duplicateChannelName(t *testing.T) { + kube := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build() + _, err := Resolve(context.Background(), kube, "default", []v1alpha2.AgentHarnessChannel{ + {Name: "dup", Type: v1alpha2.AgentHarnessChannelTypeTelegram, Telegram: &v1alpha2.AgentHarnessTelegramChannelSpec{ + BotToken: v1alpha2.AgentHarnessChannelCredential{Value: "a"}, + }}, + {Name: "dup", Type: v1alpha2.AgentHarnessChannelTypeTelegram, Telegram: &v1alpha2.AgentHarnessTelegramChannelSpec{ + BotToken: v1alpha2.AgentHarnessChannelCredential{Value: "b"}, + }}, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "duplicate binding name") +} + +func TestMessagingProviderDefs_perChannel(t *testing.T) { + resolved := &Resolved{ + Telegram: []TelegramAccount{{Name: "tg1"}, {Name: "tg2"}}, + Slack: []SlackAccount{{Name: "sl1"}}, + Secrets: map[string]string{ + TelegramBotTokenEnvKey("tg1"): "tok1", + TelegramBotTokenEnvKey("tg2"): "tok2", + SlackBotTokenEnvKey("sl1"): "xoxb", + SlackAppTokenEnvKey("sl1"): "xapp", + }, + } + defs := MessagingProviderDefs("ns-h", resolved.Secrets, resolved) + require.Len(t, defs, 4) + require.Equal(t, "ns-h-telegram-TG1", defs[0].Name) + require.Equal(t, "tok1", defs[0].Credentials[TelegramBotTokenEnvKey("tg1")]) + require.Equal(t, "ns-h-telegram-TG2", defs[1].Name) + require.Equal(t, "tok2", defs[1].Credentials[TelegramBotTokenEnvKey("tg2")]) +} diff --git a/go/core/pkg/sandboxbackend/openshell/hermes.go b/go/core/pkg/sandboxbackend/openshell/hermes.go index c58490732a..f674d0aeea 100644 --- a/go/core/pkg/sandboxbackend/openshell/hermes.go +++ b/go/core/pkg/sandboxbackend/openshell/hermes.go @@ -144,10 +144,7 @@ func (b *HermesBackend) OnAgentHarnessReady(ctx context.Context, ah *v1alpha2.Ag return fmt.Errorf("start hermes gateway: exit %d: %s", code, strings.TrimSpace(stderr)) } - waitGateway := fmt.Sprintf( - `for i in $(seq 1 30); do ss -tln 2>/dev/null | grep -q "127.0.0.1:%d" && exit 0; sleep 1; done; exit 0`, - hermes.HermesInternalGatewayPort, - ) + waitGateway := hermes.GatewayListenWaitScript(hermes.HermesInternalGatewayPort) code, stderr, err = b.ExecSandbox(withAuth(gwCtx, token), execID, []string{"sh", "-c", waitGateway}, nil, execEnv, 45) if err != nil { return fmt.Errorf("wait for hermes gateway listen: %w", err) diff --git a/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap.go b/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap.go index ea8928419a..009290d58b 100644 --- a/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap.go +++ b/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap.go @@ -119,31 +119,35 @@ func BuildHermesConfigYAML(mc *v1alpha2.ModelConfig, msg *messagingState) ([]byt return raw, nil } -// BuildHermesEnvFile returns .env file bytes and populates execEnv with resolved channel secrets. -func BuildHermesEnvFile(msg *messagingState, execEnv map[string]string) []byte { +// BuildHermesEnvFile returns .env file bytes for Hermes bootstrap (gateway port, channel placeholders, allowlists). +// Resolved channel secrets belong in execEnv; callers populate that separately (see BuildBootstrapArtifacts). +func BuildHermesEnvFile(msg *messagingState) []byte { lines := []string{ fmt.Sprintf("API_SERVER_PORT=%d", HermesInternalGatewayPort), "API_SERVER_HOST=127.0.0.1", } - if msg != nil { - if msg.hasTelegram() { - lines = append(lines, "TELEGRAM_BOT_TOKEN="+channels.ResolveEnvPlaceholder(channels.EnvTelegramBotToken)) - if allow := msg.telegramAllow(); len(allow) > 0 { - lines = append(lines, "TELEGRAM_ALLOWED_USERS="+strings.Join(allow, ",")) + if msg != nil && msg.resolved != nil { + for _, tg := range msg.resolved.Telegram { + botKey := channels.TelegramBotTokenEnvKey(tg.Name) + lines = append(lines, botKey+"="+channels.ResolveEnvPlaceholder(botKey)) + if len(tg.AllowFrom) > 0 { + lines = append(lines, channels.TelegramAllowedUsersEnvKey(tg.Name)+"="+strings.Join(tg.AllowFrom, ",")) } } - if msg.hasSlack() { + for _, sl := range msg.resolved.Slack { + botKey := channels.SlackBotTokenEnvKey(sl.Name) + appKey := channels.SlackAppTokenEnvKey(sl.Name) lines = append(lines, - "SLACK_BOT_TOKEN="+channels.SlackBotTokenPlaceholder(), - "SLACK_APP_TOKEN="+channels.SlackAppTokenPlaceholder(), + botKey+"="+channels.SlackBotTokenPlaceholder(botKey), + appKey+"="+channels.SlackAppTokenPlaceholder(appKey), ) - if allow := msg.slackAllow(); len(allow) > 0 { - lines = append(lines, channels.EnvSlackAllowedUsers+"="+strings.Join(allow, ",")) + if len(sl.AllowedUserIDs) > 0 { + lines = append(lines, channels.SlackAllowedUsersEnvKey(sl.Name)+"="+strings.Join(sl.AllowedUserIDs, ",")) } - if home := msg.slackHomeChannel(); home != "" { - lines = append(lines, channels.EnvSlackHomeChannel+"="+home) - if name := msg.slackHomeChannelName(); name != "" { - lines = append(lines, channels.EnvSlackHomeChannelName+"="+name) + if home := strings.TrimSpace(sl.HomeChannel); home != "" { + lines = append(lines, channels.SlackHomeChannelEnvKey(sl.Name)+"="+home) + if name := strings.TrimSpace(sl.HomeChannelName); name != "" { + lines = append(lines, channels.SlackHomeChannelNameEnvKey(sl.Name)+"="+name) } } } @@ -169,6 +173,6 @@ func BuildBootstrapArtifacts(ctx context.Context, kube client.Client, namespace if err != nil { return nil, nil, nil, err } - envFile = BuildHermesEnvFile(msg, execEnv) + envFile = BuildHermesEnvFile(msg) return configYAML, envFile, execEnv, nil } diff --git a/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap_test.go b/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap_test.go index d9d77357b7..3ca2f2e6d5 100644 --- a/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap_test.go +++ b/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap_test.go @@ -86,13 +86,13 @@ func TestBuildBootstrapArtifacts_TelegramSlack(t *testing.T) { require.NoError(t, err) require.Contains(t, string(configYAML), "require_mention: true") env := string(envFile) - require.Contains(t, env, "TELEGRAM_BOT_TOKEN=openshell:resolve:env:TELEGRAM_BOT_TOKEN") - require.Contains(t, env, "TELEGRAM_ALLOWED_USERS=123456789") - require.Contains(t, env, "SLACK_BOT_TOKEN=xoxb-OPENSHELL-RESOLVE-ENV-SLACK_BOT_TOKEN") - require.Contains(t, env, "SLACK_APP_TOKEN=xapp-OPENSHELL-RESOLVE-ENV-SLACK_APP_TOKEN") - require.Contains(t, env, "SLACK_ALLOWED_USERS=U01234567,U89ABCDEF") - require.Contains(t, env, "SLACK_HOME_CHANNEL=C01234567890") - require.Contains(t, env, "SLACK_HOME_CHANNEL_NAME=general") - require.Equal(t, "tg-token", execEnv["TELEGRAM_BOT_TOKEN"]) - require.Equal(t, "xoxb-bot", execEnv["SLACK_BOT_TOKEN"]) + require.Contains(t, env, "TELEGRAM_BOT_TOKEN_TG=openshell:resolve:env:TELEGRAM_BOT_TOKEN_TG") + require.Contains(t, env, "TELEGRAM_ALLOWED_USERS_TG=123456789") + require.Contains(t, env, "SLACK_BOT_TOKEN_SL=xoxb-OPENSHELL-RESOLVE-ENV-SLACK_BOT_TOKEN_SL") + require.Contains(t, env, "SLACK_APP_TOKEN_SL=xapp-OPENSHELL-RESOLVE-ENV-SLACK_APP_TOKEN_SL") + require.Contains(t, env, "SLACK_ALLOWED_USERS_SL=U01234567,U89ABCDEF") + require.Contains(t, env, "SLACK_HOME_CHANNEL_SL=C01234567890") + require.Contains(t, env, "SLACK_HOME_CHANNEL_NAME_SL=general") + require.Equal(t, "tg-token", execEnv["TELEGRAM_BOT_TOKEN_TG"]) + require.Equal(t, "xoxb-bot", execEnv["SLACK_BOT_TOKEN_SL"]) } diff --git a/go/core/pkg/sandboxbackend/openshell/hermes/channels.go b/go/core/pkg/sandboxbackend/openshell/hermes/channels.go index 568b23ea5f..a00d9c5c77 100644 --- a/go/core/pkg/sandboxbackend/openshell/hermes/channels.go +++ b/go/core/pkg/sandboxbackend/openshell/hermes/channels.go @@ -25,38 +25,6 @@ func (st *messagingState) hasTelegram() bool { return st != nil && st.resolved != nil && st.resolved.HasTelegram } -func (st *messagingState) hasSlack() bool { - return st != nil && st.resolved != nil && st.resolved.HasSlack -} - -func (st *messagingState) telegramAllow() []string { - if st == nil || st.resolved == nil { - return nil - } - return st.resolved.TelegramAllow -} - -func (st *messagingState) slackAllow() []string { - if st == nil || st.resolved == nil { - return nil - } - return st.resolved.SlackAllow -} - -func (st *messagingState) slackHomeChannel() string { - if st == nil || st.resolved == nil { - return "" - } - return st.resolved.SlackHomeChannel -} - -func (st *messagingState) slackHomeChannelName() string { - if st == nil || st.resolved == nil { - return "" - } - return st.resolved.SlackHomeChannelName -} - func (st *messagingState) secrets() map[string]string { if st == nil || st.resolved == nil { return nil diff --git a/go/core/pkg/sandboxbackend/openshell/hermes/constants.go b/go/core/pkg/sandboxbackend/openshell/hermes/constants.go index 62c83e707a..0cca8e1244 100644 --- a/go/core/pkg/sandboxbackend/openshell/hermes/constants.go +++ b/go/core/pkg/sandboxbackend/openshell/hermes/constants.go @@ -2,7 +2,7 @@ package hermes const ( // HermesSandboxBaseImage is the default OpenShell VM image for Hermes harnesses. - HermesSandboxBaseImage = "ghcr.io/nvidia/nemoclaw/hermes-sandbox-base:latest" + HermesSandboxBaseImage = "ghcr.io/nvidia/nemoclaw/hermes-sandbox-base:3e56f808" // HermesConfigDir is the in-sandbox Hermes config root (HERMES_HOME). HermesConfigDir = "/sandbox/.hermes" diff --git a/go/core/pkg/sandboxbackend/openshell/hermes/gateway_wait.go b/go/core/pkg/sandboxbackend/openshell/hermes/gateway_wait.go new file mode 100644 index 0000000000..9630fa501a --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/hermes/gateway_wait.go @@ -0,0 +1,26 @@ +package hermes + +import "fmt" + +// GatewayListenWaitScript returns a shell snippet that exits 0 once 127.0.0.1:port is +// listening, exit 2 if ss is unavailable, or exit 1 on timeout with diagnostics. +func GatewayListenWaitScript(port int) string { + return fmt.Sprintf(`listen='127.0.0.1:%d' +if ! command -v ss >/dev/null 2>&1; then + echo 'hermes gateway wait: ss not found in PATH' >&2 + exit 2 +fi +for i in $(seq 1 30); do + if ss -tln 2>/dev/null | grep -qF "$listen"; then + exit 0 + fi + sleep 1 +done +echo "hermes gateway wait: timed out after 30s waiting for $listen" >&2 +echo '--- ss -tln ---' >&2 +ss -tln 2>&1 | head -20 >&2 || true +echo '--- tail /tmp/gateway.log ---' >&2 +tail -n 40 /tmp/gateway.log 2>&1 >&2 || echo '(no /tmp/gateway.log)' >&2 +exit 1 +`, port) +} diff --git a/go/core/pkg/sandboxbackend/openshell/hermes/gateway_wait_test.go b/go/core/pkg/sandboxbackend/openshell/hermes/gateway_wait_test.go new file mode 100644 index 0000000000..820d3e2018 --- /dev/null +++ b/go/core/pkg/sandboxbackend/openshell/hermes/gateway_wait_test.go @@ -0,0 +1,27 @@ +package hermes_test + +import ( + "strings" + "testing" + + "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/openshell/hermes" + "github.com/stretchr/testify/require" +) + +func TestGatewayListenWaitScript(t *testing.T) { + script := hermes.GatewayListenWaitScript(hermes.HermesInternalGatewayPort) + require.Contains(t, script, "127.0.0.1:18642") + require.Contains(t, script, "ss not found") + require.Contains(t, script, "/tmp/gateway.log") + require.Contains(t, script, "exit 1") + require.NotContains(t, script, "done; exit 0") + // Must not succeed after the poll loop without a listen match. + lines := strings.Split(script, "\n") + lastNonEmpty := "" + for _, line := range lines { + if strings.TrimSpace(line) != "" { + lastNonEmpty = strings.TrimSpace(line) + } + } + require.Equal(t, "exit 1", lastNonEmpty) +} diff --git a/go/core/pkg/sandboxbackend/openshell/hermes/messaging_providers_test.go b/go/core/pkg/sandboxbackend/openshell/hermes/messaging_providers_test.go index e45047019a..6a1bf4fcb3 100644 --- a/go/core/pkg/sandboxbackend/openshell/hermes/messaging_providers_test.go +++ b/go/core/pkg/sandboxbackend/openshell/hermes/messaging_providers_test.go @@ -44,6 +44,6 @@ func TestMessagingProviderDefsFromChannels(t *testing.T) { require.NoError(t, err) defs := channels.MessagingProviderDefs("default-mybot", resolved.Secrets, resolved) require.Len(t, defs, 1) - require.Equal(t, "default-mybot-telegram-bridge", defs[0].Name) - require.Equal(t, "123:ABC", defs[0].Credentials[channels.EnvTelegramBotToken]) + require.Equal(t, "default-mybot-telegram-TG", defs[0].Name) + require.Equal(t, "123:ABC", defs[0].Credentials[channels.TelegramBotTokenEnvKey("tg")]) } diff --git a/go/core/pkg/sandboxbackend/openshell/openclaw/bootstrap_test.go b/go/core/pkg/sandboxbackend/openshell/openclaw/bootstrap_test.go index 297406d95f..4dfa1a2633 100644 --- a/go/core/pkg/sandboxbackend/openshell/openclaw/bootstrap_test.go +++ b/go/core/pkg/sandboxbackend/openshell/openclaw/bootstrap_test.go @@ -95,7 +95,7 @@ func TestBuildBootstrapJSON_OpenAIAndTelegram(t *testing.T) { raw, env, err := openclaw.BuildBootstrapJSON(context.Background(), kube, ns, sbx, mc, 18800) require.NoError(t, err) require.Equal(t, "sk-test", env["OPENAI_API_KEY"]) - require.Equal(t, "telegram-bot-token", env["TELEGRAM_BOT_TOKEN"]) + require.Equal(t, "telegram-bot-token", env["TELEGRAM_BOT_TOKEN_TG1"]) var root map[string]any require.NoError(t, json.Unmarshal(raw, &root)) @@ -118,11 +118,11 @@ func TestBuildBootstrapJSON_OpenAIAndTelegram(t *testing.T) { kagent := secProvs["kagent"].(map[string]any) require.Equal(t, "env", kagent["source"]) al := kagent["allowlist"].([]any) - require.ElementsMatch(t, []any{"TELEGRAM_BOT_TOKEN", "OPENAI_API_KEY"}, al) + require.ElementsMatch(t, []any{"TELEGRAM_BOT_TOKEN_TG1", "OPENAI_API_KEY"}, al) ch := root["channels"].(map[string]any) require.Contains(t, ch, "telegram") tg := ch["telegram"].(map[string]any) tgAcc := tg["accounts"].(map[string]any) tg1 := tgAcc["tg1"].(map[string]any) - require.Equal(t, "openshell:resolve:env:TELEGRAM_BOT_TOKEN", tg1["botToken"]) + require.Equal(t, "openshell:resolve:env:TELEGRAM_BOT_TOKEN_TG1", tg1["botToken"]) } diff --git a/go/core/pkg/sandboxbackend/openshell/openclaw/channels.go b/go/core/pkg/sandboxbackend/openshell/openclaw/channels.go index f29835816b..fd7dde05ce 100644 --- a/go/core/pkg/sandboxbackend/openshell/openclaw/channels.go +++ b/go/core/pkg/sandboxbackend/openshell/openclaw/channels.go @@ -38,7 +38,7 @@ func (a *harnessChannels) channelsJSON() *channelsConfig { acc := telegramAccount{ Name: tg.Name, Enabled: true, - BotToken: openshellResolveEnv(channels.EnvTelegramBotToken), + BotToken: openshellResolveEnv(channels.TelegramBotTokenEnvKey(tg.Name)), } if len(tg.AllowFrom) > 0 { acc.DMPolicy = "allowlist" @@ -61,12 +61,14 @@ func (a *harnessChannels) channelsJSON() *channelsConfig { accounts := make(map[string]slackAccount, len(r.Slack)) var def string for _, sl := range r.Slack { + botKey := channels.SlackBotTokenEnvKey(sl.Name) + appKey := channels.SlackAppTokenEnvKey(sl.Name) acc := slackAccount{ Name: sl.Name, Enabled: true, Mode: "socket", - BotToken: channels.SlackBotTokenPlaceholder(), - AppToken: channels.SlackAppTokenPlaceholder(), + BotToken: channels.SlackBotTokenPlaceholder(botKey), + AppToken: channels.SlackAppTokenPlaceholder(appKey), UserTokenReadOnly: true, GroupPolicy: string(sl.ChannelAccess), Capabilities: slackCaps{ diff --git a/go/core/pkg/sandboxbackend/openshell/openshell_test.go b/go/core/pkg/sandboxbackend/openshell/openshell_test.go index 4df8266635..1f21c0d161 100644 --- a/go/core/pkg/sandboxbackend/openshell/openshell_test.go +++ b/go/core/pkg/sandboxbackend/openshell/openshell_test.go @@ -485,13 +485,13 @@ func TestUpsertMessagingProviders(t *testing.T) { names, err := UpsertMessagingProviders(context.Background(), clients, kube, ah) require.NoError(t, err) - require.Equal(t, []string{"default-hermes1-telegram-bridge"}, names) + require.Equal(t, []string{"default-hermes1-telegram-TG"}, names) fg.mu.Lock() defer fg.mu.Unlock() - p := fg.providers["default-hermes1-telegram-bridge"] + p := fg.providers["default-hermes1-telegram-TG"] require.NotNil(t, p) - require.Equal(t, "tg-secret", p.GetCredentials()["TELEGRAM_BOT_TOKEN"]) + require.Equal(t, "tg-secret", p.GetCredentials()["TELEGRAM_BOT_TOKEN_TG"]) require.Equal(t, "generic", p.GetType()) } diff --git a/ui/src/lib/__tests__/openClawSandboxForm.test.ts b/ui/src/lib/__tests__/openClawSandboxForm.test.ts index 8f18c143f8..ba98f7c12c 100644 --- a/ui/src/lib/__tests__/openClawSandboxForm.test.ts +++ b/ui/src/lib/__tests__/openClawSandboxForm.test.ts @@ -33,17 +33,50 @@ describe("validateOpenClawSandboxForm sections", () => { expect(r?.message).toContain("not a valid hostname"); }); - it("tags channel credential failures as channels", () => { + it("tags channel credential failures as channels", () => { + const row = newOpenClawChannelRow(); + row.name = "slack1"; + row.channelType = "slack"; + row.botToken = ""; + const r = validateOpenClawSandboxForm({ + openClaw: { ...defaultOpenClawSandboxFormSlice(), channels: [row] }, + modelRef: "ns/m1", + }); + expect(r?.section).toBe("channels"); + expect(r?.message).toContain("slack1"); + }); + + it("rejects duplicate channel binding names", () => { + const row = newOpenClawChannelRow(); + row.name = "dup"; + row.channelType = "telegram"; + row.botToken = "token-a"; + const row2 = newOpenClawChannelRow(); + row2.name = "dup"; + row2.channelType = "telegram"; + row2.botToken = "token-b"; + const r = validateOpenClawSandboxForm({ + openClaw: { ...defaultOpenClawSandboxFormSlice(), channels: [row, row2] }, + modelRef: "ns/m1", + }); + expect(r?.section).toBe("channels"); + expect(r?.message).toContain("Duplicate"); + }); + + it("requires Slack allowlist channels when backend is unset (defaults to openclaw)", () => { const row = newOpenClawChannelRow(); row.name = "slack1"; row.channelType = "slack"; - row.botToken = ""; + row.botToken = "xoxb-test"; + row.appToken = "xapp-test"; + row.channelAccess = "allowlist"; + row.allowlistChannels = ""; const r = validateOpenClawSandboxForm({ openClaw: { ...defaultOpenClawSandboxFormSlice(), channels: [row] }, modelRef: "ns/m1", }); expect(r?.section).toBe("channels"); - expect(r?.message).toContain("slack1"); + expect(r?.message).toContain("allowlist"); }); }); diff --git a/ui/src/lib/openClawSandboxForm.ts b/ui/src/lib/openClawSandboxForm.ts index d900f934f0..5d9953da44 100644 --- a/ui/src/lib/openClawSandboxForm.ts +++ b/ui/src/lib/openClawSandboxForm.ts @@ -4,6 +4,10 @@ import { k8sRefUtils } from "@/lib/k8sUtils"; /** Default Sandbox CR backend when the harness form does not specify one. */ const SANDBOX_BACKEND_OPENCLAW = "openclaw" as const; +function resolveSandboxBackend(backend?: AgentHarnessSandboxBackend): AgentHarnessSandboxBackend { + return backend ?? SANDBOX_BACKEND_OPENCLAW; +} + export type AgentHarnessSandboxBackend = "openclaw" | "nemoclaw" | "hermes"; export type SandboxChannelFormType = "telegram" | "slack"; @@ -171,7 +175,7 @@ export function validateOpenClawSandboxForm(args: { modelRef: string | undefined; backend?: AgentHarnessSandboxBackend; }): OpenClawSandboxFormValidationError | undefined { - const clawBackend = isClawHarnessBackend(args.backend); + const clawBackend = isClawHarnessBackend(resolveSandboxBackend(args.backend)); const mr = (args.modelRef || "").trim(); if (!mr) { return openClawValidationFail("general", "Please select a model config for this sandbox."); @@ -186,6 +190,7 @@ export function validateOpenClawSandboxForm(args: { } } + const seenChannelNames = new Set(); for (const ch of args.openClaw.channels) { const cn = ch.name.trim(); if (!cn) { @@ -199,6 +204,13 @@ export function validateOpenClawSandboxForm(args: { } continue; } + if (seenChannelNames.has(cn)) { + return openClawValidationFail( + "channels", + `Duplicate channel binding name "${cn}". Each channel needs a unique name.`, + ); + } + seenChannelNames.add(cn); const bot = credentialFromRow( ch.botTokenSource, @@ -317,8 +329,7 @@ export function buildSandboxCRDraft(args: { botToken: bot, appToken: app, }; - const backend = args.backend ?? SANDBOX_BACKEND_OPENCLAW; - if (isClawHarnessBackend(backend)) { + if (isClawHarnessBackend(resolveSandboxBackend(args.backend))) { slack.channelAccess = ch.channelAccess; if (ch.channelAccess === "allowlist") { slack.allowlistChannels = trimSplitList(ch.allowlistChannels); @@ -346,7 +357,7 @@ export function buildSandboxCRDraft(args: { channels.push(base); } - const backend = args.backend ?? SANDBOX_BACKEND_OPENCLAW; + const backend = resolveSandboxBackend(args.backend); const spec: Record = { backend, modelConfigRef, From 5ec1b9c057a430b07022d8a363f27edf23ed6349 Mon Sep 17 00:00:00 2001 From: Peter Jausovec Date: Sat, 23 May 2026 18:52:20 -0700 Subject: [PATCH 5/8] split slack settings into separate structs Signed-off-by: Peter Jausovec --- .../crd/bases/kagent.dev_agentharnesses.yaml | 27 ++--- go/api/v1alpha2/agentharness_types.go | 57 ++++++--- go/api/v1alpha2/zz_generated.deepcopy.go | 56 +++++++-- .../openshell/channels/credentials.go | 14 +-- .../openshell/channels/resolve.go | 112 ++++++++++++++---- .../openshell/channels/resolve_test.go | 4 +- .../openshell/hermes/bootstrap.go | 2 +- .../openshell/hermes/bootstrap_test.go | 13 +- .../openshell/hermes/channels.go | 4 +- .../hermes/messaging_providers_test.go | 3 +- .../openshell/hermes/policy_test.go | 5 +- .../openshell/messaging_providers.go | 2 +- .../openshell/openclaw/bootstrap.go | 2 +- .../openshell/openclaw/channels.go | 4 +- .../openshell/translate_test.go | 8 +- .../templates/kagent.dev_agentharnesses.yaml | 27 ++--- .../agent-form/OpenClawSandboxFields.tsx | 4 +- .../lib/__tests__/openClawSandboxForm.test.ts | 8 +- ui/src/lib/openClawSandboxForm.ts | 4 +- 19 files changed, 238 insertions(+), 118 deletions(-) diff --git a/go/api/config/crd/bases/kagent.dev_agentharnesses.yaml b/go/api/config/crd/bases/kagent.dev_agentharnesses.yaml index 8c0cf3d07a..310142bb01 100644 --- a/go/api/config/crd/bases/kagent.dev_agentharnesses.yaml +++ b/go/api/config/crd/bases/kagent.dev_agentharnesses.yaml @@ -76,7 +76,7 @@ spec: OpenClaw inside the harness VM. items: description: AgentHarnessChannel declares one messenger binding - inside an OpenClaw/NemoClaw harness VM. + inside a harness VM. properties: name: description: Name is a stable id for this binding (OpenClaw @@ -84,16 +84,11 @@ spec: minLength: 1 type: string slack: - description: |- - AgentHarnessSlackChannelSpec configures Slack when AgentHarnessChannel.type is Slack. - - OpenClaw and NemoClaw use channelAccess, allowlistChannels, and interactiveReplies. - Hermes uses allowedUserIDs (SLACK_ALLOWED_USERS), homeChannel (SLACK_HOME_CHANNEL), - and homeChannelName (SLACK_HOME_CHANNEL_NAME) in the sandbox .env. + description: Slack configures Slack when type is Slack. properties: allowedUserIDs: - description: 'AllowedUserIDs restricts which Slack user - IDs may interact with the bot (Hermes: SLACK_ALLOWED_USERS).' + description: AllowedUserIDs restricts which Slack member + IDs may interact with the bot (SLACK_ALLOWED_USERS). items: type: string type: array @@ -121,7 +116,7 @@ spec: type: object allowlistChannels: description: AllowlistChannels is required when channelAccess - is allowlist (OpenClaw / NemoClaw only). + is allowlist. items: type: string type: array @@ -194,8 +189,8 @@ spec: rule: (has(self.value) && !has(self.valueFrom)) || (!has(self.value) && has(self.valueFrom)) channelAccess: - description: ChannelAccess controls OpenClaw routing (open, - allowlist, disabled). Omit for Hermes harnesses. + description: AgentHarnessChannelAccess controls whether + the bot listens broadly or only on an allowlist. enum: - allowlist - open @@ -203,7 +198,7 @@ spec: type: string homeChannel: description: HomeChannel is the default Slack channel ID - for Hermes cron/scheduled messages (SLACK_HOME_CHANNEL). + for cron/scheduled messages (SLACK_HOME_CHANNEL). type: string homeChannelName: description: HomeChannelName is a human-readable label for @@ -217,14 +212,14 @@ spec: - botToken type: object x-kubernetes-validations: + - message: allowedUserIDs and allowedUserIDsFrom are mutually + exclusive + rule: '!(size(self.allowedUserIDs) > 0 && has(self.allowedUserIDsFrom))' - message: allowlistChannels is required when channelAccess is allowlist rule: '!has(self.channelAccess) || self.channelAccess != ''allowlist'' || (has(self.allowlistChannels) && size(self.allowlistChannels) > 0)' - - message: allowedUserIDs and allowedUserIDsFrom are mutually - exclusive - rule: '!(size(self.allowedUserIDs) > 0 && has(self.allowedUserIDsFrom))' telegram: description: AgentHarnessTelegramChannelSpec configures Telegram when AgentHarnessChannel.type is Telegram. diff --git a/go/api/v1alpha2/agentharness_types.go b/go/api/v1alpha2/agentharness_types.go index a8f1b990f8..c6ad4c2e8b 100644 --- a/go/api/v1alpha2/agentharness_types.go +++ b/go/api/v1alpha2/agentharness_types.go @@ -79,34 +79,30 @@ type AgentHarnessTelegramChannelSpec struct { AllowedUserIDsFrom *ValueSource `json:"allowedUserIDsFrom,omitempty"` } -// AgentHarnessSlackChannelSpec configures Slack when AgentHarnessChannel.type is Slack. -// -// OpenClaw and NemoClaw use channelAccess, allowlistChannels, and interactiveReplies. -// Hermes uses allowedUserIDs (SLACK_ALLOWED_USERS), homeChannel (SLACK_HOME_CHANNEL), -// and homeChannelName (SLACK_HOME_CHANNEL_NAME) in the sandbox .env. +// AgentHarnessOpenClawSlackOptions configures OpenClaw/NemoClaw-specific Slack routing. // // +kubebuilder:validation:XValidation:rule="!has(self.channelAccess) || self.channelAccess != 'allowlist' || (has(self.allowlistChannels) && size(self.allowlistChannels) > 0)",message="allowlistChannels is required when channelAccess is allowlist" -// +kubebuilder:validation:XValidation:rule="!(size(self.allowedUserIDs) > 0 && has(self.allowedUserIDsFrom))",message="allowedUserIDs and allowedUserIDsFrom are mutually exclusive" -type AgentHarnessSlackChannelSpec struct { - // +required - BotToken AgentHarnessChannelCredential `json:"botToken"` - // +required - AppToken AgentHarnessChannelCredential `json:"appToken"` - // ChannelAccess controls OpenClaw routing (open, allowlist, disabled). Omit for Hermes harnesses. +type AgentHarnessOpenClawSlackOptions struct { // +optional ChannelAccess AgentHarnessChannelAccess `json:"channelAccess,omitempty"` - // AllowlistChannels is required when channelAccess is allowlist (OpenClaw / NemoClaw only). + // AllowlistChannels is required when channelAccess is allowlist. // +optional AllowlistChannels []string `json:"allowlistChannels,omitempty"` // +optional // +kubebuilder:default=true InteractiveReplies *bool `json:"interactiveReplies,omitempty"` - // AllowedUserIDs restricts which Slack user IDs may interact with the bot (Hermes: SLACK_ALLOWED_USERS). +} + +// AgentHarnessHermesSlackOptions configures Hermes-specific Slack settings (env vars in the sandbox). +// +// +kubebuilder:validation:XValidation:rule="!(size(self.allowedUserIDs) > 0 && has(self.allowedUserIDsFrom))",message="allowedUserIDs and allowedUserIDsFrom are mutually exclusive" +type AgentHarnessHermesSlackOptions struct { + // AllowedUserIDs restricts which Slack member IDs may interact with the bot (SLACK_ALLOWED_USERS). // +optional AllowedUserIDs []string `json:"allowedUserIDs,omitempty"` // +optional AllowedUserIDsFrom *ValueSource `json:"allowedUserIDsFrom,omitempty"` - // HomeChannel is the default Slack channel ID for Hermes cron/scheduled messages (SLACK_HOME_CHANNEL). + // HomeChannel is the default Slack channel ID for cron/scheduled messages (SLACK_HOME_CHANNEL). // +optional HomeChannel string `json:"homeChannel,omitempty"` // HomeChannelName is a human-readable label for HomeChannel (SLACK_HOME_CHANNEL_NAME). @@ -114,7 +110,35 @@ type AgentHarnessSlackChannelSpec struct { HomeChannelName string `json:"homeChannelName,omitempty"` } -// AgentHarnessChannel declares one messenger binding inside an OpenClaw/NemoClaw harness VM. +// AgentHarnessSlackChannelSpec configures Slack when AgentHarnessChannel.type is Slack. +// YAML is flat: botToken, appToken, plus backend-specific fields. Which fields apply is determined +// by spec.backend on the AgentHarness; OpenClaw and Hermes settings are separate structs in Go. +type AgentHarnessSlackChannelSpec struct { + // +required + BotToken AgentHarnessChannelCredential `json:"botToken"` + // +required + AppToken AgentHarnessChannelCredential `json:"appToken"` + AgentHarnessOpenClawSlackOptions `json:",inline"` + AgentHarnessHermesSlackOptions `json:",inline"` +} + +// OpenClawOptions returns OpenClaw/NemoClaw Slack settings embedded in the spec. +func (s *AgentHarnessSlackChannelSpec) OpenClawOptions() *AgentHarnessOpenClawSlackOptions { + if s == nil { + return nil + } + return &s.AgentHarnessOpenClawSlackOptions +} + +// HermesOptions returns Hermes Slack settings embedded in the spec. +func (s *AgentHarnessSlackChannelSpec) HermesOptions() *AgentHarnessHermesSlackOptions { + if s == nil { + return nil + } + return &s.AgentHarnessHermesSlackOptions +} + +// AgentHarnessChannel declares one messenger binding inside a harness VM. // // +kubebuilder:validation:XValidation:rule="(self.type == 'telegram' && has(self.telegram) && !has(self.slack)) || (self.type == 'slack' && has(self.slack) && !has(self.telegram))",message="exactly one of telegram or slack must be set and must match type" type AgentHarnessChannel struct { @@ -126,6 +150,7 @@ type AgentHarnessChannel struct { Type AgentHarnessChannelType `json:"type"` // +optional Telegram *AgentHarnessTelegramChannelSpec `json:"telegram,omitempty"` + // Slack configures Slack when type is Slack. // +optional Slack *AgentHarnessSlackChannelSpec `json:"slack,omitempty"` } diff --git a/go/api/v1alpha2/zz_generated.deepcopy.go b/go/api/v1alpha2/zz_generated.deepcopy.go index cb1d70a875..ad1233c84e 100644 --- a/go/api/v1alpha2/zz_generated.deepcopy.go +++ b/go/api/v1alpha2/zz_generated.deepcopy.go @@ -163,6 +163,31 @@ func (in *AgentHarnessConnection) DeepCopy() *AgentHarnessConnection { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentHarnessHermesSlackOptions) DeepCopyInto(out *AgentHarnessHermesSlackOptions) { + *out = *in + if in.AllowedUserIDs != nil { + in, out := &in.AllowedUserIDs, &out.AllowedUserIDs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AllowedUserIDsFrom != nil { + in, out := &in.AllowedUserIDsFrom, &out.AllowedUserIDsFrom + *out = new(ValueSource) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentHarnessHermesSlackOptions. +func (in *AgentHarnessHermesSlackOptions) DeepCopy() *AgentHarnessHermesSlackOptions { + if in == nil { + return nil + } + out := new(AgentHarnessHermesSlackOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AgentHarnessList) DeepCopyInto(out *AgentHarnessList) { *out = *in @@ -216,10 +241,8 @@ func (in *AgentHarnessNetwork) DeepCopy() *AgentHarnessNetwork { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AgentHarnessSlackChannelSpec) DeepCopyInto(out *AgentHarnessSlackChannelSpec) { +func (in *AgentHarnessOpenClawSlackOptions) DeepCopyInto(out *AgentHarnessOpenClawSlackOptions) { *out = *in - in.BotToken.DeepCopyInto(&out.BotToken) - in.AppToken.DeepCopyInto(&out.AppToken) if in.AllowlistChannels != nil { in, out := &in.AllowlistChannels, &out.AllowlistChannels *out = make([]string, len(*in)) @@ -230,16 +253,25 @@ func (in *AgentHarnessSlackChannelSpec) DeepCopyInto(out *AgentHarnessSlackChann *out = new(bool) **out = **in } - if in.AllowedUserIDs != nil { - in, out := &in.AllowedUserIDs, &out.AllowedUserIDs - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.AllowedUserIDsFrom != nil { - in, out := &in.AllowedUserIDsFrom, &out.AllowedUserIDsFrom - *out = new(ValueSource) - **out = **in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentHarnessOpenClawSlackOptions. +func (in *AgentHarnessOpenClawSlackOptions) DeepCopy() *AgentHarnessOpenClawSlackOptions { + if in == nil { + return nil } + out := new(AgentHarnessOpenClawSlackOptions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentHarnessSlackChannelSpec) DeepCopyInto(out *AgentHarnessSlackChannelSpec) { + *out = *in + in.BotToken.DeepCopyInto(&out.BotToken) + in.AppToken.DeepCopyInto(&out.AppToken) + in.AgentHarnessOpenClawSlackOptions.DeepCopyInto(&out.AgentHarnessOpenClawSlackOptions) + in.AgentHarnessHermesSlackOptions.DeepCopyInto(&out.AgentHarnessHermesSlackOptions) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentHarnessSlackChannelSpec. diff --git a/go/core/pkg/sandboxbackend/openshell/channels/credentials.go b/go/core/pkg/sandboxbackend/openshell/channels/credentials.go index c52bb29981..315a2e6c84 100644 --- a/go/core/pkg/sandboxbackend/openshell/channels/credentials.go +++ b/go/core/pkg/sandboxbackend/openshell/channels/credentials.go @@ -52,11 +52,11 @@ func TelegramAllowFrom(ctx context.Context, kube client.Client, namespace string return nil, nil } -// SlackAllowedUsers returns allowed Slack user IDs from the channel spec (Hermes SLACK_ALLOWED_USERS). -func SlackAllowedUsers(ctx context.Context, kube client.Client, namespace string, spec *v1alpha2.AgentHarnessSlackChannelSpec) ([]string, error) { - if len(spec.AllowedUserIDs) > 0 { - out := make([]string, 0, len(spec.AllowedUserIDs)) - for _, id := range spec.AllowedUserIDs { +// HermesSlackAllowedUsers returns allowed Slack member IDs from the Hermes channel spec (SLACK_ALLOWED_USERS). +func HermesSlackAllowedUsers(ctx context.Context, kube client.Client, namespace string, opts *v1alpha2.AgentHarnessHermesSlackOptions) ([]string, error) { + if len(opts.AllowedUserIDs) > 0 { + out := make([]string, 0, len(opts.AllowedUserIDs)) + for _, id := range opts.AllowedUserIDs { s := strings.TrimSpace(id) if s != "" { out = append(out, s) @@ -64,8 +64,8 @@ func SlackAllowedUsers(ctx context.Context, kube client.Client, namespace string } return out, nil } - if spec.AllowedUserIDsFrom != nil { - raw, err := spec.AllowedUserIDsFrom.Resolve(ctx, kube, namespace) + if opts.AllowedUserIDsFrom != nil { + raw, err := opts.AllowedUserIDsFrom.Resolve(ctx, kube, namespace) if err != nil { return nil, fmt.Errorf("resolve allowedUserIDsFrom: %w", err) } diff --git a/go/core/pkg/sandboxbackend/openshell/channels/resolve.go b/go/core/pkg/sandboxbackend/openshell/channels/resolve.go index 8df3a284d5..24d97242c5 100644 --- a/go/core/pkg/sandboxbackend/openshell/channels/resolve.go +++ b/go/core/pkg/sandboxbackend/openshell/channels/resolve.go @@ -49,7 +49,7 @@ type Resolved struct { // Resolve reads AgentHarness channels, populates per-channel credential env keys in Secrets, // and returns structured account metadata for Hermes/OpenClaw bootstrap. -func Resolve(ctx context.Context, kube client.Client, namespace string, channels []v1alpha2.AgentHarnessChannel) (*Resolved, error) { +func Resolve(ctx context.Context, kube client.Client, namespace string, backend v1alpha2.AgentHarnessBackendType, channels []v1alpha2.AgentHarnessChannel) (*Resolved, error) { r := &Resolved{Secrets: map[string]string{}} seenNames := make(map[string]struct{}, len(channels)) for _, ch := range channels { @@ -67,7 +67,7 @@ func Resolve(ctx context.Context, kube client.Client, namespace string, channels return nil, err } case v1alpha2.AgentHarnessChannelTypeSlack: - if err := r.addSlack(ctx, kube, namespace, ch); err != nil { + if err := r.addSlackChannel(ctx, kube, namespace, backend, ch); err != nil { return nil, err } default: @@ -97,48 +97,114 @@ func (r *Resolved) addTelegram(ctx context.Context, kube client.Client, namespac return nil } -func (r *Resolved) addSlack(ctx context.Context, kube client.Client, namespace string, ch v1alpha2.AgentHarnessChannel) error { +func (r *Resolved) addSlackChannel(ctx context.Context, kube client.Client, namespace string, backend v1alpha2.AgentHarnessBackendType, ch v1alpha2.AgentHarnessChannel) error { spec := ch.Slack if spec == nil { return fmt.Errorf("channel %q: slack spec is required", ch.Name) } - if err := PutChannelCredential(ctx, kube, namespace, spec.BotToken, SlackBotTokenEnvKey(ch.Name), r.Secrets); err != nil { - return fmt.Errorf("channel %q slack bot token: %w", ch.Name, err) + switch backend { + case v1alpha2.AgentHarnessBackendHermes: + if err := rejectOpenClawSlackFields(ch.Name, spec); err != nil { + return err + } + return r.addHermesSlack(ctx, kube, namespace, ch.Name, spec.BotToken, spec.AppToken, spec.HermesOptions()) + case v1alpha2.AgentHarnessBackendOpenClaw, v1alpha2.AgentHarnessBackendNemoClaw: + if err := rejectHermesSlackFields(ch.Name, spec); err != nil { + return err + } + return r.addOpenClawSlack(ctx, kube, namespace, ch.Name, spec.BotToken, spec.AppToken, spec.OpenClawOptions()) + default: + return fmt.Errorf("channel %q: slack channels are not supported for backend %q", ch.Name, backend) + } +} + +func rejectOpenClawSlackFields(channelName string, spec *v1alpha2.AgentHarnessSlackChannelSpec) error { + opts := spec.OpenClawOptions() + if opts == nil { + return nil } - if err := PutChannelCredential(ctx, kube, namespace, spec.AppToken, SlackAppTokenEnvKey(ch.Name), r.Secrets); err != nil { - return fmt.Errorf("channel %q slack app token: %w", ch.Name, err) + if opts.ChannelAccess != "" { + return fmt.Errorf("channel %q: channelAccess is not supported when backend is hermes", channelName) } - allow, err := SlackAllowedUsers(ctx, kube, namespace, spec) - if err != nil { - return fmt.Errorf("channel %q slack allowed users: %w", ch.Name, err) + if len(opts.AllowlistChannels) > 0 { + return fmt.Errorf("channel %q: allowlistChannels is not supported when backend is hermes", channelName) + } + if opts.InteractiveReplies != nil { + return fmt.Errorf("channel %q: interactiveReplies is not supported when backend is hermes", channelName) + } + return nil +} + +func rejectHermesSlackFields(channelName string, spec *v1alpha2.AgentHarnessSlackChannelSpec) error { + opts := spec.HermesOptions() + if opts == nil { + return nil + } + if len(opts.AllowedUserIDs) > 0 || opts.AllowedUserIDsFrom != nil || + strings.TrimSpace(opts.HomeChannel) != "" || strings.TrimSpace(opts.HomeChannelName) != "" { + return fmt.Errorf("channel %q: Hermes slack fields are not supported when backend is openclaw or nemoclaw", channelName) + } + return nil +} + +func (r *Resolved) putSlackCredentials(ctx context.Context, kube client.Client, namespace, channelName string, botToken, appToken v1alpha2.AgentHarnessChannelCredential) error { + if err := PutChannelCredential(ctx, kube, namespace, botToken, SlackBotTokenEnvKey(channelName), r.Secrets); err != nil { + return fmt.Errorf("channel %q slack bot token: %w", channelName, err) + } + if err := PutChannelCredential(ctx, kube, namespace, appToken, SlackAppTokenEnvKey(channelName), r.Secrets); err != nil { + return fmt.Errorf("channel %q slack app token: %w", channelName, err) + } + return nil +} + +func (r *Resolved) addOpenClawSlack(ctx context.Context, kube client.Client, namespace, channelName string, botToken, appToken v1alpha2.AgentHarnessChannelCredential, opts *v1alpha2.AgentHarnessOpenClawSlackOptions) error { + if err := r.putSlackCredentials(ctx, kube, namespace, channelName, botToken, appToken); err != nil { + return err } interactive := true - if spec.InteractiveReplies != nil { - interactive = *spec.InteractiveReplies + if opts.InteractiveReplies != nil { + interactive = *opts.InteractiveReplies } - access := spec.ChannelAccess + access := opts.ChannelAccess if access == "" { access = v1alpha2.AgentHarnessChannelAccessOpen } r.HasSlack = true - if len(allow) > 0 { - r.SlackAllow = append(r.SlackAllow, allow...) - } - homeChannel := strings.TrimSpace(spec.HomeChannel) - homeChannelName := strings.TrimSpace(spec.HomeChannelName) r.Slack = append(r.Slack, SlackAccount{ - Name: ch.Name, + Name: channelName, ChannelAccess: access, - AllowlistChannels: TrimNonEmptyStrings(spec.AllowlistChannels), - AllowedUserIDs: allow, - HomeChannel: homeChannel, - HomeChannelName: homeChannelName, + AllowlistChannels: TrimNonEmptyStrings(opts.AllowlistChannels), InteractiveReplies: interactive, }) if !r.slackSeen { r.slackRootPolicy = access r.slackSeen = true } + return nil +} + +func (r *Resolved) addHermesSlack(ctx context.Context, kube client.Client, namespace, channelName string, botToken, appToken v1alpha2.AgentHarnessChannelCredential, opts *v1alpha2.AgentHarnessHermesSlackOptions) error { + if err := r.putSlackCredentials(ctx, kube, namespace, channelName, botToken, appToken); err != nil { + return err + } + allow, err := HermesSlackAllowedUsers(ctx, kube, namespace, opts) + if err != nil { + return fmt.Errorf("channel %q slack allowed users: %w", channelName, err) + } + homeChannel := strings.TrimSpace(opts.HomeChannel) + homeChannelName := strings.TrimSpace(opts.HomeChannelName) + r.HasSlack = true + if len(allow) > 0 { + r.SlackAllow = append(r.SlackAllow, allow...) + } + r.Slack = append(r.Slack, SlackAccount{ + Name: channelName, + ChannelAccess: v1alpha2.AgentHarnessChannelAccessOpen, + AllowedUserIDs: allow, + HomeChannel: homeChannel, + HomeChannelName: homeChannelName, + InteractiveReplies: true, + }) if r.SlackHomeChannel == "" && homeChannel != "" { r.SlackHomeChannel = homeChannel r.SlackHomeChannelName = homeChannelName diff --git a/go/core/pkg/sandboxbackend/openshell/channels/resolve_test.go b/go/core/pkg/sandboxbackend/openshell/channels/resolve_test.go index 19d1590d77..330b71ed3f 100644 --- a/go/core/pkg/sandboxbackend/openshell/channels/resolve_test.go +++ b/go/core/pkg/sandboxbackend/openshell/channels/resolve_test.go @@ -29,7 +29,7 @@ func TestResolve_perChannelTelegramSecrets(t *testing.T) { }, }, } - resolved, err := Resolve(context.Background(), kube, ns, channels) + resolved, err := Resolve(context.Background(), kube, ns, v1alpha2.AgentHarnessBackendHermes, channels) require.NoError(t, err) require.Equal(t, "token-a", resolved.Secrets[TelegramBotTokenEnvKey("bot-a")]) require.Equal(t, "token-b", resolved.Secrets[TelegramBotTokenEnvKey("bot-b")]) @@ -37,7 +37,7 @@ func TestResolve_perChannelTelegramSecrets(t *testing.T) { func TestResolve_duplicateChannelName(t *testing.T) { kube := fake.NewClientBuilder().WithScheme(scheme.Scheme).Build() - _, err := Resolve(context.Background(), kube, "default", []v1alpha2.AgentHarnessChannel{ + _, err := Resolve(context.Background(), kube, "default", v1alpha2.AgentHarnessBackendHermes, []v1alpha2.AgentHarnessChannel{ {Name: "dup", Type: v1alpha2.AgentHarnessChannelTypeTelegram, Telegram: &v1alpha2.AgentHarnessTelegramChannelSpec{ BotToken: v1alpha2.AgentHarnessChannelCredential{Value: "a"}, }}, diff --git a/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap.go b/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap.go index 009290d58b..778e468ec2 100644 --- a/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap.go +++ b/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap.go @@ -163,7 +163,7 @@ func BuildBootstrapArtifacts(ctx context.Context, kube client.Client, namespace execEnv = map[string]string{} var msg *messagingState if ah != nil && len(ah.Spec.Channels) > 0 { - msg, err = AccumulateMessagingChannels(ctx, kube, namespace, ah.Spec.Channels, nil) + msg, err = AccumulateMessagingChannels(ctx, kube, namespace, ah.Spec.Backend, ah.Spec.Channels, nil) if err != nil { return nil, nil, nil, err } diff --git a/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap_test.go b/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap_test.go index 3ca2f2e6d5..1996c8653e 100644 --- a/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap_test.go +++ b/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap_test.go @@ -51,6 +51,7 @@ func TestBuildBootstrapArtifacts_TelegramSlack(t *testing.T) { ah := &v1alpha2.AgentHarness{ ObjectMeta: metav1.ObjectMeta{Name: "h1", Namespace: ns}, Spec: v1alpha2.AgentHarnessSpec{ + Backend: v1alpha2.AgentHarnessBackendHermes, Channels: []v1alpha2.AgentHarnessChannel{ { Name: "tg", @@ -70,11 +71,13 @@ func TestBuildBootstrapArtifacts_TelegramSlack(t *testing.T) { Name: "sl", Type: v1alpha2.AgentHarnessChannelTypeSlack, Slack: &v1alpha2.AgentHarnessSlackChannelSpec{ - BotToken: v1alpha2.AgentHarnessChannelCredential{Value: "xoxb-bot"}, - AppToken: v1alpha2.AgentHarnessChannelCredential{Value: "xapp-app"}, - AllowedUserIDs: []string{"U01234567", "U89ABCDEF"}, - HomeChannel: "C01234567890", - HomeChannelName: "general", + BotToken: v1alpha2.AgentHarnessChannelCredential{Value: "xoxb-bot"}, + AppToken: v1alpha2.AgentHarnessChannelCredential{Value: "xapp-app"}, + AgentHarnessHermesSlackOptions: v1alpha2.AgentHarnessHermesSlackOptions{ + AllowedUserIDs: []string{"U01234567", "U89ABCDEF"}, + HomeChannel: "C01234567890", + HomeChannelName: "general", + }, }, }, }, diff --git a/go/core/pkg/sandboxbackend/openshell/hermes/channels.go b/go/core/pkg/sandboxbackend/openshell/hermes/channels.go index a00d9c5c77..877f16651f 100644 --- a/go/core/pkg/sandboxbackend/openshell/hermes/channels.go +++ b/go/core/pkg/sandboxbackend/openshell/hermes/channels.go @@ -13,8 +13,8 @@ type messagingState struct { } // AccumulateMessagingChannels resolves channel credentials and returns messaging state for Hermes bootstrap. -func AccumulateMessagingChannels(ctx context.Context, kube client.Client, namespace string, specChannels []v1alpha2.AgentHarnessChannel, _ map[string]string) (*messagingState, error) { - resolved, err := channels.Resolve(ctx, kube, namespace, specChannels) +func AccumulateMessagingChannels(ctx context.Context, kube client.Client, namespace string, backend v1alpha2.AgentHarnessBackendType, specChannels []v1alpha2.AgentHarnessChannel, _ map[string]string) (*messagingState, error) { + resolved, err := channels.Resolve(ctx, kube, namespace, backend, specChannels) if err != nil { return nil, err } diff --git a/go/core/pkg/sandboxbackend/openshell/hermes/messaging_providers_test.go b/go/core/pkg/sandboxbackend/openshell/hermes/messaging_providers_test.go index 6a1bf4fcb3..b090a8df90 100644 --- a/go/core/pkg/sandboxbackend/openshell/hermes/messaging_providers_test.go +++ b/go/core/pkg/sandboxbackend/openshell/hermes/messaging_providers_test.go @@ -23,6 +23,7 @@ func TestMessagingProviderDefsFromChannels(t *testing.T) { ah := &v1alpha2.AgentHarness{ ObjectMeta: metav1.ObjectMeta{Name: "mybot", Namespace: ns}, Spec: v1alpha2.AgentHarnessSpec{ + Backend: v1alpha2.AgentHarnessBackendHermes, Channels: []v1alpha2.AgentHarnessChannel{ { Name: "tg", @@ -40,7 +41,7 @@ func TestMessagingProviderDefsFromChannels(t *testing.T) { }, }, } - resolved, err := channels.Resolve(context.Background(), kube, ns, ah.Spec.Channels) + resolved, err := channels.Resolve(context.Background(), kube, ns, ah.Spec.Backend, ah.Spec.Channels) require.NoError(t, err) defs := channels.MessagingProviderDefs("default-mybot", resolved.Secrets, resolved) require.Len(t, defs, 1) diff --git a/go/core/pkg/sandboxbackend/openshell/hermes/policy_test.go b/go/core/pkg/sandboxbackend/openshell/hermes/policy_test.go index 0e7f504323..69311b3802 100644 --- a/go/core/pkg/sandboxbackend/openshell/hermes/policy_test.go +++ b/go/core/pkg/sandboxbackend/openshell/hermes/policy_test.go @@ -32,9 +32,8 @@ func TestChannelNetworkPolicyFragment_Slack(t *testing.T) { Name: "sl", Type: v1alpha2.AgentHarnessChannelTypeSlack, Slack: &v1alpha2.AgentHarnessSlackChannelSpec{ - BotToken: v1alpha2.AgentHarnessChannelCredential{Value: "xoxb-bot"}, - AppToken: v1alpha2.AgentHarnessChannelCredential{Value: "xapp-app"}, - ChannelAccess: v1alpha2.AgentHarnessChannelAccessOpen, + BotToken: v1alpha2.AgentHarnessChannelCredential{Value: "xoxb-bot"}, + AppToken: v1alpha2.AgentHarnessChannelCredential{Value: "xapp-app"}, }, }, }, diff --git a/go/core/pkg/sandboxbackend/openshell/messaging_providers.go b/go/core/pkg/sandboxbackend/openshell/messaging_providers.go index cb2a2f64cc..147e379bff 100644 --- a/go/core/pkg/sandboxbackend/openshell/messaging_providers.go +++ b/go/core/pkg/sandboxbackend/openshell/messaging_providers.go @@ -28,7 +28,7 @@ func UpsertMessagingProviders( return nil, fmt.Errorf("openshell: Kubernetes client is required for messaging providers") } - resolved, err := channels.Resolve(ctx, kube, ah.Namespace, ah.Spec.Channels) + resolved, err := channels.Resolve(ctx, kube, ah.Namespace, ah.Spec.Backend, ah.Spec.Channels) if err != nil { return nil, err } diff --git a/go/core/pkg/sandboxbackend/openshell/openclaw/bootstrap.go b/go/core/pkg/sandboxbackend/openshell/openclaw/bootstrap.go index b96330b92c..db2fdb373e 100644 --- a/go/core/pkg/sandboxbackend/openshell/openclaw/bootstrap.go +++ b/go/core/pkg/sandboxbackend/openshell/openclaw/bootstrap.go @@ -39,7 +39,7 @@ func BuildBootstrapJSON(ctx context.Context, kube client.Client, namespace strin providerRecord := GatewayProviderRecordName(mc.Spec.Provider) doc := buildCoreBootstrapDocument(mc, gwPort, apiKeyEnv, providerRecord, modelID, apiAdapter) - chState, err := accumulateHarnessChannels(ctx, kube, namespace, sbx.Spec.Channels, env) + chState, err := accumulateHarnessChannels(ctx, kube, namespace, sbx.Spec.Backend, sbx.Spec.Channels, env) if err != nil { return nil, nil, err } diff --git a/go/core/pkg/sandboxbackend/openshell/openclaw/channels.go b/go/core/pkg/sandboxbackend/openshell/openclaw/channels.go index fd7dde05ce..1223a30f2f 100644 --- a/go/core/pkg/sandboxbackend/openshell/openclaw/channels.go +++ b/go/core/pkg/sandboxbackend/openshell/openclaw/channels.go @@ -13,8 +13,8 @@ type harnessChannels struct { resolved *channels.Resolved } -func accumulateHarnessChannels(ctx context.Context, kube client.Client, namespace string, specChannels []v1alpha2.AgentHarnessChannel, env map[string]string) (*harnessChannels, error) { - resolved, err := channels.Resolve(ctx, kube, namespace, specChannels) +func accumulateHarnessChannels(ctx context.Context, kube client.Client, namespace string, backend v1alpha2.AgentHarnessBackendType, specChannels []v1alpha2.AgentHarnessChannel, env map[string]string) (*harnessChannels, error) { + resolved, err := channels.Resolve(ctx, kube, namespace, backend, specChannels) if err != nil { return nil, err } diff --git a/go/core/pkg/sandboxbackend/openshell/translate_test.go b/go/core/pkg/sandboxbackend/openshell/translate_test.go index 332264719b..cbee2dfc92 100644 --- a/go/core/pkg/sandboxbackend/openshell/translate_test.go +++ b/go/core/pkg/sandboxbackend/openshell/translate_test.go @@ -165,9 +165,11 @@ func TestBuildOpenshellCreateRequest_OpenClaw_Slack_HasSlackPolicy(t *testing.T) Name: "s1", Type: v1alpha2.AgentHarnessChannelTypeSlack, Slack: &v1alpha2.AgentHarnessSlackChannelSpec{ - BotToken: v1alpha2.AgentHarnessChannelCredential{Value: "b"}, - AppToken: v1alpha2.AgentHarnessChannelCredential{Value: "a"}, - ChannelAccess: v1alpha2.AgentHarnessChannelAccessOpen, + BotToken: v1alpha2.AgentHarnessChannelCredential{Value: "b"}, + AppToken: v1alpha2.AgentHarnessChannelCredential{Value: "a"}, + AgentHarnessOpenClawSlackOptions: v1alpha2.AgentHarnessOpenClawSlackOptions{ + ChannelAccess: v1alpha2.AgentHarnessChannelAccessOpen, + }, }, }, }, diff --git a/helm/kagent-crds/templates/kagent.dev_agentharnesses.yaml b/helm/kagent-crds/templates/kagent.dev_agentharnesses.yaml index 8c0cf3d07a..310142bb01 100644 --- a/helm/kagent-crds/templates/kagent.dev_agentharnesses.yaml +++ b/helm/kagent-crds/templates/kagent.dev_agentharnesses.yaml @@ -76,7 +76,7 @@ spec: OpenClaw inside the harness VM. items: description: AgentHarnessChannel declares one messenger binding - inside an OpenClaw/NemoClaw harness VM. + inside a harness VM. properties: name: description: Name is a stable id for this binding (OpenClaw @@ -84,16 +84,11 @@ spec: minLength: 1 type: string slack: - description: |- - AgentHarnessSlackChannelSpec configures Slack when AgentHarnessChannel.type is Slack. - - OpenClaw and NemoClaw use channelAccess, allowlistChannels, and interactiveReplies. - Hermes uses allowedUserIDs (SLACK_ALLOWED_USERS), homeChannel (SLACK_HOME_CHANNEL), - and homeChannelName (SLACK_HOME_CHANNEL_NAME) in the sandbox .env. + description: Slack configures Slack when type is Slack. properties: allowedUserIDs: - description: 'AllowedUserIDs restricts which Slack user - IDs may interact with the bot (Hermes: SLACK_ALLOWED_USERS).' + description: AllowedUserIDs restricts which Slack member + IDs may interact with the bot (SLACK_ALLOWED_USERS). items: type: string type: array @@ -121,7 +116,7 @@ spec: type: object allowlistChannels: description: AllowlistChannels is required when channelAccess - is allowlist (OpenClaw / NemoClaw only). + is allowlist. items: type: string type: array @@ -194,8 +189,8 @@ spec: rule: (has(self.value) && !has(self.valueFrom)) || (!has(self.value) && has(self.valueFrom)) channelAccess: - description: ChannelAccess controls OpenClaw routing (open, - allowlist, disabled). Omit for Hermes harnesses. + description: AgentHarnessChannelAccess controls whether + the bot listens broadly or only on an allowlist. enum: - allowlist - open @@ -203,7 +198,7 @@ spec: type: string homeChannel: description: HomeChannel is the default Slack channel ID - for Hermes cron/scheduled messages (SLACK_HOME_CHANNEL). + for cron/scheduled messages (SLACK_HOME_CHANNEL). type: string homeChannelName: description: HomeChannelName is a human-readable label for @@ -217,14 +212,14 @@ spec: - botToken type: object x-kubernetes-validations: + - message: allowedUserIDs and allowedUserIDsFrom are mutually + exclusive + rule: '!(size(self.allowedUserIDs) > 0 && has(self.allowedUserIDsFrom))' - message: allowlistChannels is required when channelAccess is allowlist rule: '!has(self.channelAccess) || self.channelAccess != ''allowlist'' || (has(self.allowlistChannels) && size(self.allowlistChannels) > 0)' - - message: allowedUserIDs and allowedUserIDsFrom are mutually - exclusive - rule: '!(size(self.allowedUserIDs) > 0 && has(self.allowedUserIDsFrom))' telegram: description: AgentHarnessTelegramChannelSpec configures Telegram when AgentHarnessChannel.type is Telegram. diff --git a/ui/src/components/agent-form/OpenClawSandboxFields.tsx b/ui/src/components/agent-form/OpenClawSandboxFields.tsx index 40d508f82b..8af459e329 100644 --- a/ui/src/components/agent-form/OpenClawSandboxFields.tsx +++ b/ui/src/components/agent-form/OpenClawSandboxFields.tsx @@ -506,8 +506,8 @@ export function OpenClawSandboxFields({ Allowed Slack users (optional) - Restrict who can DM the bot using Slack member IDs (U…). Written to{" "} - SLACK_ALLOWED_USERS. Leave empty to allow all users. + Restrict who can interact with the bot using Slack member IDs (U…). Written to{" "} + SLACK_ALLOWED_USERS as comma-separated values. Leave empty to allow all users. { expect(draft.spec.backend).toBe("openclaw"); }); - it("writes Hermes slack allowedUserIDs without OpenClaw channel access fields", () => { + it("writes Hermes slack allowedUserIDs and home channel fields", () => { const row = newOpenClawChannelRow(); row.name = "slack-main"; row.channelType = "slack"; @@ -206,9 +206,9 @@ describe("openClawSandboxForm allowedDomains", () => { expect(channels[0].slack.allowedUserIDs).toEqual(["U01234567", "U89ABCDEF"]); expect(channels[0].slack.homeChannel).toBe("C01234567890"); expect(channels[0].slack.homeChannelName).toBe("general"); - expect(channels[0].slack.channelAccess).toBeUndefined(); - expect(channels[0].slack.allowlistChannels).toBeUndefined(); - expect(channels[0].slack.interactiveReplies).toBeUndefined(); + expect(channels[0].slack).not.toHaveProperty("channelAccess"); + expect(channels[0].slack).not.toHaveProperty("allowlistChannels"); + expect(channels[0].slack).not.toHaveProperty("interactiveReplies"); }); }); }); diff --git a/ui/src/lib/openClawSandboxForm.ts b/ui/src/lib/openClawSandboxForm.ts index 5d9953da44..cef7d16bb8 100644 --- a/ui/src/lib/openClawSandboxForm.ts +++ b/ui/src/lib/openClawSandboxForm.ts @@ -330,7 +330,9 @@ export function buildSandboxCRDraft(args: { appToken: app, }; if (isClawHarnessBackend(resolveSandboxBackend(args.backend))) { - slack.channelAccess = ch.channelAccess; + if (ch.channelAccess !== "open") { + slack.channelAccess = ch.channelAccess; + } if (ch.channelAccess === "allowlist") { slack.allowlistChannels = trimSplitList(ch.allowlistChannels); } From a9e917ac59ce53c2ee2a02b37b17366bc5f4d7f0 Mon Sep 17 00:00:00 2001 From: Peter Jausovec Date: Tue, 26 May 2026 09:23:48 -0700 Subject: [PATCH 6/8] fix formatting Signed-off-by: Peter Jausovec --- go/api/v1alpha2/agentharness_types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/api/v1alpha2/agentharness_types.go b/go/api/v1alpha2/agentharness_types.go index c6ad4c2e8b..934e58ef0c 100644 --- a/go/api/v1alpha2/agentharness_types.go +++ b/go/api/v1alpha2/agentharness_types.go @@ -117,7 +117,7 @@ type AgentHarnessSlackChannelSpec struct { // +required BotToken AgentHarnessChannelCredential `json:"botToken"` // +required - AppToken AgentHarnessChannelCredential `json:"appToken"` + AppToken AgentHarnessChannelCredential `json:"appToken"` AgentHarnessOpenClawSlackOptions `json:",inline"` AgentHarnessHermesSlackOptions `json:",inline"` } From 0a46f7cd4e7b4be5bb77ae5f9ccb764d71dcd954 Mon Sep 17 00:00:00 2001 From: Peter Jausovec Date: Tue, 26 May 2026 11:20:45 -0700 Subject: [PATCH 7/8] refactor slack configuration to separate backend-specific settings for Hermes and OpenClaw Signed-off-by: Peter Jausovec --- .../crd/bases/kagent.dev_agentharnesses.yaml | 140 ++++++++++-------- go/api/v1alpha2/agentharness_types.go | 31 ++-- go/api/v1alpha2/zz_generated.deepcopy.go | 12 +- .../openshell/channels/resolve.go | 43 +----- .../openshell/hermes/bootstrap_test.go | 2 +- .../openshell/translate_test.go | 2 +- .../templates/kagent.dev_agentharnesses.yaml | 140 ++++++++++-------- 7 files changed, 186 insertions(+), 184 deletions(-) diff --git a/go/api/config/crd/bases/kagent.dev_agentharnesses.yaml b/go/api/config/crd/bases/kagent.dev_agentharnesses.yaml index 310142bb01..3294ccd922 100644 --- a/go/api/config/crd/bases/kagent.dev_agentharnesses.yaml +++ b/go/api/config/crd/bases/kagent.dev_agentharnesses.yaml @@ -86,40 +86,6 @@ spec: slack: description: Slack configures Slack when type is Slack. properties: - allowedUserIDs: - description: AllowedUserIDs restricts which Slack member - IDs may interact with the bot (SLACK_ALLOWED_USERS). - items: - type: string - type: array - allowedUserIDsFrom: - description: ValueSource defines a source for configuration - values from a Secret or ConfigMap - properties: - key: - description: The key of the ConfigMap or Secret. - maxLength: 253 - type: string - name: - description: The name of the ConfigMap or Secret. - maxLength: 253 - type: string - type: - enum: - - ConfigMap - - Secret - type: string - required: - - key - - name - - type - type: object - allowlistChannels: - description: AllowlistChannels is required when channelAccess - is allowlist. - items: - type: string - type: array appToken: description: AgentHarnessChannelCredential supplies a token from an inline value or a Secret/ConfigMap key. @@ -188,38 +154,82 @@ spec: - message: Exactly one of value or valueFrom must be specified rule: (has(self.value) && !has(self.valueFrom)) || (!has(self.value) && has(self.valueFrom)) - channelAccess: - description: AgentHarnessChannelAccess controls whether - the bot listens broadly or only on an allowlist. - enum: - - allowlist - - open - - disabled - type: string - homeChannel: - description: HomeChannel is the default Slack channel ID - for cron/scheduled messages (SLACK_HOME_CHANNEL). - type: string - homeChannelName: - description: HomeChannelName is a human-readable label for - HomeChannel (SLACK_HOME_CHANNEL_NAME). - type: string - interactiveReplies: - default: true - type: boolean + hermes: + description: Hermes configures Hermes-specific Slack settings. + properties: + allowedUserIDs: + description: AllowedUserIDs restricts which Slack member + IDs may interact with the bot (SLACK_ALLOWED_USERS). + items: + type: string + type: array + allowedUserIDsFrom: + description: ValueSource defines a source for configuration + values from a Secret or ConfigMap + properties: + key: + description: The key of the ConfigMap or Secret. + maxLength: 253 + type: string + name: + description: The name of the ConfigMap or Secret. + maxLength: 253 + type: string + type: + enum: + - ConfigMap + - Secret + type: string + required: + - key + - name + - type + type: object + homeChannel: + description: HomeChannel is the default Slack channel + ID for cron/scheduled messages (SLACK_HOME_CHANNEL). + type: string + homeChannelName: + description: HomeChannelName is a human-readable label + for HomeChannel (SLACK_HOME_CHANNEL_NAME). + type: string + type: object + x-kubernetes-validations: + - message: allowedUserIDs and allowedUserIDsFrom are mutually + exclusive + rule: '!(size(self.allowedUserIDs) > 0 && has(self.allowedUserIDsFrom))' + openclaw: + description: OpenClaw configures OpenClaw/NemoClaw-specific + Slack routing. + properties: + allowlistChannels: + description: AllowlistChannels is required when channelAccess + is allowlist. + items: + type: string + type: array + channelAccess: + description: AgentHarnessChannelAccess controls whether + the bot listens broadly or only on an allowlist. + enum: + - allowlist + - open + - disabled + type: string + interactiveReplies: + default: true + type: boolean + type: object + x-kubernetes-validations: + - message: allowlistChannels is required when channelAccess + is allowlist + rule: '!has(self.channelAccess) || self.channelAccess + != ''allowlist'' || (has(self.allowlistChannels) && + size(self.allowlistChannels) > 0)' required: - appToken - botToken type: object - x-kubernetes-validations: - - message: allowedUserIDs and allowedUserIDsFrom are mutually - exclusive - rule: '!(size(self.allowedUserIDs) > 0 && has(self.allowedUserIDsFrom))' - - message: allowlistChannels is required when channelAccess - is allowlist - rule: '!has(self.channelAccess) || self.channelAccess != ''allowlist'' - || (has(self.allowlistChannels) && size(self.allowlistChannels) - > 0)' telegram: description: AgentHarnessTelegramChannelSpec configures Telegram when AgentHarnessChannel.type is Telegram. @@ -500,6 +510,12 @@ spec: required: - backend type: object + x-kubernetes-validations: + - message: slack backend-specific settings must match spec.backend + rule: '!has(self.channels) || self.channels.all(c, c.type != ''slack'' + || (has(c.slack) && ((self.backend == ''hermes'' && has(c.slack.hermes) + && !has(c.slack.openclaw)) || ((self.backend == ''openclaw'' || self.backend + == ''nemoclaw'') && has(c.slack.openclaw) && !has(c.slack.hermes)))))' status: description: AgentHarnessStatus is the observed state of an AgentHarness. properties: diff --git a/go/api/v1alpha2/agentharness_types.go b/go/api/v1alpha2/agentharness_types.go index 934e58ef0c..909d09d85d 100644 --- a/go/api/v1alpha2/agentharness_types.go +++ b/go/api/v1alpha2/agentharness_types.go @@ -111,31 +111,19 @@ type AgentHarnessHermesSlackOptions struct { } // AgentHarnessSlackChannelSpec configures Slack when AgentHarnessChannel.type is Slack. -// YAML is flat: botToken, appToken, plus backend-specific fields. Which fields apply is determined -// by spec.backend on the AgentHarness; OpenClaw and Hermes settings are separate structs in Go. +// Backend-specific settings live under the matching backend key; AgentHarnessSpec validation +// requires the key to match spec.backend. type AgentHarnessSlackChannelSpec struct { // +required BotToken AgentHarnessChannelCredential `json:"botToken"` // +required - AppToken AgentHarnessChannelCredential `json:"appToken"` - AgentHarnessOpenClawSlackOptions `json:",inline"` - AgentHarnessHermesSlackOptions `json:",inline"` -} - -// OpenClawOptions returns OpenClaw/NemoClaw Slack settings embedded in the spec. -func (s *AgentHarnessSlackChannelSpec) OpenClawOptions() *AgentHarnessOpenClawSlackOptions { - if s == nil { - return nil - } - return &s.AgentHarnessOpenClawSlackOptions -} - -// HermesOptions returns Hermes Slack settings embedded in the spec. -func (s *AgentHarnessSlackChannelSpec) HermesOptions() *AgentHarnessHermesSlackOptions { - if s == nil { - return nil - } - return &s.AgentHarnessHermesSlackOptions + AppToken AgentHarnessChannelCredential `json:"appToken"` + // OpenClaw configures OpenClaw/NemoClaw-specific Slack routing. + // +optional + OpenClaw *AgentHarnessOpenClawSlackOptions `json:"openclaw,omitempty"` + // Hermes configures Hermes-specific Slack settings. + // +optional + Hermes *AgentHarnessHermesSlackOptions `json:"hermes,omitempty"` } // AgentHarnessChannel declares one messenger binding inside a harness VM. @@ -161,6 +149,7 @@ type AgentHarnessChannel struct { // An AgentHarness is distinct from a SandboxAgent: it has no agent runtime baked // in. The backend is responsible for provisioning an environment that stays // ready to accept incoming commands. +// +kubebuilder:validation:XValidation:rule="!has(self.channels) || self.channels.all(c, c.type != 'slack' || (has(c.slack) && ((self.backend == 'hermes' && has(c.slack.hermes) && !has(c.slack.openclaw)) || ((self.backend == 'openclaw' || self.backend == 'nemoclaw') && has(c.slack.openclaw) && !has(c.slack.hermes)))))",message="slack backend-specific settings must match spec.backend" type AgentHarnessSpec struct { // Backend selects the control plane to use. Required. // +required diff --git a/go/api/v1alpha2/zz_generated.deepcopy.go b/go/api/v1alpha2/zz_generated.deepcopy.go index ad1233c84e..52d10ed714 100644 --- a/go/api/v1alpha2/zz_generated.deepcopy.go +++ b/go/api/v1alpha2/zz_generated.deepcopy.go @@ -270,8 +270,16 @@ func (in *AgentHarnessSlackChannelSpec) DeepCopyInto(out *AgentHarnessSlackChann *out = *in in.BotToken.DeepCopyInto(&out.BotToken) in.AppToken.DeepCopyInto(&out.AppToken) - in.AgentHarnessOpenClawSlackOptions.DeepCopyInto(&out.AgentHarnessOpenClawSlackOptions) - in.AgentHarnessHermesSlackOptions.DeepCopyInto(&out.AgentHarnessHermesSlackOptions) + if in.OpenClaw != nil { + in, out := &in.OpenClaw, &out.OpenClaw + *out = new(AgentHarnessOpenClawSlackOptions) + (*in).DeepCopyInto(*out) + } + if in.Hermes != nil { + in, out := &in.Hermes, &out.Hermes + *out = new(AgentHarnessHermesSlackOptions) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentHarnessSlackChannelSpec. diff --git a/go/core/pkg/sandboxbackend/openshell/channels/resolve.go b/go/core/pkg/sandboxbackend/openshell/channels/resolve.go index 24d97242c5..8f424d3d62 100644 --- a/go/core/pkg/sandboxbackend/openshell/channels/resolve.go +++ b/go/core/pkg/sandboxbackend/openshell/channels/resolve.go @@ -104,49 +104,22 @@ func (r *Resolved) addSlackChannel(ctx context.Context, kube client.Client, name } switch backend { case v1alpha2.AgentHarnessBackendHermes: - if err := rejectOpenClawSlackFields(ch.Name, spec); err != nil { - return err + opts := spec.Hermes + if opts == nil { + opts = &v1alpha2.AgentHarnessHermesSlackOptions{} } - return r.addHermesSlack(ctx, kube, namespace, ch.Name, spec.BotToken, spec.AppToken, spec.HermesOptions()) + return r.addHermesSlack(ctx, kube, namespace, ch.Name, spec.BotToken, spec.AppToken, opts) case v1alpha2.AgentHarnessBackendOpenClaw, v1alpha2.AgentHarnessBackendNemoClaw: - if err := rejectHermesSlackFields(ch.Name, spec); err != nil { - return err + opts := spec.OpenClaw + if opts == nil { + opts = &v1alpha2.AgentHarnessOpenClawSlackOptions{} } - return r.addOpenClawSlack(ctx, kube, namespace, ch.Name, spec.BotToken, spec.AppToken, spec.OpenClawOptions()) + return r.addOpenClawSlack(ctx, kube, namespace, ch.Name, spec.BotToken, spec.AppToken, opts) default: return fmt.Errorf("channel %q: slack channels are not supported for backend %q", ch.Name, backend) } } -func rejectOpenClawSlackFields(channelName string, spec *v1alpha2.AgentHarnessSlackChannelSpec) error { - opts := spec.OpenClawOptions() - if opts == nil { - return nil - } - if opts.ChannelAccess != "" { - return fmt.Errorf("channel %q: channelAccess is not supported when backend is hermes", channelName) - } - if len(opts.AllowlistChannels) > 0 { - return fmt.Errorf("channel %q: allowlistChannels is not supported when backend is hermes", channelName) - } - if opts.InteractiveReplies != nil { - return fmt.Errorf("channel %q: interactiveReplies is not supported when backend is hermes", channelName) - } - return nil -} - -func rejectHermesSlackFields(channelName string, spec *v1alpha2.AgentHarnessSlackChannelSpec) error { - opts := spec.HermesOptions() - if opts == nil { - return nil - } - if len(opts.AllowedUserIDs) > 0 || opts.AllowedUserIDsFrom != nil || - strings.TrimSpace(opts.HomeChannel) != "" || strings.TrimSpace(opts.HomeChannelName) != "" { - return fmt.Errorf("channel %q: Hermes slack fields are not supported when backend is openclaw or nemoclaw", channelName) - } - return nil -} - func (r *Resolved) putSlackCredentials(ctx context.Context, kube client.Client, namespace, channelName string, botToken, appToken v1alpha2.AgentHarnessChannelCredential) error { if err := PutChannelCredential(ctx, kube, namespace, botToken, SlackBotTokenEnvKey(channelName), r.Secrets); err != nil { return fmt.Errorf("channel %q slack bot token: %w", channelName, err) diff --git a/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap_test.go b/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap_test.go index 1996c8653e..57a3b870b2 100644 --- a/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap_test.go +++ b/go/core/pkg/sandboxbackend/openshell/hermes/bootstrap_test.go @@ -73,7 +73,7 @@ func TestBuildBootstrapArtifacts_TelegramSlack(t *testing.T) { Slack: &v1alpha2.AgentHarnessSlackChannelSpec{ BotToken: v1alpha2.AgentHarnessChannelCredential{Value: "xoxb-bot"}, AppToken: v1alpha2.AgentHarnessChannelCredential{Value: "xapp-app"}, - AgentHarnessHermesSlackOptions: v1alpha2.AgentHarnessHermesSlackOptions{ + Hermes: &v1alpha2.AgentHarnessHermesSlackOptions{ AllowedUserIDs: []string{"U01234567", "U89ABCDEF"}, HomeChannel: "C01234567890", HomeChannelName: "general", diff --git a/go/core/pkg/sandboxbackend/openshell/translate_test.go b/go/core/pkg/sandboxbackend/openshell/translate_test.go index cbee2dfc92..463ad5aa14 100644 --- a/go/core/pkg/sandboxbackend/openshell/translate_test.go +++ b/go/core/pkg/sandboxbackend/openshell/translate_test.go @@ -167,7 +167,7 @@ func TestBuildOpenshellCreateRequest_OpenClaw_Slack_HasSlackPolicy(t *testing.T) Slack: &v1alpha2.AgentHarnessSlackChannelSpec{ BotToken: v1alpha2.AgentHarnessChannelCredential{Value: "b"}, AppToken: v1alpha2.AgentHarnessChannelCredential{Value: "a"}, - AgentHarnessOpenClawSlackOptions: v1alpha2.AgentHarnessOpenClawSlackOptions{ + OpenClaw: &v1alpha2.AgentHarnessOpenClawSlackOptions{ ChannelAccess: v1alpha2.AgentHarnessChannelAccessOpen, }, }, diff --git a/helm/kagent-crds/templates/kagent.dev_agentharnesses.yaml b/helm/kagent-crds/templates/kagent.dev_agentharnesses.yaml index 310142bb01..3294ccd922 100644 --- a/helm/kagent-crds/templates/kagent.dev_agentharnesses.yaml +++ b/helm/kagent-crds/templates/kagent.dev_agentharnesses.yaml @@ -86,40 +86,6 @@ spec: slack: description: Slack configures Slack when type is Slack. properties: - allowedUserIDs: - description: AllowedUserIDs restricts which Slack member - IDs may interact with the bot (SLACK_ALLOWED_USERS). - items: - type: string - type: array - allowedUserIDsFrom: - description: ValueSource defines a source for configuration - values from a Secret or ConfigMap - properties: - key: - description: The key of the ConfigMap or Secret. - maxLength: 253 - type: string - name: - description: The name of the ConfigMap or Secret. - maxLength: 253 - type: string - type: - enum: - - ConfigMap - - Secret - type: string - required: - - key - - name - - type - type: object - allowlistChannels: - description: AllowlistChannels is required when channelAccess - is allowlist. - items: - type: string - type: array appToken: description: AgentHarnessChannelCredential supplies a token from an inline value or a Secret/ConfigMap key. @@ -188,38 +154,82 @@ spec: - message: Exactly one of value or valueFrom must be specified rule: (has(self.value) && !has(self.valueFrom)) || (!has(self.value) && has(self.valueFrom)) - channelAccess: - description: AgentHarnessChannelAccess controls whether - the bot listens broadly or only on an allowlist. - enum: - - allowlist - - open - - disabled - type: string - homeChannel: - description: HomeChannel is the default Slack channel ID - for cron/scheduled messages (SLACK_HOME_CHANNEL). - type: string - homeChannelName: - description: HomeChannelName is a human-readable label for - HomeChannel (SLACK_HOME_CHANNEL_NAME). - type: string - interactiveReplies: - default: true - type: boolean + hermes: + description: Hermes configures Hermes-specific Slack settings. + properties: + allowedUserIDs: + description: AllowedUserIDs restricts which Slack member + IDs may interact with the bot (SLACK_ALLOWED_USERS). + items: + type: string + type: array + allowedUserIDsFrom: + description: ValueSource defines a source for configuration + values from a Secret or ConfigMap + properties: + key: + description: The key of the ConfigMap or Secret. + maxLength: 253 + type: string + name: + description: The name of the ConfigMap or Secret. + maxLength: 253 + type: string + type: + enum: + - ConfigMap + - Secret + type: string + required: + - key + - name + - type + type: object + homeChannel: + description: HomeChannel is the default Slack channel + ID for cron/scheduled messages (SLACK_HOME_CHANNEL). + type: string + homeChannelName: + description: HomeChannelName is a human-readable label + for HomeChannel (SLACK_HOME_CHANNEL_NAME). + type: string + type: object + x-kubernetes-validations: + - message: allowedUserIDs and allowedUserIDsFrom are mutually + exclusive + rule: '!(size(self.allowedUserIDs) > 0 && has(self.allowedUserIDsFrom))' + openclaw: + description: OpenClaw configures OpenClaw/NemoClaw-specific + Slack routing. + properties: + allowlistChannels: + description: AllowlistChannels is required when channelAccess + is allowlist. + items: + type: string + type: array + channelAccess: + description: AgentHarnessChannelAccess controls whether + the bot listens broadly or only on an allowlist. + enum: + - allowlist + - open + - disabled + type: string + interactiveReplies: + default: true + type: boolean + type: object + x-kubernetes-validations: + - message: allowlistChannels is required when channelAccess + is allowlist + rule: '!has(self.channelAccess) || self.channelAccess + != ''allowlist'' || (has(self.allowlistChannels) && + size(self.allowlistChannels) > 0)' required: - appToken - botToken type: object - x-kubernetes-validations: - - message: allowedUserIDs and allowedUserIDsFrom are mutually - exclusive - rule: '!(size(self.allowedUserIDs) > 0 && has(self.allowedUserIDsFrom))' - - message: allowlistChannels is required when channelAccess - is allowlist - rule: '!has(self.channelAccess) || self.channelAccess != ''allowlist'' - || (has(self.allowlistChannels) && size(self.allowlistChannels) - > 0)' telegram: description: AgentHarnessTelegramChannelSpec configures Telegram when AgentHarnessChannel.type is Telegram. @@ -500,6 +510,12 @@ spec: required: - backend type: object + x-kubernetes-validations: + - message: slack backend-specific settings must match spec.backend + rule: '!has(self.channels) || self.channels.all(c, c.type != ''slack'' + || (has(c.slack) && ((self.backend == ''hermes'' && has(c.slack.hermes) + && !has(c.slack.openclaw)) || ((self.backend == ''openclaw'' || self.backend + == ''nemoclaw'') && has(c.slack.openclaw) && !has(c.slack.hermes)))))' status: description: AgentHarnessStatus is the observed state of an AgentHarness. properties: From 2cf1f138356bf07b5f756270370604c6c855329a Mon Sep 17 00:00:00 2001 From: Peter Jausovec Date: Tue, 26 May 2026 12:15:28 -0700 Subject: [PATCH 8/8] fix maxitems Signed-off-by: Peter Jausovec --- go/api/config/crd/bases/kagent.dev_agentharnesses.yaml | 4 ++++ go/api/v1alpha2/agentharness_types.go | 4 ++++ helm/kagent-crds/templates/kagent.dev_agentharnesses.yaml | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/go/api/config/crd/bases/kagent.dev_agentharnesses.yaml b/go/api/config/crd/bases/kagent.dev_agentharnesses.yaml index 3294ccd922..308d7ba0f2 100644 --- a/go/api/config/crd/bases/kagent.dev_agentharnesses.yaml +++ b/go/api/config/crd/bases/kagent.dev_agentharnesses.yaml @@ -162,6 +162,7 @@ spec: IDs may interact with the bot (SLACK_ALLOWED_USERS). items: type: string + maxItems: 1024 type: array allowedUserIDsFrom: description: ValueSource defines a source for configuration @@ -207,6 +208,7 @@ spec: is allowlist. items: type: string + maxItems: 1024 type: array channelAccess: description: AgentHarnessChannelAccess controls whether @@ -237,6 +239,7 @@ spec: allowedUserIDs: items: type: string + maxItems: 1024 type: array allowedUserIDsFrom: description: ValueSource defines a source for configuration @@ -317,6 +320,7 @@ spec: match type rule: (self.type == 'telegram' && has(self.telegram) && !has(self.slack)) || (self.type == 'slack' && has(self.slack) && !has(self.telegram)) + maxItems: 1024 type: array description: description: Description is a short human-readable summary shown in diff --git a/go/api/v1alpha2/agentharness_types.go b/go/api/v1alpha2/agentharness_types.go index 909d09d85d..c6a43f6c02 100644 --- a/go/api/v1alpha2/agentharness_types.go +++ b/go/api/v1alpha2/agentharness_types.go @@ -74,6 +74,7 @@ type AgentHarnessTelegramChannelSpec struct { // +required BotToken AgentHarnessChannelCredential `json:"botToken"` // +optional + // +kubebuilder:validation:MaxItems=1024 AllowedUserIDs []string `json:"allowedUserIDs,omitempty"` // +optional AllowedUserIDsFrom *ValueSource `json:"allowedUserIDsFrom,omitempty"` @@ -87,6 +88,7 @@ type AgentHarnessOpenClawSlackOptions struct { ChannelAccess AgentHarnessChannelAccess `json:"channelAccess,omitempty"` // AllowlistChannels is required when channelAccess is allowlist. // +optional + // +kubebuilder:validation:MaxItems=1024 AllowlistChannels []string `json:"allowlistChannels,omitempty"` // +optional // +kubebuilder:default=true @@ -99,6 +101,7 @@ type AgentHarnessOpenClawSlackOptions struct { type AgentHarnessHermesSlackOptions struct { // AllowedUserIDs restricts which Slack member IDs may interact with the bot (SLACK_ALLOWED_USERS). // +optional + // +kubebuilder:validation:MaxItems=1024 AllowedUserIDs []string `json:"allowedUserIDs,omitempty"` // +optional AllowedUserIDsFrom *ValueSource `json:"allowedUserIDsFrom,omitempty"` @@ -185,6 +188,7 @@ type AgentHarnessSpec struct { // Channels configures Telegram and Slack integrations for OpenClaw inside the harness VM. // +optional + // +kubebuilder:validation:MaxItems=1024 Channels []AgentHarnessChannel `json:"channels,omitempty"` } diff --git a/helm/kagent-crds/templates/kagent.dev_agentharnesses.yaml b/helm/kagent-crds/templates/kagent.dev_agentharnesses.yaml index 3294ccd922..308d7ba0f2 100644 --- a/helm/kagent-crds/templates/kagent.dev_agentharnesses.yaml +++ b/helm/kagent-crds/templates/kagent.dev_agentharnesses.yaml @@ -162,6 +162,7 @@ spec: IDs may interact with the bot (SLACK_ALLOWED_USERS). items: type: string + maxItems: 1024 type: array allowedUserIDsFrom: description: ValueSource defines a source for configuration @@ -207,6 +208,7 @@ spec: is allowlist. items: type: string + maxItems: 1024 type: array channelAccess: description: AgentHarnessChannelAccess controls whether @@ -237,6 +239,7 @@ spec: allowedUserIDs: items: type: string + maxItems: 1024 type: array allowedUserIDsFrom: description: ValueSource defines a source for configuration @@ -317,6 +320,7 @@ spec: match type rule: (self.type == 'telegram' && has(self.telegram) && !has(self.slack)) || (self.type == 'slack' && has(self.slack) && !has(self.telegram)) + maxItems: 1024 type: array description: description: Description is a short human-readable summary shown in