diff --git a/Cargo.lock b/Cargo.lock
index c936b824e..5361af655 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2627,6 +2627,7 @@ dependencies = [
"base64 0.22.1",
"chrono",
"dirs",
+ "futures-util",
"globset",
"ignore",
"lopdf",
@@ -2652,6 +2653,7 @@ dependencies = [
"tokio",
"tokio-cron-scheduler",
"tokio-stream",
+ "tokio-tungstenite",
"tonic",
"tonic-build",
"uuid",
diff --git a/crates/agent-gateway/embed.go b/crates/agent-gateway/embed.go
index 93eb21e1b..fe1d98ef4 100644
--- a/crates/agent-gateway/embed.go
+++ b/crates/agent-gateway/embed.go
@@ -4,5 +4,9 @@ import "embed"
// WebUIAssets contains the embedded WebUI build output served by the HTTP server.
//
-//go:embed web/dist
+// The all: prefix is required because Vite may emit chunks whose names begin
+// with "_" (for example, lodash's _baseFor chunk). Plain directory embeds
+// silently exclude files and directories beginning with "." or "_".
+//
+//go:embed all:web/dist
var WebUIAssets embed.FS
diff --git a/crates/agent-gateway/embed_test.go b/crates/agent-gateway/embed_test.go
new file mode 100644
index 000000000..1596c6788
--- /dev/null
+++ b/crates/agent-gateway/embed_test.go
@@ -0,0 +1,56 @@
+package gateway
+
+import (
+ "io/fs"
+ "os"
+ "sort"
+ "testing"
+)
+
+func TestWebUIAssetsIncludeEntireDistTree(t *testing.T) {
+ diskFiles := regularFileSizes(t, os.DirFS("."), "web/dist")
+ embeddedFiles := regularFileSizes(t, WebUIAssets, "web/dist")
+
+ var missing []string
+ for file, size := range diskFiles {
+ embeddedSize, ok := embeddedFiles[file]
+ if !ok {
+ missing = append(missing, file)
+ continue
+ }
+ if embeddedSize != size {
+ t.Fatalf("embedded WebUI asset %q size = %d, want %d", file, embeddedSize, size)
+ }
+ }
+
+ if len(missing) > 0 {
+ sort.Strings(missing)
+ t.Fatalf("embedded WebUI assets are missing files from web/dist: %v", missing)
+ }
+}
+
+func regularFileSizes(t *testing.T, fileSystem fs.FS, root string) map[string]int64 {
+ t.Helper()
+
+ files := make(map[string]int64)
+ err := fs.WalkDir(fileSystem, root, func(path string, entry fs.DirEntry, walkErr error) error {
+ if walkErr != nil {
+ return walkErr
+ }
+ if entry.IsDir() {
+ return nil
+ }
+ info, err := entry.Info()
+ if err != nil {
+ return err
+ }
+ if info.Mode().IsRegular() {
+ files[path] = info.Size()
+ }
+ return nil
+ })
+ if err != nil {
+ t.Fatalf("walk %q: %v", root, err)
+ }
+ return files
+}
diff --git a/crates/agent-gateway/internal/config/config.go b/crates/agent-gateway/internal/config/config.go
index ad699a7de..eedf50bbb 100644
--- a/crates/agent-gateway/internal/config/config.go
+++ b/crates/agent-gateway/internal/config/config.go
@@ -17,6 +17,8 @@ type Config struct {
TLSCert string
TLSKey string
RequestTimeout time.Duration
+ ChatStartTimeout time.Duration
+ ChatRenderStartTimeout time.Duration
HeartbeatPeriod time.Duration
WebSocketHeartbeatPeriod time.Duration
WebSocketWriteTimeout time.Duration
@@ -32,6 +34,8 @@ func Load() *Config {
flag.StringVar(&cfg.TLSCert, "tls-cert", getenv("LIVEAGENT_GATEWAY_TLS_CERT", ""), "TLS certificate path")
flag.StringVar(&cfg.TLSKey, "tls-key", getenv("LIVEAGENT_GATEWAY_TLS_KEY", ""), "TLS private key path")
flag.DurationVar(&cfg.RequestTimeout, "request-timeout", getenvDuration("LIVEAGENT_GATEWAY_REQUEST_TIMEOUT", 2*time.Minute), "request timeout for non-streaming API calls")
+ flag.DurationVar(&cfg.ChatStartTimeout, "chat-start-timeout", getenvDuration("LIVEAGENT_GATEWAY_CHAT_START_TIMEOUT", 15*time.Second), "timeout waiting for the desktop backend to accept a remote chat request")
+ flag.DurationVar(&cfg.ChatRenderStartTimeout, "chat-render-start-timeout", getenvDuration("LIVEAGENT_GATEWAY_CHAT_RENDER_START_TIMEOUT", 45*time.Second), "timeout waiting for the desktop app to start an accepted remote chat request")
flag.DurationVar(&cfg.HeartbeatPeriod, "heartbeat-period", getenvDuration("LIVEAGENT_GATEWAY_HEARTBEAT_PERIOD", 30*time.Second), "ping interval for agent connection")
flag.DurationVar(&cfg.WebSocketHeartbeatPeriod, "websocket-heartbeat-period", getenvDuration("LIVEAGENT_GATEWAY_WS_HEARTBEAT_PERIOD", 15*time.Second), "ping interval for browser WebSocket connections")
flag.DurationVar(&cfg.WebSocketWriteTimeout, "websocket-write-timeout", getenvDuration("LIVEAGENT_GATEWAY_WS_WRITE_TIMEOUT", 10*time.Second), "write timeout for browser WebSocket connections")
@@ -49,6 +53,12 @@ func Load() *Config {
if cfg.GRPCMaxMessageBytes <= 0 {
cfg.GRPCMaxMessageBytes = DefaultGRPCMaxMessageBytes
}
+ if cfg.ChatStartTimeout <= 0 {
+ cfg.ChatStartTimeout = 15 * time.Second
+ }
+ if cfg.ChatRenderStartTimeout <= 0 {
+ cfg.ChatRenderStartTimeout = 45 * time.Second
+ }
if cfg.WebSocketHeartbeatPeriod <= 0 {
cfg.WebSocketHeartbeatPeriod = 15 * time.Second
}
diff --git a/crates/agent-gateway/internal/proto/v1/gateway.pb.go b/crates/agent-gateway/internal/proto/v1/gateway.pb.go
index 0d65fb222..7c159763e 100644
--- a/crates/agent-gateway/internal/proto/v1/gateway.pb.go
+++ b/crates/agent-gateway/internal/proto/v1/gateway.pb.go
@@ -21,6 +21,82 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
+type TunnelFrameKind int32
+
+const (
+ TunnelFrameKind_TUNNEL_FRAME_KIND_UNSPECIFIED TunnelFrameKind = 0
+ TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_REQUEST_START TunnelFrameKind = 1
+ TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_REQUEST_BODY TunnelFrameKind = 2
+ TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_REQUEST_END TunnelFrameKind = 3
+ TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_START TunnelFrameKind = 4
+ TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY TunnelFrameKind = 5
+ TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_END TunnelFrameKind = 6
+ TunnelFrameKind_TUNNEL_FRAME_KIND_WS_OPEN TunnelFrameKind = 7
+ TunnelFrameKind_TUNNEL_FRAME_KIND_WS_FRAME TunnelFrameKind = 8
+ TunnelFrameKind_TUNNEL_FRAME_KIND_WS_CLOSE TunnelFrameKind = 9
+ TunnelFrameKind_TUNNEL_FRAME_KIND_ERROR TunnelFrameKind = 10
+ TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL TunnelFrameKind = 11
+)
+
+// Enum value maps for TunnelFrameKind.
+var (
+ TunnelFrameKind_name = map[int32]string{
+ 0: "TUNNEL_FRAME_KIND_UNSPECIFIED",
+ 1: "TUNNEL_FRAME_KIND_HTTP_REQUEST_START",
+ 2: "TUNNEL_FRAME_KIND_HTTP_REQUEST_BODY",
+ 3: "TUNNEL_FRAME_KIND_HTTP_REQUEST_END",
+ 4: "TUNNEL_FRAME_KIND_HTTP_RESPONSE_START",
+ 5: "TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY",
+ 6: "TUNNEL_FRAME_KIND_HTTP_RESPONSE_END",
+ 7: "TUNNEL_FRAME_KIND_WS_OPEN",
+ 8: "TUNNEL_FRAME_KIND_WS_FRAME",
+ 9: "TUNNEL_FRAME_KIND_WS_CLOSE",
+ 10: "TUNNEL_FRAME_KIND_ERROR",
+ 11: "TUNNEL_FRAME_KIND_CANCEL",
+ }
+ TunnelFrameKind_value = map[string]int32{
+ "TUNNEL_FRAME_KIND_UNSPECIFIED": 0,
+ "TUNNEL_FRAME_KIND_HTTP_REQUEST_START": 1,
+ "TUNNEL_FRAME_KIND_HTTP_REQUEST_BODY": 2,
+ "TUNNEL_FRAME_KIND_HTTP_REQUEST_END": 3,
+ "TUNNEL_FRAME_KIND_HTTP_RESPONSE_START": 4,
+ "TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY": 5,
+ "TUNNEL_FRAME_KIND_HTTP_RESPONSE_END": 6,
+ "TUNNEL_FRAME_KIND_WS_OPEN": 7,
+ "TUNNEL_FRAME_KIND_WS_FRAME": 8,
+ "TUNNEL_FRAME_KIND_WS_CLOSE": 9,
+ "TUNNEL_FRAME_KIND_ERROR": 10,
+ "TUNNEL_FRAME_KIND_CANCEL": 11,
+ }
+)
+
+func (x TunnelFrameKind) Enum() *TunnelFrameKind {
+ p := new(TunnelFrameKind)
+ *p = x
+ return p
+}
+
+func (x TunnelFrameKind) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (TunnelFrameKind) Descriptor() protoreflect.EnumDescriptor {
+ return file_proto_v1_gateway_proto_enumTypes[0].Descriptor()
+}
+
+func (TunnelFrameKind) Type() protoreflect.EnumType {
+ return &file_proto_v1_gateway_proto_enumTypes[0]
+}
+
+func (x TunnelFrameKind) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use TunnelFrameKind.Descriptor instead.
+func (TunnelFrameKind) EnumDescriptor() ([]byte, []int) {
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{0}
+}
+
type ChatEvent_ChatEventType int32
const (
@@ -69,11 +145,11 @@ func (x ChatEvent_ChatEventType) String() string {
}
func (ChatEvent_ChatEventType) Descriptor() protoreflect.EnumDescriptor {
- return file_proto_v1_gateway_proto_enumTypes[0].Descriptor()
+ return file_proto_v1_gateway_proto_enumTypes[1].Descriptor()
}
func (ChatEvent_ChatEventType) Type() protoreflect.EnumType {
- return &file_proto_v1_gateway_proto_enumTypes[0]
+ return &file_proto_v1_gateway_proto_enumTypes[1]
}
func (x ChatEvent_ChatEventType) Number() protoreflect.EnumNumber {
@@ -82,7 +158,7 @@ func (x ChatEvent_ChatEventType) Number() protoreflect.EnumNumber {
// Deprecated: Use ChatEvent_ChatEventType.Descriptor instead.
func (ChatEvent_ChatEventType) EnumDescriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{23, 0}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{28, 0}
}
type AuthRequest struct {
@@ -248,6 +324,9 @@ type GatewayEnvelope struct {
// *GatewayEnvelope_GitRequest
// *GatewayEnvelope_FsReadEditableText
// *GatewayEnvelope_FsReadWorkspaceImage
+ // *GatewayEnvelope_TunnelControl
+ // *GatewayEnvelope_TunnelControlResp
+ // *GatewayEnvelope_TunnelFrame
Payload isGatewayEnvelope_Payload `protobuf_oneof:"payload"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
@@ -637,6 +716,33 @@ func (x *GatewayEnvelope) GetFsReadWorkspaceImage() *FsReadWorkspaceImageRequest
return nil
}
+func (x *GatewayEnvelope) GetTunnelControl() *TunnelControlRequest {
+ if x != nil {
+ if x, ok := x.Payload.(*GatewayEnvelope_TunnelControl); ok {
+ return x.TunnelControl
+ }
+ }
+ return nil
+}
+
+func (x *GatewayEnvelope) GetTunnelControlResp() *TunnelControlResponse {
+ if x != nil {
+ if x, ok := x.Payload.(*GatewayEnvelope_TunnelControlResp); ok {
+ return x.TunnelControlResp
+ }
+ }
+ return nil
+}
+
+func (x *GatewayEnvelope) GetTunnelFrame() *TunnelFrame {
+ if x != nil {
+ if x, ok := x.Payload.(*GatewayEnvelope_TunnelFrame); ok {
+ return x.TunnelFrame
+ }
+ }
+ return nil
+}
+
type isGatewayEnvelope_Payload interface {
isGatewayEnvelope_Payload()
}
@@ -789,6 +895,18 @@ type GatewayEnvelope_FsReadWorkspaceImage struct {
FsReadWorkspaceImage *FsReadWorkspaceImageRequest `protobuf:"bytes,63,opt,name=fs_read_workspace_image,json=fsReadWorkspaceImage,proto3,oneof"`
}
+type GatewayEnvelope_TunnelControl struct {
+ TunnelControl *TunnelControlRequest `protobuf:"bytes,67,opt,name=tunnel_control,json=tunnelControl,proto3,oneof"`
+}
+
+type GatewayEnvelope_TunnelControlResp struct {
+ TunnelControlResp *TunnelControlResponse `protobuf:"bytes,68,opt,name=tunnel_control_resp,json=tunnelControlResp,proto3,oneof"`
+}
+
+type GatewayEnvelope_TunnelFrame struct {
+ TunnelFrame *TunnelFrame `protobuf:"bytes,69,opt,name=tunnel_frame,json=tunnelFrame,proto3,oneof"`
+}
+
func (*GatewayEnvelope_ChatRequest) isGatewayEnvelope_Payload() {}
func (*GatewayEnvelope_CancelChat) isGatewayEnvelope_Payload() {}
@@ -863,6 +981,12 @@ func (*GatewayEnvelope_FsReadEditableText) isGatewayEnvelope_Payload() {}
func (*GatewayEnvelope_FsReadWorkspaceImage) isGatewayEnvelope_Payload() {}
+func (*GatewayEnvelope_TunnelControl) isGatewayEnvelope_Payload() {}
+
+func (*GatewayEnvelope_TunnelControlResp) isGatewayEnvelope_Payload() {}
+
+func (*GatewayEnvelope_TunnelFrame) isGatewayEnvelope_Payload() {}
+
type AgentEnvelope struct {
state protoimpl.MessageState `protogen:"open.v1"`
RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
@@ -908,6 +1032,11 @@ type AgentEnvelope struct {
// *AgentEnvelope_GitResponse
// *AgentEnvelope_FsReadEditableTextResp
// *AgentEnvelope_FsReadWorkspaceImageResp
+ // *AgentEnvelope_TunnelControl
+ // *AgentEnvelope_TunnelControlResp
+ // *AgentEnvelope_TunnelFrame
+ // *AgentEnvelope_ChatControl
+ // *AgentEnvelope_RuntimeStatus
// *AgentEnvelope_Error
Payload isAgentEnvelope_Payload `protobuf_oneof:"payload"`
unknownFields protoimpl.UnknownFields
@@ -1316,6 +1445,51 @@ func (x *AgentEnvelope) GetFsReadWorkspaceImageResp() *FsReadWorkspaceImageRespo
return nil
}
+func (x *AgentEnvelope) GetTunnelControl() *TunnelControlRequest {
+ if x != nil {
+ if x, ok := x.Payload.(*AgentEnvelope_TunnelControl); ok {
+ return x.TunnelControl
+ }
+ }
+ return nil
+}
+
+func (x *AgentEnvelope) GetTunnelControlResp() *TunnelControlResponse {
+ if x != nil {
+ if x, ok := x.Payload.(*AgentEnvelope_TunnelControlResp); ok {
+ return x.TunnelControlResp
+ }
+ }
+ return nil
+}
+
+func (x *AgentEnvelope) GetTunnelFrame() *TunnelFrame {
+ if x != nil {
+ if x, ok := x.Payload.(*AgentEnvelope_TunnelFrame); ok {
+ return x.TunnelFrame
+ }
+ }
+ return nil
+}
+
+func (x *AgentEnvelope) GetChatControl() *ChatControlEvent {
+ if x != nil {
+ if x, ok := x.Payload.(*AgentEnvelope_ChatControl); ok {
+ return x.ChatControl
+ }
+ }
+ return nil
+}
+
+func (x *AgentEnvelope) GetRuntimeStatus() *RuntimeStatusEvent {
+ if x != nil {
+ if x, ok := x.Payload.(*AgentEnvelope_RuntimeStatus); ok {
+ return x.RuntimeStatus
+ }
+ }
+ return nil
+}
+
func (x *AgentEnvelope) GetError() *ErrorResponse {
if x != nil {
if x, ok := x.Payload.(*AgentEnvelope_Error); ok {
@@ -1485,6 +1659,26 @@ type AgentEnvelope_FsReadWorkspaceImageResp struct {
FsReadWorkspaceImageResp *FsReadWorkspaceImageResponse `protobuf:"bytes,66,opt,name=fs_read_workspace_image_resp,json=fsReadWorkspaceImageResp,proto3,oneof"`
}
+type AgentEnvelope_TunnelControl struct {
+ TunnelControl *TunnelControlRequest `protobuf:"bytes,67,opt,name=tunnel_control,json=tunnelControl,proto3,oneof"`
+}
+
+type AgentEnvelope_TunnelControlResp struct {
+ TunnelControlResp *TunnelControlResponse `protobuf:"bytes,68,opt,name=tunnel_control_resp,json=tunnelControlResp,proto3,oneof"`
+}
+
+type AgentEnvelope_TunnelFrame struct {
+ TunnelFrame *TunnelFrame `protobuf:"bytes,69,opt,name=tunnel_frame,json=tunnelFrame,proto3,oneof"`
+}
+
+type AgentEnvelope_ChatControl struct {
+ ChatControl *ChatControlEvent `protobuf:"bytes,70,opt,name=chat_control,json=chatControl,proto3,oneof"`
+}
+
+type AgentEnvelope_RuntimeStatus struct {
+ RuntimeStatus *RuntimeStatusEvent `protobuf:"bytes,71,opt,name=runtime_status,json=runtimeStatus,proto3,oneof"`
+}
+
type AgentEnvelope_Error struct {
Error *ErrorResponse `protobuf:"bytes,99,opt,name=error,proto3,oneof"`
}
@@ -1567,6 +1761,16 @@ func (*AgentEnvelope_FsReadEditableTextResp) isAgentEnvelope_Payload() {}
func (*AgentEnvelope_FsReadWorkspaceImageResp) isAgentEnvelope_Payload() {}
+func (*AgentEnvelope_TunnelControl) isAgentEnvelope_Payload() {}
+
+func (*AgentEnvelope_TunnelControlResp) isAgentEnvelope_Payload() {}
+
+func (*AgentEnvelope_TunnelFrame) isAgentEnvelope_Payload() {}
+
+func (*AgentEnvelope_ChatControl) isAgentEnvelope_Payload() {}
+
+func (*AgentEnvelope_RuntimeStatus) isAgentEnvelope_Payload() {}
+
func (*AgentEnvelope_Error) isAgentEnvelope_Payload() {}
type ChatSelectedModel struct {
@@ -2033,28 +2237,36 @@ func (x *UploadedImagePreviewResponse) GetData() string {
return ""
}
-type MemoryManageRequest struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"`
- ArgsJson string `protobuf:"bytes,2,opt,name=args_json,json=argsJson,proto3" json:"args_json,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
+type TunnelControlRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"`
+ TunnelId string `protobuf:"bytes,2,opt,name=tunnel_id,json=tunnelId,proto3" json:"tunnel_id,omitempty"`
+ Slug string `protobuf:"bytes,3,opt,name=slug,proto3" json:"slug,omitempty"`
+ TargetUrl string `protobuf:"bytes,4,opt,name=target_url,json=targetUrl,proto3" json:"target_url,omitempty"`
+ Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty"`
+ TtlSeconds uint32 `protobuf:"varint,6,opt,name=ttl_seconds,json=ttlSeconds,proto3" json:"ttl_seconds,omitempty"`
+ ExpiresAt int64 `protobuf:"varint,7,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"`
+ PublicUrl string `protobuf:"bytes,8,opt,name=public_url,json=publicUrl,proto3" json:"public_url,omitempty"`
+ PublicBaseUrl string `protobuf:"bytes,9,opt,name=public_base_url,json=publicBaseUrl,proto3" json:"public_base_url,omitempty"`
+ ProjectPathKey string `protobuf:"bytes,10,opt,name=project_path_key,json=projectPathKey,proto3" json:"project_path_key,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
}
-func (x *MemoryManageRequest) Reset() {
- *x = MemoryManageRequest{}
+func (x *TunnelControlRequest) Reset() {
+ *x = TunnelControlRequest{}
mi := &file_proto_v1_gateway_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
-func (x *MemoryManageRequest) String() string {
+func (x *TunnelControlRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*MemoryManageRequest) ProtoMessage() {}
+func (*TunnelControlRequest) ProtoMessage() {}
-func (x *MemoryManageRequest) ProtoReflect() protoreflect.Message {
+func (x *TunnelControlRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_v1_gateway_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -2066,46 +2278,106 @@ func (x *MemoryManageRequest) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use MemoryManageRequest.ProtoReflect.Descriptor instead.
-func (*MemoryManageRequest) Descriptor() ([]byte, []int) {
+// Deprecated: Use TunnelControlRequest.ProtoReflect.Descriptor instead.
+func (*TunnelControlRequest) Descriptor() ([]byte, []int) {
return file_proto_v1_gateway_proto_rawDescGZIP(), []int{12}
}
-func (x *MemoryManageRequest) GetCommand() string {
+func (x *TunnelControlRequest) GetAction() string {
if x != nil {
- return x.Command
+ return x.Action
}
return ""
}
-func (x *MemoryManageRequest) GetArgsJson() string {
+func (x *TunnelControlRequest) GetTunnelId() string {
if x != nil {
- return x.ArgsJson
+ return x.TunnelId
}
return ""
}
-type MemoryManageResponse struct {
+func (x *TunnelControlRequest) GetSlug() string {
+ if x != nil {
+ return x.Slug
+ }
+ return ""
+}
+
+func (x *TunnelControlRequest) GetTargetUrl() string {
+ if x != nil {
+ return x.TargetUrl
+ }
+ return ""
+}
+
+func (x *TunnelControlRequest) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *TunnelControlRequest) GetTtlSeconds() uint32 {
+ if x != nil {
+ return x.TtlSeconds
+ }
+ return 0
+}
+
+func (x *TunnelControlRequest) GetExpiresAt() int64 {
+ if x != nil {
+ return x.ExpiresAt
+ }
+ return 0
+}
+
+func (x *TunnelControlRequest) GetPublicUrl() string {
+ if x != nil {
+ return x.PublicUrl
+ }
+ return ""
+}
+
+func (x *TunnelControlRequest) GetPublicBaseUrl() string {
+ if x != nil {
+ return x.PublicBaseUrl
+ }
+ return ""
+}
+
+func (x *TunnelControlRequest) GetProjectPathKey() string {
+ if x != nil {
+ return x.ProjectPathKey
+ }
+ return ""
+}
+
+type TunnelControlResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
- ResultJson string `protobuf:"bytes,1,opt,name=result_json,json=resultJson,proto3" json:"result_json,omitempty"`
+ Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"`
+ Tunnels []*TunnelSummary `protobuf:"bytes,2,rep,name=tunnels,proto3" json:"tunnels,omitempty"`
+ Tunnel *TunnelSummary `protobuf:"bytes,3,opt,name=tunnel,proto3" json:"tunnel,omitempty"`
+ ErrorCode string `protobuf:"bytes,4,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"`
+ ErrorMessage string `protobuf:"bytes,5,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
-func (x *MemoryManageResponse) Reset() {
- *x = MemoryManageResponse{}
+func (x *TunnelControlResponse) Reset() {
+ *x = TunnelControlResponse{}
mi := &file_proto_v1_gateway_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
-func (x *MemoryManageResponse) String() string {
+func (x *TunnelControlResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*MemoryManageResponse) ProtoMessage() {}
+func (*TunnelControlResponse) ProtoMessage() {}
-func (x *MemoryManageResponse) ProtoReflect() protoreflect.Message {
+func (x *TunnelControlResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_v1_gateway_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -2117,48 +2389,76 @@ func (x *MemoryManageResponse) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use MemoryManageResponse.ProtoReflect.Descriptor instead.
-func (*MemoryManageResponse) Descriptor() ([]byte, []int) {
+// Deprecated: Use TunnelControlResponse.ProtoReflect.Descriptor instead.
+func (*TunnelControlResponse) Descriptor() ([]byte, []int) {
return file_proto_v1_gateway_proto_rawDescGZIP(), []int{13}
}
-func (x *MemoryManageResponse) GetResultJson() string {
+func (x *TunnelControlResponse) GetAction() string {
if x != nil {
- return x.ResultJson
+ return x.Action
}
return ""
}
-type TerminalRequest struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"`
- SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"`
- ProjectPathKey string `protobuf:"bytes,3,opt,name=project_path_key,json=projectPathKey,proto3" json:"project_path_key,omitempty"`
- Cwd string `protobuf:"bytes,4,opt,name=cwd,proto3" json:"cwd,omitempty"`
- Shell string `protobuf:"bytes,5,opt,name=shell,proto3" json:"shell,omitempty"`
- Title string `protobuf:"bytes,6,opt,name=title,proto3" json:"title,omitempty"`
- Data string `protobuf:"bytes,7,opt,name=data,proto3" json:"data,omitempty"`
- Cols uint32 `protobuf:"varint,8,opt,name=cols,proto3" json:"cols,omitempty"`
- Rows uint32 `protobuf:"varint,9,opt,name=rows,proto3" json:"rows,omitempty"`
- MaxBytes uint32 `protobuf:"varint,10,opt,name=max_bytes,json=maxBytes,proto3" json:"max_bytes,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
+func (x *TunnelControlResponse) GetTunnels() []*TunnelSummary {
+ if x != nil {
+ return x.Tunnels
+ }
+ return nil
}
-func (x *TerminalRequest) Reset() {
- *x = TerminalRequest{}
+func (x *TunnelControlResponse) GetTunnel() *TunnelSummary {
+ if x != nil {
+ return x.Tunnel
+ }
+ return nil
+}
+
+func (x *TunnelControlResponse) GetErrorCode() string {
+ if x != nil {
+ return x.ErrorCode
+ }
+ return ""
+}
+
+func (x *TunnelControlResponse) GetErrorMessage() string {
+ if x != nil {
+ return x.ErrorMessage
+ }
+ return ""
+}
+
+type TunnelSummary struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+ Slug string `protobuf:"bytes,2,opt,name=slug,proto3" json:"slug,omitempty"`
+ Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
+ TargetUrl string `protobuf:"bytes,4,opt,name=target_url,json=targetUrl,proto3" json:"target_url,omitempty"`
+ PublicUrl string `protobuf:"bytes,5,opt,name=public_url,json=publicUrl,proto3" json:"public_url,omitempty"`
+ CreatedAt int64 `protobuf:"varint,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
+ ExpiresAt int64 `protobuf:"varint,7,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"`
+ ActiveConnections uint32 `protobuf:"varint,8,opt,name=active_connections,json=activeConnections,proto3" json:"active_connections,omitempty"`
+ Status string `protobuf:"bytes,9,opt,name=status,proto3" json:"status,omitempty"`
+ ProjectPathKey string `protobuf:"bytes,10,opt,name=project_path_key,json=projectPathKey,proto3" json:"project_path_key,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *TunnelSummary) Reset() {
+ *x = TunnelSummary{}
mi := &file_proto_v1_gateway_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
-func (x *TerminalRequest) String() string {
+func (x *TunnelSummary) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*TerminalRequest) ProtoMessage() {}
+func (*TunnelSummary) ProtoMessage() {}
-func (x *TerminalRequest) ProtoReflect() protoreflect.Message {
+func (x *TunnelSummary) ProtoReflect() protoreflect.Message {
mi := &file_proto_v1_gateway_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -2170,83 +2470,479 @@ func (x *TerminalRequest) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use TerminalRequest.ProtoReflect.Descriptor instead.
-func (*TerminalRequest) Descriptor() ([]byte, []int) {
+// Deprecated: Use TunnelSummary.ProtoReflect.Descriptor instead.
+func (*TunnelSummary) Descriptor() ([]byte, []int) {
return file_proto_v1_gateway_proto_rawDescGZIP(), []int{14}
}
-func (x *TerminalRequest) GetAction() string {
+func (x *TunnelSummary) GetId() string {
if x != nil {
- return x.Action
+ return x.Id
}
return ""
}
-func (x *TerminalRequest) GetSessionId() string {
+func (x *TunnelSummary) GetSlug() string {
if x != nil {
- return x.SessionId
+ return x.Slug
}
return ""
}
-func (x *TerminalRequest) GetProjectPathKey() string {
+func (x *TunnelSummary) GetName() string {
if x != nil {
- return x.ProjectPathKey
+ return x.Name
}
return ""
}
-func (x *TerminalRequest) GetCwd() string {
+func (x *TunnelSummary) GetTargetUrl() string {
if x != nil {
- return x.Cwd
+ return x.TargetUrl
}
return ""
}
-func (x *TerminalRequest) GetShell() string {
+func (x *TunnelSummary) GetPublicUrl() string {
if x != nil {
- return x.Shell
+ return x.PublicUrl
}
return ""
}
-func (x *TerminalRequest) GetTitle() string {
+func (x *TunnelSummary) GetCreatedAt() int64 {
if x != nil {
- return x.Title
+ return x.CreatedAt
}
- return ""
+ return 0
}
-func (x *TerminalRequest) GetData() string {
+func (x *TunnelSummary) GetExpiresAt() int64 {
if x != nil {
- return x.Data
+ return x.ExpiresAt
}
- return ""
+ return 0
}
-func (x *TerminalRequest) GetCols() uint32 {
+func (x *TunnelSummary) GetActiveConnections() uint32 {
if x != nil {
- return x.Cols
+ return x.ActiveConnections
}
return 0
}
-func (x *TerminalRequest) GetRows() uint32 {
+func (x *TunnelSummary) GetStatus() string {
if x != nil {
- return x.Rows
+ return x.Status
}
- return 0
+ return ""
}
-func (x *TerminalRequest) GetMaxBytes() uint32 {
+func (x *TunnelSummary) GetProjectPathKey() string {
if x != nil {
- return x.MaxBytes
+ return x.ProjectPathKey
}
- return 0
+ return ""
}
-type TerminalSession struct {
- state protoimpl.MessageState `protogen:"open.v1"`
+type TunnelHeader struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+ Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *TunnelHeader) Reset() {
+ *x = TunnelHeader{}
+ mi := &file_proto_v1_gateway_proto_msgTypes[15]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *TunnelHeader) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*TunnelHeader) ProtoMessage() {}
+
+func (x *TunnelHeader) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_v1_gateway_proto_msgTypes[15]
+ 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 TunnelHeader.ProtoReflect.Descriptor instead.
+func (*TunnelHeader) Descriptor() ([]byte, []int) {
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{15}
+}
+
+func (x *TunnelHeader) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *TunnelHeader) GetValue() string {
+ if x != nil {
+ return x.Value
+ }
+ return ""
+}
+
+type TunnelFrame struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ StreamId string `protobuf:"bytes,1,opt,name=stream_id,json=streamId,proto3" json:"stream_id,omitempty"`
+ TunnelId string `protobuf:"bytes,2,opt,name=tunnel_id,json=tunnelId,proto3" json:"tunnel_id,omitempty"`
+ Slug string `protobuf:"bytes,3,opt,name=slug,proto3" json:"slug,omitempty"`
+ Kind TunnelFrameKind `protobuf:"varint,4,opt,name=kind,proto3,enum=liveagent.gateway.v1.TunnelFrameKind" json:"kind,omitempty"`
+ Method string `protobuf:"bytes,5,opt,name=method,proto3" json:"method,omitempty"`
+ Path string `protobuf:"bytes,6,opt,name=path,proto3" json:"path,omitempty"`
+ Headers []*TunnelHeader `protobuf:"bytes,7,rep,name=headers,proto3" json:"headers,omitempty"`
+ StatusCode uint32 `protobuf:"varint,8,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"`
+ Body []byte `protobuf:"bytes,9,opt,name=body,proto3" json:"body,omitempty"`
+ EndStream bool `protobuf:"varint,10,opt,name=end_stream,json=endStream,proto3" json:"end_stream,omitempty"`
+ Error string `protobuf:"bytes,11,opt,name=error,proto3" json:"error,omitempty"`
+ WsMessageType string `protobuf:"bytes,12,opt,name=ws_message_type,json=wsMessageType,proto3" json:"ws_message_type,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *TunnelFrame) Reset() {
+ *x = TunnelFrame{}
+ mi := &file_proto_v1_gateway_proto_msgTypes[16]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *TunnelFrame) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*TunnelFrame) ProtoMessage() {}
+
+func (x *TunnelFrame) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_v1_gateway_proto_msgTypes[16]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use TunnelFrame.ProtoReflect.Descriptor instead.
+func (*TunnelFrame) Descriptor() ([]byte, []int) {
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{16}
+}
+
+func (x *TunnelFrame) GetStreamId() string {
+ if x != nil {
+ return x.StreamId
+ }
+ return ""
+}
+
+func (x *TunnelFrame) GetTunnelId() string {
+ if x != nil {
+ return x.TunnelId
+ }
+ return ""
+}
+
+func (x *TunnelFrame) GetSlug() string {
+ if x != nil {
+ return x.Slug
+ }
+ return ""
+}
+
+func (x *TunnelFrame) GetKind() TunnelFrameKind {
+ if x != nil {
+ return x.Kind
+ }
+ return TunnelFrameKind_TUNNEL_FRAME_KIND_UNSPECIFIED
+}
+
+func (x *TunnelFrame) GetMethod() string {
+ if x != nil {
+ return x.Method
+ }
+ return ""
+}
+
+func (x *TunnelFrame) GetPath() string {
+ if x != nil {
+ return x.Path
+ }
+ return ""
+}
+
+func (x *TunnelFrame) GetHeaders() []*TunnelHeader {
+ if x != nil {
+ return x.Headers
+ }
+ return nil
+}
+
+func (x *TunnelFrame) GetStatusCode() uint32 {
+ if x != nil {
+ return x.StatusCode
+ }
+ return 0
+}
+
+func (x *TunnelFrame) GetBody() []byte {
+ if x != nil {
+ return x.Body
+ }
+ return nil
+}
+
+func (x *TunnelFrame) GetEndStream() bool {
+ if x != nil {
+ return x.EndStream
+ }
+ return false
+}
+
+func (x *TunnelFrame) GetError() string {
+ if x != nil {
+ return x.Error
+ }
+ return ""
+}
+
+func (x *TunnelFrame) GetWsMessageType() string {
+ if x != nil {
+ return x.WsMessageType
+ }
+ return ""
+}
+
+type MemoryManageRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"`
+ ArgsJson string `protobuf:"bytes,2,opt,name=args_json,json=argsJson,proto3" json:"args_json,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *MemoryManageRequest) Reset() {
+ *x = MemoryManageRequest{}
+ mi := &file_proto_v1_gateway_proto_msgTypes[17]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *MemoryManageRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*MemoryManageRequest) ProtoMessage() {}
+
+func (x *MemoryManageRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_v1_gateway_proto_msgTypes[17]
+ 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 MemoryManageRequest.ProtoReflect.Descriptor instead.
+func (*MemoryManageRequest) Descriptor() ([]byte, []int) {
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{17}
+}
+
+func (x *MemoryManageRequest) GetCommand() string {
+ if x != nil {
+ return x.Command
+ }
+ return ""
+}
+
+func (x *MemoryManageRequest) GetArgsJson() string {
+ if x != nil {
+ return x.ArgsJson
+ }
+ return ""
+}
+
+type MemoryManageResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ ResultJson string `protobuf:"bytes,1,opt,name=result_json,json=resultJson,proto3" json:"result_json,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *MemoryManageResponse) Reset() {
+ *x = MemoryManageResponse{}
+ mi := &file_proto_v1_gateway_proto_msgTypes[18]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *MemoryManageResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*MemoryManageResponse) ProtoMessage() {}
+
+func (x *MemoryManageResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_v1_gateway_proto_msgTypes[18]
+ 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 MemoryManageResponse.ProtoReflect.Descriptor instead.
+func (*MemoryManageResponse) Descriptor() ([]byte, []int) {
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{18}
+}
+
+func (x *MemoryManageResponse) GetResultJson() string {
+ if x != nil {
+ return x.ResultJson
+ }
+ return ""
+}
+
+type TerminalRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"`
+ SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"`
+ ProjectPathKey string `protobuf:"bytes,3,opt,name=project_path_key,json=projectPathKey,proto3" json:"project_path_key,omitempty"`
+ Cwd string `protobuf:"bytes,4,opt,name=cwd,proto3" json:"cwd,omitempty"`
+ Shell string `protobuf:"bytes,5,opt,name=shell,proto3" json:"shell,omitempty"`
+ Title string `protobuf:"bytes,6,opt,name=title,proto3" json:"title,omitempty"`
+ Data string `protobuf:"bytes,7,opt,name=data,proto3" json:"data,omitempty"`
+ Cols uint32 `protobuf:"varint,8,opt,name=cols,proto3" json:"cols,omitempty"`
+ Rows uint32 `protobuf:"varint,9,opt,name=rows,proto3" json:"rows,omitempty"`
+ MaxBytes uint32 `protobuf:"varint,10,opt,name=max_bytes,json=maxBytes,proto3" json:"max_bytes,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *TerminalRequest) Reset() {
+ *x = TerminalRequest{}
+ mi := &file_proto_v1_gateway_proto_msgTypes[19]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *TerminalRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*TerminalRequest) ProtoMessage() {}
+
+func (x *TerminalRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_v1_gateway_proto_msgTypes[19]
+ 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 TerminalRequest.ProtoReflect.Descriptor instead.
+func (*TerminalRequest) Descriptor() ([]byte, []int) {
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{19}
+}
+
+func (x *TerminalRequest) GetAction() string {
+ if x != nil {
+ return x.Action
+ }
+ return ""
+}
+
+func (x *TerminalRequest) GetSessionId() string {
+ if x != nil {
+ return x.SessionId
+ }
+ return ""
+}
+
+func (x *TerminalRequest) GetProjectPathKey() string {
+ if x != nil {
+ return x.ProjectPathKey
+ }
+ return ""
+}
+
+func (x *TerminalRequest) GetCwd() string {
+ if x != nil {
+ return x.Cwd
+ }
+ return ""
+}
+
+func (x *TerminalRequest) GetShell() string {
+ if x != nil {
+ return x.Shell
+ }
+ return ""
+}
+
+func (x *TerminalRequest) GetTitle() string {
+ if x != nil {
+ return x.Title
+ }
+ return ""
+}
+
+func (x *TerminalRequest) GetData() string {
+ if x != nil {
+ return x.Data
+ }
+ return ""
+}
+
+func (x *TerminalRequest) GetCols() uint32 {
+ if x != nil {
+ return x.Cols
+ }
+ return 0
+}
+
+func (x *TerminalRequest) GetRows() uint32 {
+ if x != nil {
+ return x.Rows
+ }
+ return 0
+}
+
+func (x *TerminalRequest) GetMaxBytes() uint32 {
+ if x != nil {
+ return x.MaxBytes
+ }
+ return 0
+}
+
+type TerminalSession struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
ProjectPathKey string `protobuf:"bytes,2,opt,name=project_path_key,json=projectPathKey,proto3" json:"project_path_key,omitempty"`
Cwd string `protobuf:"bytes,3,opt,name=cwd,proto3" json:"cwd,omitempty"`
@@ -2266,7 +2962,7 @@ type TerminalSession struct {
func (x *TerminalSession) Reset() {
*x = TerminalSession{}
- mi := &file_proto_v1_gateway_proto_msgTypes[15]
+ mi := &file_proto_v1_gateway_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2278,7 +2974,7 @@ func (x *TerminalSession) String() string {
func (*TerminalSession) ProtoMessage() {}
func (x *TerminalSession) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[15]
+ mi := &file_proto_v1_gateway_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2291,7 +2987,7 @@ func (x *TerminalSession) ProtoReflect() protoreflect.Message {
// Deprecated: Use TerminalSession.ProtoReflect.Descriptor instead.
func (*TerminalSession) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{15}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{20}
}
func (x *TerminalSession) GetId() string {
@@ -2396,7 +3092,7 @@ type TerminalShellOption struct {
func (x *TerminalShellOption) Reset() {
*x = TerminalShellOption{}
- mi := &file_proto_v1_gateway_proto_msgTypes[16]
+ mi := &file_proto_v1_gateway_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2408,7 +3104,7 @@ func (x *TerminalShellOption) String() string {
func (*TerminalShellOption) ProtoMessage() {}
func (x *TerminalShellOption) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[16]
+ mi := &file_proto_v1_gateway_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2421,7 +3117,7 @@ func (x *TerminalShellOption) ProtoReflect() protoreflect.Message {
// Deprecated: Use TerminalShellOption.ProtoReflect.Descriptor instead.
func (*TerminalShellOption) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{16}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{21}
}
func (x *TerminalShellOption) GetId() string {
@@ -2462,7 +3158,7 @@ type TerminalResponse struct {
func (x *TerminalResponse) Reset() {
*x = TerminalResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[17]
+ mi := &file_proto_v1_gateway_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2474,7 +3170,7 @@ func (x *TerminalResponse) String() string {
func (*TerminalResponse) ProtoMessage() {}
func (x *TerminalResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[17]
+ mi := &file_proto_v1_gateway_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2487,7 +3183,7 @@ func (x *TerminalResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use TerminalResponse.ProtoReflect.Descriptor instead.
func (*TerminalResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{17}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{22}
}
func (x *TerminalResponse) GetAction() string {
@@ -2568,7 +3264,7 @@ type TerminalEvent struct {
func (x *TerminalEvent) Reset() {
*x = TerminalEvent{}
- mi := &file_proto_v1_gateway_proto_msgTypes[18]
+ mi := &file_proto_v1_gateway_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2580,7 +3276,7 @@ func (x *TerminalEvent) String() string {
func (*TerminalEvent) ProtoMessage() {}
func (x *TerminalEvent) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[18]
+ mi := &file_proto_v1_gateway_proto_msgTypes[23]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2593,7 +3289,7 @@ func (x *TerminalEvent) ProtoReflect() protoreflect.Message {
// Deprecated: Use TerminalEvent.ProtoReflect.Descriptor instead.
func (*TerminalEvent) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{18}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{23}
}
func (x *TerminalEvent) GetKind() string {
@@ -2656,7 +3352,7 @@ type GitRequest struct {
func (x *GitRequest) Reset() {
*x = GitRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[19]
+ mi := &file_proto_v1_gateway_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2668,7 +3364,7 @@ func (x *GitRequest) String() string {
func (*GitRequest) ProtoMessage() {}
func (x *GitRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[19]
+ mi := &file_proto_v1_gateway_proto_msgTypes[24]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2681,7 +3377,7 @@ func (x *GitRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GitRequest.ProtoReflect.Descriptor instead.
func (*GitRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{19}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{24}
}
func (x *GitRequest) GetAction() string {
@@ -2715,7 +3411,7 @@ type GitResponse struct {
func (x *GitResponse) Reset() {
*x = GitResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[20]
+ mi := &file_proto_v1_gateway_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2727,7 +3423,7 @@ func (x *GitResponse) String() string {
func (*GitResponse) ProtoMessage() {}
func (x *GitResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[20]
+ mi := &file_proto_v1_gateway_proto_msgTypes[25]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2740,7 +3436,7 @@ func (x *GitResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GitResponse.ProtoReflect.Descriptor instead.
func (*GitResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{20}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{25}
}
func (x *GitResponse) GetAction() string {
@@ -2774,7 +3470,7 @@ type ChatRequest struct {
func (x *ChatRequest) Reset() {
*x = ChatRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[21]
+ mi := &file_proto_v1_gateway_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2786,7 +3482,7 @@ func (x *ChatRequest) String() string {
func (*ChatRequest) ProtoMessage() {}
func (x *ChatRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[21]
+ mi := &file_proto_v1_gateway_proto_msgTypes[26]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2799,94 +3495,310 @@ func (x *ChatRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ChatRequest.ProtoReflect.Descriptor instead.
func (*ChatRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{21}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{26}
+}
+
+func (x *ChatRequest) GetConversationId() string {
+ if x != nil {
+ return x.ConversationId
+ }
+ return ""
+}
+
+func (x *ChatRequest) GetMessage() string {
+ if x != nil {
+ return x.Message
+ }
+ return ""
+}
+
+func (x *ChatRequest) GetSelectedModel() *ChatSelectedModel {
+ if x != nil {
+ return x.SelectedModel
+ }
+ return nil
+}
+
+func (x *ChatRequest) GetExecutionMode() string {
+ if x != nil {
+ return x.ExecutionMode
+ }
+ return ""
+}
+
+func (x *ChatRequest) GetWorkdir() string {
+ if x != nil {
+ return x.Workdir
+ }
+ return ""
+}
+
+func (x *ChatRequest) GetSelectedSystemTools() []string {
+ if x != nil {
+ return x.SelectedSystemTools
+ }
+ return nil
+}
+
+func (x *ChatRequest) GetUploadedFiles() []*ChatUploadedFile {
+ if x != nil {
+ return x.UploadedFiles
+ }
+ return nil
+}
+
+func (x *ChatRequest) GetClientRequestId() string {
+ if x != nil {
+ return x.ClientRequestId
+ }
+ return ""
+}
+
+func (x *ChatRequest) GetRuntimeControls() *ChatRuntimeControls {
+ if x != nil {
+ return x.RuntimeControls
+ }
+ return nil
+}
+
+type CancelChatRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *CancelChatRequest) Reset() {
+ *x = CancelChatRequest{}
+ mi := &file_proto_v1_gateway_proto_msgTypes[27]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *CancelChatRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CancelChatRequest) ProtoMessage() {}
+
+func (x *CancelChatRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_v1_gateway_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 CancelChatRequest.ProtoReflect.Descriptor instead.
+func (*CancelChatRequest) Descriptor() ([]byte, []int) {
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{27}
+}
+
+func (x *CancelChatRequest) GetConversationId() string {
+ if x != nil {
+ return x.ConversationId
+ }
+ return ""
+}
+
+type ChatEvent struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Type ChatEvent_ChatEventType `protobuf:"varint,1,opt,name=type,proto3,enum=liveagent.gateway.v1.ChatEvent_ChatEventType" json:"type,omitempty"`
+ ConversationId string `protobuf:"bytes,2,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"`
+ Data string `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ChatEvent) Reset() {
+ *x = ChatEvent{}
+ mi := &file_proto_v1_gateway_proto_msgTypes[28]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ChatEvent) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ChatEvent) ProtoMessage() {}
+
+func (x *ChatEvent) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_v1_gateway_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 ChatEvent.ProtoReflect.Descriptor instead.
+func (*ChatEvent) Descriptor() ([]byte, []int) {
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{28}
+}
+
+func (x *ChatEvent) GetType() ChatEvent_ChatEventType {
+ if x != nil {
+ return x.Type
+ }
+ return ChatEvent_TOKEN
+}
+
+func (x *ChatEvent) GetConversationId() string {
+ if x != nil {
+ return x.ConversationId
+ }
+ return ""
+}
+
+func (x *ChatEvent) GetData() string {
+ if x != nil {
+ return x.Data
+ }
+ return ""
+}
+
+type ChatControlEvent struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
+ ClientRequestId string `protobuf:"bytes,2,opt,name=client_request_id,json=clientRequestId,proto3" json:"client_request_id,omitempty"`
+ ConversationId string `protobuf:"bytes,3,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"`
+ RunEpoch int64 `protobuf:"varint,4,opt,name=run_epoch,json=runEpoch,proto3" json:"run_epoch,omitempty"`
+ Type string `protobuf:"bytes,5,opt,name=type,proto3" json:"type,omitempty"`
+ State string `protobuf:"bytes,6,opt,name=state,proto3" json:"state,omitempty"`
+ ErrorCode string `protobuf:"bytes,7,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"`
+ Message string `protobuf:"bytes,8,opt,name=message,proto3" json:"message,omitempty"`
+ Seq int64 `protobuf:"varint,9,opt,name=seq,proto3" json:"seq,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ChatControlEvent) Reset() {
+ *x = ChatControlEvent{}
+ mi := &file_proto_v1_gateway_proto_msgTypes[29]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ChatControlEvent) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ChatControlEvent) ProtoMessage() {}
+
+func (x *ChatControlEvent) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_v1_gateway_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 ChatControlEvent.ProtoReflect.Descriptor instead.
+func (*ChatControlEvent) Descriptor() ([]byte, []int) {
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{29}
}
-func (x *ChatRequest) GetConversationId() string {
+func (x *ChatControlEvent) GetRequestId() string {
if x != nil {
- return x.ConversationId
+ return x.RequestId
}
return ""
}
-func (x *ChatRequest) GetMessage() string {
+func (x *ChatControlEvent) GetClientRequestId() string {
if x != nil {
- return x.Message
+ return x.ClientRequestId
}
return ""
}
-func (x *ChatRequest) GetSelectedModel() *ChatSelectedModel {
+func (x *ChatControlEvent) GetConversationId() string {
if x != nil {
- return x.SelectedModel
+ return x.ConversationId
}
- return nil
+ return ""
}
-func (x *ChatRequest) GetExecutionMode() string {
+func (x *ChatControlEvent) GetRunEpoch() int64 {
if x != nil {
- return x.ExecutionMode
+ return x.RunEpoch
}
- return ""
+ return 0
}
-func (x *ChatRequest) GetWorkdir() string {
+func (x *ChatControlEvent) GetType() string {
if x != nil {
- return x.Workdir
+ return x.Type
}
return ""
}
-func (x *ChatRequest) GetSelectedSystemTools() []string {
+func (x *ChatControlEvent) GetState() string {
if x != nil {
- return x.SelectedSystemTools
+ return x.State
}
- return nil
+ return ""
}
-func (x *ChatRequest) GetUploadedFiles() []*ChatUploadedFile {
+func (x *ChatControlEvent) GetErrorCode() string {
if x != nil {
- return x.UploadedFiles
+ return x.ErrorCode
}
- return nil
+ return ""
}
-func (x *ChatRequest) GetClientRequestId() string {
+func (x *ChatControlEvent) GetMessage() string {
if x != nil {
- return x.ClientRequestId
+ return x.Message
}
return ""
}
-func (x *ChatRequest) GetRuntimeControls() *ChatRuntimeControls {
+func (x *ChatControlEvent) GetSeq() int64 {
if x != nil {
- return x.RuntimeControls
+ return x.Seq
}
- return nil
+ return 0
}
-type CancelChatRequest struct {
+type RuntimeStatusEvent struct {
state protoimpl.MessageState `protogen:"open.v1"`
- ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"`
+ WorkerId string `protobuf:"bytes,1,opt,name=worker_id,json=workerId,proto3" json:"worker_id,omitempty"`
+ State string `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"`
+ Visible bool `protobuf:"varint,3,opt,name=visible,proto3" json:"visible,omitempty"`
+ ActiveRunCount uint32 `protobuf:"varint,4,opt,name=active_run_count,json=activeRunCount,proto3" json:"active_run_count,omitempty"`
+ Timestamp int64 `protobuf:"varint,5,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
-func (x *CancelChatRequest) Reset() {
- *x = CancelChatRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[22]
+func (x *RuntimeStatusEvent) Reset() {
+ *x = RuntimeStatusEvent{}
+ mi := &file_proto_v1_gateway_proto_msgTypes[30]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
-func (x *CancelChatRequest) String() string {
+func (x *RuntimeStatusEvent) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*CancelChatRequest) ProtoMessage() {}
+func (*RuntimeStatusEvent) ProtoMessage() {}
-func (x *CancelChatRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[22]
+func (x *RuntimeStatusEvent) ProtoReflect() protoreflect.Message {
+ mi := &file_proto_v1_gateway_proto_msgTypes[30]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2897,76 +3809,44 @@ func (x *CancelChatRequest) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use CancelChatRequest.ProtoReflect.Descriptor instead.
-func (*CancelChatRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{22}
+// Deprecated: Use RuntimeStatusEvent.ProtoReflect.Descriptor instead.
+func (*RuntimeStatusEvent) Descriptor() ([]byte, []int) {
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{30}
}
-func (x *CancelChatRequest) GetConversationId() string {
+func (x *RuntimeStatusEvent) GetWorkerId() string {
if x != nil {
- return x.ConversationId
+ return x.WorkerId
}
return ""
}
-type ChatEvent struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- Type ChatEvent_ChatEventType `protobuf:"varint,1,opt,name=type,proto3,enum=liveagent.gateway.v1.ChatEvent_ChatEventType" json:"type,omitempty"`
- ConversationId string `protobuf:"bytes,2,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"`
- Data string `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
-}
-
-func (x *ChatEvent) Reset() {
- *x = ChatEvent{}
- mi := &file_proto_v1_gateway_proto_msgTypes[23]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *ChatEvent) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*ChatEvent) ProtoMessage() {}
-
-func (x *ChatEvent) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[23]
+func (x *RuntimeStatusEvent) GetState() string {
if x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
+ return x.State
}
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use ChatEvent.ProtoReflect.Descriptor instead.
-func (*ChatEvent) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{23}
+ return ""
}
-func (x *ChatEvent) GetType() ChatEvent_ChatEventType {
+func (x *RuntimeStatusEvent) GetVisible() bool {
if x != nil {
- return x.Type
+ return x.Visible
}
- return ChatEvent_TOKEN
+ return false
}
-func (x *ChatEvent) GetConversationId() string {
+func (x *RuntimeStatusEvent) GetActiveRunCount() uint32 {
if x != nil {
- return x.ConversationId
+ return x.ActiveRunCount
}
- return ""
+ return 0
}
-func (x *ChatEvent) GetData() string {
+func (x *RuntimeStatusEvent) GetTimestamp() int64 {
if x != nil {
- return x.Data
+ return x.Timestamp
}
- return ""
+ return 0
}
type CronManageRequest struct {
@@ -2980,7 +3860,7 @@ type CronManageRequest struct {
func (x *CronManageRequest) Reset() {
*x = CronManageRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[24]
+ mi := &file_proto_v1_gateway_proto_msgTypes[31]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2992,7 +3872,7 @@ func (x *CronManageRequest) String() string {
func (*CronManageRequest) ProtoMessage() {}
func (x *CronManageRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[24]
+ mi := &file_proto_v1_gateway_proto_msgTypes[31]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3005,7 +3885,7 @@ func (x *CronManageRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CronManageRequest.ProtoReflect.Descriptor instead.
func (*CronManageRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{24}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{31}
}
func (x *CronManageRequest) GetAction() string {
@@ -3039,7 +3919,7 @@ type CronManageResponse struct {
func (x *CronManageResponse) Reset() {
*x = CronManageResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[25]
+ mi := &file_proto_v1_gateway_proto_msgTypes[32]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3051,7 +3931,7 @@ func (x *CronManageResponse) String() string {
func (*CronManageResponse) ProtoMessage() {}
func (x *CronManageResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[25]
+ mi := &file_proto_v1_gateway_proto_msgTypes[32]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3064,7 +3944,7 @@ func (x *CronManageResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use CronManageResponse.ProtoReflect.Descriptor instead.
func (*CronManageResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{25}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{32}
}
func (x *CronManageResponse) GetAction() string {
@@ -3093,7 +3973,7 @@ type HistoryListRequest struct {
func (x *HistoryListRequest) Reset() {
*x = HistoryListRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[26]
+ mi := &file_proto_v1_gateway_proto_msgTypes[33]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3105,7 +3985,7 @@ func (x *HistoryListRequest) String() string {
func (*HistoryListRequest) ProtoMessage() {}
func (x *HistoryListRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[26]
+ mi := &file_proto_v1_gateway_proto_msgTypes[33]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3118,7 +3998,7 @@ func (x *HistoryListRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryListRequest.ProtoReflect.Descriptor instead.
func (*HistoryListRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{26}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{33}
}
func (x *HistoryListRequest) GetPage() int32 {
@@ -3159,7 +4039,7 @@ type HistoryListResponse struct {
func (x *HistoryListResponse) Reset() {
*x = HistoryListResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[27]
+ mi := &file_proto_v1_gateway_proto_msgTypes[34]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3171,7 +4051,7 @@ func (x *HistoryListResponse) String() string {
func (*HistoryListResponse) ProtoMessage() {}
func (x *HistoryListResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[27]
+ mi := &file_proto_v1_gateway_proto_msgTypes[34]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3184,7 +4064,7 @@ func (x *HistoryListResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryListResponse.ProtoReflect.Descriptor instead.
func (*HistoryListResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{27}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{34}
}
func (x *HistoryListResponse) GetConversations() []*ConversationSummary {
@@ -3221,7 +4101,7 @@ type ConversationSummary struct {
func (x *ConversationSummary) Reset() {
*x = ConversationSummary{}
- mi := &file_proto_v1_gateway_proto_msgTypes[28]
+ mi := &file_proto_v1_gateway_proto_msgTypes[35]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3233,7 +4113,7 @@ func (x *ConversationSummary) String() string {
func (*ConversationSummary) ProtoMessage() {}
func (x *ConversationSummary) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[28]
+ mi := &file_proto_v1_gateway_proto_msgTypes[35]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3246,7 +4126,7 @@ func (x *ConversationSummary) ProtoReflect() protoreflect.Message {
// Deprecated: Use ConversationSummary.ProtoReflect.Descriptor instead.
func (*ConversationSummary) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{28}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{35}
}
func (x *ConversationSummary) GetId() string {
@@ -3343,7 +4223,7 @@ type HistoryGetRequest struct {
func (x *HistoryGetRequest) Reset() {
*x = HistoryGetRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[29]
+ mi := &file_proto_v1_gateway_proto_msgTypes[36]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3355,7 +4235,7 @@ func (x *HistoryGetRequest) String() string {
func (*HistoryGetRequest) ProtoMessage() {}
func (x *HistoryGetRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[29]
+ mi := &file_proto_v1_gateway_proto_msgTypes[36]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3368,7 +4248,7 @@ func (x *HistoryGetRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryGetRequest.ProtoReflect.Descriptor instead.
func (*HistoryGetRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{29}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{36}
}
func (x *HistoryGetRequest) GetConversationId() string {
@@ -3399,7 +4279,7 @@ type HistoryGetResponse struct {
func (x *HistoryGetResponse) Reset() {
*x = HistoryGetResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[30]
+ mi := &file_proto_v1_gateway_proto_msgTypes[37]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3411,7 +4291,7 @@ func (x *HistoryGetResponse) String() string {
func (*HistoryGetResponse) ProtoMessage() {}
func (x *HistoryGetResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[30]
+ mi := &file_proto_v1_gateway_proto_msgTypes[37]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3424,7 +4304,7 @@ func (x *HistoryGetResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryGetResponse.ProtoReflect.Descriptor instead.
func (*HistoryGetResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{30}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{37}
}
func (x *HistoryGetResponse) GetConversationId() string {
@@ -3479,7 +4359,7 @@ type HistoryRenameRequest struct {
func (x *HistoryRenameRequest) Reset() {
*x = HistoryRenameRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[31]
+ mi := &file_proto_v1_gateway_proto_msgTypes[38]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3491,7 +4371,7 @@ func (x *HistoryRenameRequest) String() string {
func (*HistoryRenameRequest) ProtoMessage() {}
func (x *HistoryRenameRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[31]
+ mi := &file_proto_v1_gateway_proto_msgTypes[38]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3504,7 +4384,7 @@ func (x *HistoryRenameRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryRenameRequest.ProtoReflect.Descriptor instead.
func (*HistoryRenameRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{31}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{38}
}
func (x *HistoryRenameRequest) GetConversationId() string {
@@ -3530,7 +4410,7 @@ type HistoryRenameResponse struct {
func (x *HistoryRenameResponse) Reset() {
*x = HistoryRenameResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[32]
+ mi := &file_proto_v1_gateway_proto_msgTypes[39]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3542,7 +4422,7 @@ func (x *HistoryRenameResponse) String() string {
func (*HistoryRenameResponse) ProtoMessage() {}
func (x *HistoryRenameResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[32]
+ mi := &file_proto_v1_gateway_proto_msgTypes[39]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3555,7 +4435,7 @@ func (x *HistoryRenameResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryRenameResponse.ProtoReflect.Descriptor instead.
func (*HistoryRenameResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{32}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{39}
}
func (x *HistoryRenameResponse) GetConversation() *ConversationSummary {
@@ -3575,7 +4455,7 @@ type HistoryPinRequest struct {
func (x *HistoryPinRequest) Reset() {
*x = HistoryPinRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[33]
+ mi := &file_proto_v1_gateway_proto_msgTypes[40]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3587,7 +4467,7 @@ func (x *HistoryPinRequest) String() string {
func (*HistoryPinRequest) ProtoMessage() {}
func (x *HistoryPinRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[33]
+ mi := &file_proto_v1_gateway_proto_msgTypes[40]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3600,7 +4480,7 @@ func (x *HistoryPinRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryPinRequest.ProtoReflect.Descriptor instead.
func (*HistoryPinRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{33}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{40}
}
func (x *HistoryPinRequest) GetConversationId() string {
@@ -3626,7 +4506,7 @@ type HistoryPinResponse struct {
func (x *HistoryPinResponse) Reset() {
*x = HistoryPinResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[34]
+ mi := &file_proto_v1_gateway_proto_msgTypes[41]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3638,7 +4518,7 @@ func (x *HistoryPinResponse) String() string {
func (*HistoryPinResponse) ProtoMessage() {}
func (x *HistoryPinResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[34]
+ mi := &file_proto_v1_gateway_proto_msgTypes[41]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3651,7 +4531,7 @@ func (x *HistoryPinResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryPinResponse.ProtoReflect.Descriptor instead.
func (*HistoryPinResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{34}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{41}
}
func (x *HistoryPinResponse) GetConversation() *ConversationSummary {
@@ -3675,7 +4555,7 @@ type HistoryShareStatus struct {
func (x *HistoryShareStatus) Reset() {
*x = HistoryShareStatus{}
- mi := &file_proto_v1_gateway_proto_msgTypes[35]
+ mi := &file_proto_v1_gateway_proto_msgTypes[42]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3687,7 +4567,7 @@ func (x *HistoryShareStatus) String() string {
func (*HistoryShareStatus) ProtoMessage() {}
func (x *HistoryShareStatus) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[35]
+ mi := &file_proto_v1_gateway_proto_msgTypes[42]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3700,7 +4580,7 @@ func (x *HistoryShareStatus) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryShareStatus.ProtoReflect.Descriptor instead.
func (*HistoryShareStatus) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{35}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{42}
}
func (x *HistoryShareStatus) GetConversationId() string {
@@ -3754,7 +4634,7 @@ type HistoryShareGetRequest struct {
func (x *HistoryShareGetRequest) Reset() {
*x = HistoryShareGetRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[36]
+ mi := &file_proto_v1_gateway_proto_msgTypes[43]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3766,7 +4646,7 @@ func (x *HistoryShareGetRequest) String() string {
func (*HistoryShareGetRequest) ProtoMessage() {}
func (x *HistoryShareGetRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[36]
+ mi := &file_proto_v1_gateway_proto_msgTypes[43]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3779,7 +4659,7 @@ func (x *HistoryShareGetRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryShareGetRequest.ProtoReflect.Descriptor instead.
func (*HistoryShareGetRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{36}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{43}
}
func (x *HistoryShareGetRequest) GetConversationId() string {
@@ -3798,7 +4678,7 @@ type HistoryShareGetResponse struct {
func (x *HistoryShareGetResponse) Reset() {
*x = HistoryShareGetResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[37]
+ mi := &file_proto_v1_gateway_proto_msgTypes[44]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3810,7 +4690,7 @@ func (x *HistoryShareGetResponse) String() string {
func (*HistoryShareGetResponse) ProtoMessage() {}
func (x *HistoryShareGetResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[37]
+ mi := &file_proto_v1_gateway_proto_msgTypes[44]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3823,7 +4703,7 @@ func (x *HistoryShareGetResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryShareGetResponse.ProtoReflect.Descriptor instead.
func (*HistoryShareGetResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{37}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{44}
}
func (x *HistoryShareGetResponse) GetShare() *HistoryShareStatus {
@@ -3844,7 +4724,7 @@ type HistoryShareSetRequest struct {
func (x *HistoryShareSetRequest) Reset() {
*x = HistoryShareSetRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[38]
+ mi := &file_proto_v1_gateway_proto_msgTypes[45]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3856,7 +4736,7 @@ func (x *HistoryShareSetRequest) String() string {
func (*HistoryShareSetRequest) ProtoMessage() {}
func (x *HistoryShareSetRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[38]
+ mi := &file_proto_v1_gateway_proto_msgTypes[45]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3869,7 +4749,7 @@ func (x *HistoryShareSetRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryShareSetRequest.ProtoReflect.Descriptor instead.
func (*HistoryShareSetRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{38}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{45}
}
func (x *HistoryShareSetRequest) GetConversationId() string {
@@ -3902,7 +4782,7 @@ type HistoryShareSetResponse struct {
func (x *HistoryShareSetResponse) Reset() {
*x = HistoryShareSetResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[39]
+ mi := &file_proto_v1_gateway_proto_msgTypes[46]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3914,7 +4794,7 @@ func (x *HistoryShareSetResponse) String() string {
func (*HistoryShareSetResponse) ProtoMessage() {}
func (x *HistoryShareSetResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[39]
+ mi := &file_proto_v1_gateway_proto_msgTypes[46]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3927,7 +4807,7 @@ func (x *HistoryShareSetResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryShareSetResponse.ProtoReflect.Descriptor instead.
func (*HistoryShareSetResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{39}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{46}
}
func (x *HistoryShareSetResponse) GetShare() *HistoryShareStatus {
@@ -3946,7 +4826,7 @@ type HistoryShareResolveRequest struct {
func (x *HistoryShareResolveRequest) Reset() {
*x = HistoryShareResolveRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[40]
+ mi := &file_proto_v1_gateway_proto_msgTypes[47]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3958,7 +4838,7 @@ func (x *HistoryShareResolveRequest) String() string {
func (*HistoryShareResolveRequest) ProtoMessage() {}
func (x *HistoryShareResolveRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[40]
+ mi := &file_proto_v1_gateway_proto_msgTypes[47]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3971,7 +4851,7 @@ func (x *HistoryShareResolveRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryShareResolveRequest.ProtoReflect.Descriptor instead.
func (*HistoryShareResolveRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{40}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{47}
}
func (x *HistoryShareResolveRequest) GetToken() string {
@@ -3994,7 +4874,7 @@ type HistoryShareResolveResponse struct {
func (x *HistoryShareResolveResponse) Reset() {
*x = HistoryShareResolveResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[41]
+ mi := &file_proto_v1_gateway_proto_msgTypes[48]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4006,7 +4886,7 @@ func (x *HistoryShareResolveResponse) String() string {
func (*HistoryShareResolveResponse) ProtoMessage() {}
func (x *HistoryShareResolveResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[41]
+ mi := &file_proto_v1_gateway_proto_msgTypes[48]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4019,7 +4899,7 @@ func (x *HistoryShareResolveResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryShareResolveResponse.ProtoReflect.Descriptor instead.
func (*HistoryShareResolveResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{41}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{48}
}
func (x *HistoryShareResolveResponse) GetConversationId() string {
@@ -4065,7 +4945,7 @@ type HistoryWorkdirsRequest struct {
func (x *HistoryWorkdirsRequest) Reset() {
*x = HistoryWorkdirsRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[42]
+ mi := &file_proto_v1_gateway_proto_msgTypes[49]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4077,7 +4957,7 @@ func (x *HistoryWorkdirsRequest) String() string {
func (*HistoryWorkdirsRequest) ProtoMessage() {}
func (x *HistoryWorkdirsRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[42]
+ mi := &file_proto_v1_gateway_proto_msgTypes[49]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4090,7 +4970,7 @@ func (x *HistoryWorkdirsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryWorkdirsRequest.ProtoReflect.Descriptor instead.
func (*HistoryWorkdirsRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{42}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{49}
}
type HistoryWorkdirSummary struct {
@@ -4104,7 +4984,7 @@ type HistoryWorkdirSummary struct {
func (x *HistoryWorkdirSummary) Reset() {
*x = HistoryWorkdirSummary{}
- mi := &file_proto_v1_gateway_proto_msgTypes[43]
+ mi := &file_proto_v1_gateway_proto_msgTypes[50]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4116,7 +4996,7 @@ func (x *HistoryWorkdirSummary) String() string {
func (*HistoryWorkdirSummary) ProtoMessage() {}
func (x *HistoryWorkdirSummary) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[43]
+ mi := &file_proto_v1_gateway_proto_msgTypes[50]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4129,7 +5009,7 @@ func (x *HistoryWorkdirSummary) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryWorkdirSummary.ProtoReflect.Descriptor instead.
func (*HistoryWorkdirSummary) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{43}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{50}
}
func (x *HistoryWorkdirSummary) GetPath() string {
@@ -4162,7 +5042,7 @@ type HistoryWorkdirsResponse struct {
func (x *HistoryWorkdirsResponse) Reset() {
*x = HistoryWorkdirsResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[44]
+ mi := &file_proto_v1_gateway_proto_msgTypes[51]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4174,7 +5054,7 @@ func (x *HistoryWorkdirsResponse) String() string {
func (*HistoryWorkdirsResponse) ProtoMessage() {}
func (x *HistoryWorkdirsResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[44]
+ mi := &file_proto_v1_gateway_proto_msgTypes[51]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4187,7 +5067,7 @@ func (x *HistoryWorkdirsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryWorkdirsResponse.ProtoReflect.Descriptor instead.
func (*HistoryWorkdirsResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{44}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{51}
}
func (x *HistoryWorkdirsResponse) GetWorkdirs() []*HistoryWorkdirSummary {
@@ -4206,7 +5086,7 @@ type HistoryDeleteRequest struct {
func (x *HistoryDeleteRequest) Reset() {
*x = HistoryDeleteRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[45]
+ mi := &file_proto_v1_gateway_proto_msgTypes[52]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4218,7 +5098,7 @@ func (x *HistoryDeleteRequest) String() string {
func (*HistoryDeleteRequest) ProtoMessage() {}
func (x *HistoryDeleteRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[45]
+ mi := &file_proto_v1_gateway_proto_msgTypes[52]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4231,7 +5111,7 @@ func (x *HistoryDeleteRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryDeleteRequest.ProtoReflect.Descriptor instead.
func (*HistoryDeleteRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{45}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{52}
}
func (x *HistoryDeleteRequest) GetConversationId() string {
@@ -4249,7 +5129,7 @@ type HistoryDeleteResponse struct {
func (x *HistoryDeleteResponse) Reset() {
*x = HistoryDeleteResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[46]
+ mi := &file_proto_v1_gateway_proto_msgTypes[53]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4261,7 +5141,7 @@ func (x *HistoryDeleteResponse) String() string {
func (*HistoryDeleteResponse) ProtoMessage() {}
func (x *HistoryDeleteResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[46]
+ mi := &file_proto_v1_gateway_proto_msgTypes[53]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4274,7 +5154,7 @@ func (x *HistoryDeleteResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryDeleteResponse.ProtoReflect.Descriptor instead.
func (*HistoryDeleteResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{46}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{53}
}
type HistoryTruncateRequest struct {
@@ -4289,7 +5169,7 @@ type HistoryTruncateRequest struct {
func (x *HistoryTruncateRequest) Reset() {
*x = HistoryTruncateRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[47]
+ mi := &file_proto_v1_gateway_proto_msgTypes[54]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4301,7 +5181,7 @@ func (x *HistoryTruncateRequest) String() string {
func (*HistoryTruncateRequest) ProtoMessage() {}
func (x *HistoryTruncateRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[47]
+ mi := &file_proto_v1_gateway_proto_msgTypes[54]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4314,7 +5194,7 @@ func (x *HistoryTruncateRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryTruncateRequest.ProtoReflect.Descriptor instead.
func (*HistoryTruncateRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{47}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{54}
}
func (x *HistoryTruncateRequest) GetConversationId() string {
@@ -4356,7 +5236,7 @@ type HistoryTruncateResponse struct {
func (x *HistoryTruncateResponse) Reset() {
*x = HistoryTruncateResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[48]
+ mi := &file_proto_v1_gateway_proto_msgTypes[55]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4368,7 +5248,7 @@ func (x *HistoryTruncateResponse) String() string {
func (*HistoryTruncateResponse) ProtoMessage() {}
func (x *HistoryTruncateResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[48]
+ mi := &file_proto_v1_gateway_proto_msgTypes[55]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4381,7 +5261,7 @@ func (x *HistoryTruncateResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistoryTruncateResponse.ProtoReflect.Descriptor instead.
func (*HistoryTruncateResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{48}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{55}
}
func (x *HistoryTruncateResponse) GetConversationId() string {
@@ -4416,7 +5296,7 @@ type HistorySyncEvent struct {
func (x *HistorySyncEvent) Reset() {
*x = HistorySyncEvent{}
- mi := &file_proto_v1_gateway_proto_msgTypes[49]
+ mi := &file_proto_v1_gateway_proto_msgTypes[56]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4428,7 +5308,7 @@ func (x *HistorySyncEvent) String() string {
func (*HistorySyncEvent) ProtoMessage() {}
func (x *HistorySyncEvent) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[49]
+ mi := &file_proto_v1_gateway_proto_msgTypes[56]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4441,7 +5321,7 @@ func (x *HistorySyncEvent) ProtoReflect() protoreflect.Message {
// Deprecated: Use HistorySyncEvent.ProtoReflect.Descriptor instead.
func (*HistorySyncEvent) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{49}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{56}
}
func (x *HistorySyncEvent) GetKind() string {
@@ -4473,7 +5353,7 @@ type ProviderListRequest struct {
func (x *ProviderListRequest) Reset() {
*x = ProviderListRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[50]
+ mi := &file_proto_v1_gateway_proto_msgTypes[57]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4485,7 +5365,7 @@ func (x *ProviderListRequest) String() string {
func (*ProviderListRequest) ProtoMessage() {}
func (x *ProviderListRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[50]
+ mi := &file_proto_v1_gateway_proto_msgTypes[57]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4498,7 +5378,7 @@ func (x *ProviderListRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ProviderListRequest.ProtoReflect.Descriptor instead.
func (*ProviderListRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{50}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{57}
}
type ProviderListResponse struct {
@@ -4510,7 +5390,7 @@ type ProviderListResponse struct {
func (x *ProviderListResponse) Reset() {
*x = ProviderListResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[51]
+ mi := &file_proto_v1_gateway_proto_msgTypes[58]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4522,7 +5402,7 @@ func (x *ProviderListResponse) String() string {
func (*ProviderListResponse) ProtoMessage() {}
func (x *ProviderListResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[51]
+ mi := &file_proto_v1_gateway_proto_msgTypes[58]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4535,7 +5415,7 @@ func (x *ProviderListResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ProviderListResponse.ProtoReflect.Descriptor instead.
func (*ProviderListResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{51}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{58}
}
func (x *ProviderListResponse) GetProvidersJson() string {
@@ -4553,7 +5433,7 @@ type SettingsGetRequest struct {
func (x *SettingsGetRequest) Reset() {
*x = SettingsGetRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[52]
+ mi := &file_proto_v1_gateway_proto_msgTypes[59]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4565,7 +5445,7 @@ func (x *SettingsGetRequest) String() string {
func (*SettingsGetRequest) ProtoMessage() {}
func (x *SettingsGetRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[52]
+ mi := &file_proto_v1_gateway_proto_msgTypes[59]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4578,7 +5458,7 @@ func (x *SettingsGetRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SettingsGetRequest.ProtoReflect.Descriptor instead.
func (*SettingsGetRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{52}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{59}
}
type SettingsGetResponse struct {
@@ -4590,7 +5470,7 @@ type SettingsGetResponse struct {
func (x *SettingsGetResponse) Reset() {
*x = SettingsGetResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[53]
+ mi := &file_proto_v1_gateway_proto_msgTypes[60]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4602,7 +5482,7 @@ func (x *SettingsGetResponse) String() string {
func (*SettingsGetResponse) ProtoMessage() {}
func (x *SettingsGetResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[53]
+ mi := &file_proto_v1_gateway_proto_msgTypes[60]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4615,7 +5495,7 @@ func (x *SettingsGetResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SettingsGetResponse.ProtoReflect.Descriptor instead.
func (*SettingsGetResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{53}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{60}
}
func (x *SettingsGetResponse) GetSettingsJson() string {
@@ -4634,7 +5514,7 @@ type SettingsUpdateRequest struct {
func (x *SettingsUpdateRequest) Reset() {
*x = SettingsUpdateRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[54]
+ mi := &file_proto_v1_gateway_proto_msgTypes[61]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4646,7 +5526,7 @@ func (x *SettingsUpdateRequest) String() string {
func (*SettingsUpdateRequest) ProtoMessage() {}
func (x *SettingsUpdateRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[54]
+ mi := &file_proto_v1_gateway_proto_msgTypes[61]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4659,7 +5539,7 @@ func (x *SettingsUpdateRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SettingsUpdateRequest.ProtoReflect.Descriptor instead.
func (*SettingsUpdateRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{54}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{61}
}
func (x *SettingsUpdateRequest) GetSettingsJson() string {
@@ -4679,7 +5559,7 @@ type SettingsUpdateResponse struct {
func (x *SettingsUpdateResponse) Reset() {
*x = SettingsUpdateResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[55]
+ mi := &file_proto_v1_gateway_proto_msgTypes[62]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4691,7 +5571,7 @@ func (x *SettingsUpdateResponse) String() string {
func (*SettingsUpdateResponse) ProtoMessage() {}
func (x *SettingsUpdateResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[55]
+ mi := &file_proto_v1_gateway_proto_msgTypes[62]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4704,7 +5584,7 @@ func (x *SettingsUpdateResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SettingsUpdateResponse.ProtoReflect.Descriptor instead.
func (*SettingsUpdateResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{55}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{62}
}
func (x *SettingsUpdateResponse) GetAccepted() bool {
@@ -4730,7 +5610,7 @@ type SettingsSyncEvent struct {
func (x *SettingsSyncEvent) Reset() {
*x = SettingsSyncEvent{}
- mi := &file_proto_v1_gateway_proto_msgTypes[56]
+ mi := &file_proto_v1_gateway_proto_msgTypes[63]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4742,7 +5622,7 @@ func (x *SettingsSyncEvent) String() string {
func (*SettingsSyncEvent) ProtoMessage() {}
func (x *SettingsSyncEvent) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[56]
+ mi := &file_proto_v1_gateway_proto_msgTypes[63]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4755,7 +5635,7 @@ func (x *SettingsSyncEvent) ProtoReflect() protoreflect.Message {
// Deprecated: Use SettingsSyncEvent.ProtoReflect.Descriptor instead.
func (*SettingsSyncEvent) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{56}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{63}
}
func (x *SettingsSyncEvent) GetSettingsJson() string {
@@ -4773,7 +5653,7 @@ type SkillFilesListRequest struct {
func (x *SkillFilesListRequest) Reset() {
*x = SkillFilesListRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[57]
+ mi := &file_proto_v1_gateway_proto_msgTypes[64]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4785,7 +5665,7 @@ func (x *SkillFilesListRequest) String() string {
func (*SkillFilesListRequest) ProtoMessage() {}
func (x *SkillFilesListRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[57]
+ mi := &file_proto_v1_gateway_proto_msgTypes[64]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4798,7 +5678,7 @@ func (x *SkillFilesListRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillFilesListRequest.ProtoReflect.Descriptor instead.
func (*SkillFilesListRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{57}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{64}
}
type SkillFilesListResponse struct {
@@ -4812,7 +5692,7 @@ type SkillFilesListResponse struct {
func (x *SkillFilesListResponse) Reset() {
*x = SkillFilesListResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[58]
+ mi := &file_proto_v1_gateway_proto_msgTypes[65]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4824,7 +5704,7 @@ func (x *SkillFilesListResponse) String() string {
func (*SkillFilesListResponse) ProtoMessage() {}
func (x *SkillFilesListResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[58]
+ mi := &file_proto_v1_gateway_proto_msgTypes[65]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4837,7 +5717,7 @@ func (x *SkillFilesListResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillFilesListResponse.ProtoReflect.Descriptor instead.
func (*SkillFilesListResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{58}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{65}
}
func (x *SkillFilesListResponse) GetRootDir() string {
@@ -4870,7 +5750,7 @@ type SkillMetadataReadRequest struct {
func (x *SkillMetadataReadRequest) Reset() {
*x = SkillMetadataReadRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[59]
+ mi := &file_proto_v1_gateway_proto_msgTypes[66]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4882,7 +5762,7 @@ func (x *SkillMetadataReadRequest) String() string {
func (*SkillMetadataReadRequest) ProtoMessage() {}
func (x *SkillMetadataReadRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[59]
+ mi := &file_proto_v1_gateway_proto_msgTypes[66]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4895,7 +5775,7 @@ func (x *SkillMetadataReadRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillMetadataReadRequest.ProtoReflect.Descriptor instead.
func (*SkillMetadataReadRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{59}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{66}
}
func (x *SkillMetadataReadRequest) GetPath() string {
@@ -4915,7 +5795,7 @@ type SkillMetadataReadResponse struct {
func (x *SkillMetadataReadResponse) Reset() {
*x = SkillMetadataReadResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[60]
+ mi := &file_proto_v1_gateway_proto_msgTypes[67]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4927,7 +5807,7 @@ func (x *SkillMetadataReadResponse) String() string {
func (*SkillMetadataReadResponse) ProtoMessage() {}
func (x *SkillMetadataReadResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[60]
+ mi := &file_proto_v1_gateway_proto_msgTypes[67]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4940,7 +5820,7 @@ func (x *SkillMetadataReadResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillMetadataReadResponse.ProtoReflect.Descriptor instead.
func (*SkillMetadataReadResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{60}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{67}
}
func (x *SkillMetadataReadResponse) GetName() string {
@@ -4968,7 +5848,7 @@ type SkillTextReadRequest struct {
func (x *SkillTextReadRequest) Reset() {
*x = SkillTextReadRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[61]
+ mi := &file_proto_v1_gateway_proto_msgTypes[68]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4980,7 +5860,7 @@ func (x *SkillTextReadRequest) String() string {
func (*SkillTextReadRequest) ProtoMessage() {}
func (x *SkillTextReadRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[61]
+ mi := &file_proto_v1_gateway_proto_msgTypes[68]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4993,7 +5873,7 @@ func (x *SkillTextReadRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillTextReadRequest.ProtoReflect.Descriptor instead.
func (*SkillTextReadRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{61}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{68}
}
func (x *SkillTextReadRequest) GetPath() string {
@@ -5027,7 +5907,7 @@ type SkillTextReadResponse struct {
func (x *SkillTextReadResponse) Reset() {
*x = SkillTextReadResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[62]
+ mi := &file_proto_v1_gateway_proto_msgTypes[69]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5039,7 +5919,7 @@ func (x *SkillTextReadResponse) String() string {
func (*SkillTextReadResponse) ProtoMessage() {}
func (x *SkillTextReadResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[62]
+ mi := &file_proto_v1_gateway_proto_msgTypes[69]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5052,7 +5932,7 @@ func (x *SkillTextReadResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillTextReadResponse.ProtoReflect.Descriptor instead.
func (*SkillTextReadResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{62}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{69}
}
func (x *SkillTextReadResponse) GetContent() string {
@@ -5078,7 +5958,7 @@ type SkillManageRequest struct {
func (x *SkillManageRequest) Reset() {
*x = SkillManageRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[63]
+ mi := &file_proto_v1_gateway_proto_msgTypes[70]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5090,7 +5970,7 @@ func (x *SkillManageRequest) String() string {
func (*SkillManageRequest) ProtoMessage() {}
func (x *SkillManageRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[63]
+ mi := &file_proto_v1_gateway_proto_msgTypes[70]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5103,7 +5983,7 @@ func (x *SkillManageRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillManageRequest.ProtoReflect.Descriptor instead.
func (*SkillManageRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{63}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{70}
}
func (x *SkillManageRequest) GetPayloadJson() string {
@@ -5122,7 +6002,7 @@ type SkillManageResponse struct {
func (x *SkillManageResponse) Reset() {
*x = SkillManageResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[64]
+ mi := &file_proto_v1_gateway_proto_msgTypes[71]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5134,7 +6014,7 @@ func (x *SkillManageResponse) String() string {
func (*SkillManageResponse) ProtoMessage() {}
func (x *SkillManageResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[64]
+ mi := &file_proto_v1_gateway_proto_msgTypes[71]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5147,7 +6027,7 @@ func (x *SkillManageResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SkillManageResponse.ProtoReflect.Descriptor instead.
func (*SkillManageResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{64}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{71}
}
func (x *SkillManageResponse) GetResultJson() string {
@@ -5168,7 +6048,7 @@ type FileMentionListRequest struct {
func (x *FileMentionListRequest) Reset() {
*x = FileMentionListRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[65]
+ mi := &file_proto_v1_gateway_proto_msgTypes[72]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5180,7 +6060,7 @@ func (x *FileMentionListRequest) String() string {
func (*FileMentionListRequest) ProtoMessage() {}
func (x *FileMentionListRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[65]
+ mi := &file_proto_v1_gateway_proto_msgTypes[72]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5193,7 +6073,7 @@ func (x *FileMentionListRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FileMentionListRequest.ProtoReflect.Descriptor instead.
func (*FileMentionListRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{65}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{72}
}
func (x *FileMentionListRequest) GetWorkdir() string {
@@ -5227,7 +6107,7 @@ type FileMentionEntry struct {
func (x *FileMentionEntry) Reset() {
*x = FileMentionEntry{}
- mi := &file_proto_v1_gateway_proto_msgTypes[66]
+ mi := &file_proto_v1_gateway_proto_msgTypes[73]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5239,7 +6119,7 @@ func (x *FileMentionEntry) String() string {
func (*FileMentionEntry) ProtoMessage() {}
func (x *FileMentionEntry) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[66]
+ mi := &file_proto_v1_gateway_proto_msgTypes[73]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5252,7 +6132,7 @@ func (x *FileMentionEntry) ProtoReflect() protoreflect.Message {
// Deprecated: Use FileMentionEntry.ProtoReflect.Descriptor instead.
func (*FileMentionEntry) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{66}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{73}
}
func (x *FileMentionEntry) GetPath() string {
@@ -5279,7 +6159,7 @@ type FileMentionListResponse struct {
func (x *FileMentionListResponse) Reset() {
*x = FileMentionListResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[67]
+ mi := &file_proto_v1_gateway_proto_msgTypes[74]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5291,7 +6171,7 @@ func (x *FileMentionListResponse) String() string {
func (*FileMentionListResponse) ProtoMessage() {}
func (x *FileMentionListResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[67]
+ mi := &file_proto_v1_gateway_proto_msgTypes[74]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5304,7 +6184,7 @@ func (x *FileMentionListResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FileMentionListResponse.ProtoReflect.Descriptor instead.
func (*FileMentionListResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{67}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{74}
}
func (x *FileMentionListResponse) GetEntries() []*FileMentionEntry {
@@ -5333,7 +6213,7 @@ type FsRoot struct {
func (x *FsRoot) Reset() {
*x = FsRoot{}
- mi := &file_proto_v1_gateway_proto_msgTypes[68]
+ mi := &file_proto_v1_gateway_proto_msgTypes[75]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5345,7 +6225,7 @@ func (x *FsRoot) String() string {
func (*FsRoot) ProtoMessage() {}
func (x *FsRoot) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[68]
+ mi := &file_proto_v1_gateway_proto_msgTypes[75]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5358,7 +6238,7 @@ func (x *FsRoot) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsRoot.ProtoReflect.Descriptor instead.
func (*FsRoot) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{68}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{75}
}
func (x *FsRoot) GetId() string {
@@ -5397,7 +6277,7 @@ type FsRootsRequest struct {
func (x *FsRootsRequest) Reset() {
*x = FsRootsRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[69]
+ mi := &file_proto_v1_gateway_proto_msgTypes[76]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5409,7 +6289,7 @@ func (x *FsRootsRequest) String() string {
func (*FsRootsRequest) ProtoMessage() {}
func (x *FsRootsRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[69]
+ mi := &file_proto_v1_gateway_proto_msgTypes[76]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5422,7 +6302,7 @@ func (x *FsRootsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsRootsRequest.ProtoReflect.Descriptor instead.
func (*FsRootsRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{69}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{76}
}
type FsRootsResponse struct {
@@ -5434,7 +6314,7 @@ type FsRootsResponse struct {
func (x *FsRootsResponse) Reset() {
*x = FsRootsResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[70]
+ mi := &file_proto_v1_gateway_proto_msgTypes[77]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5446,7 +6326,7 @@ func (x *FsRootsResponse) String() string {
func (*FsRootsResponse) ProtoMessage() {}
func (x *FsRootsResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[70]
+ mi := &file_proto_v1_gateway_proto_msgTypes[77]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5459,7 +6339,7 @@ func (x *FsRootsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsRootsResponse.ProtoReflect.Descriptor instead.
func (*FsRootsResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{70}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{77}
}
func (x *FsRootsResponse) GetRoots() []*FsRoot {
@@ -5479,7 +6359,7 @@ type FsListDirsRequest struct {
func (x *FsListDirsRequest) Reset() {
*x = FsListDirsRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[71]
+ mi := &file_proto_v1_gateway_proto_msgTypes[78]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5491,7 +6371,7 @@ func (x *FsListDirsRequest) String() string {
func (*FsListDirsRequest) ProtoMessage() {}
func (x *FsListDirsRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[71]
+ mi := &file_proto_v1_gateway_proto_msgTypes[78]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5504,7 +6384,7 @@ func (x *FsListDirsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsListDirsRequest.ProtoReflect.Descriptor instead.
func (*FsListDirsRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{71}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{78}
}
func (x *FsListDirsRequest) GetPath() string {
@@ -5531,7 +6411,7 @@ type FsDirEntry struct {
func (x *FsDirEntry) Reset() {
*x = FsDirEntry{}
- mi := &file_proto_v1_gateway_proto_msgTypes[72]
+ mi := &file_proto_v1_gateway_proto_msgTypes[79]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5543,7 +6423,7 @@ func (x *FsDirEntry) String() string {
func (*FsDirEntry) ProtoMessage() {}
func (x *FsDirEntry) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[72]
+ mi := &file_proto_v1_gateway_proto_msgTypes[79]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5556,7 +6436,7 @@ func (x *FsDirEntry) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsDirEntry.ProtoReflect.Descriptor instead.
func (*FsDirEntry) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{72}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{79}
}
func (x *FsDirEntry) GetPath() string {
@@ -5584,7 +6464,7 @@ type FsListDirsResponse struct {
func (x *FsListDirsResponse) Reset() {
*x = FsListDirsResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[73]
+ mi := &file_proto_v1_gateway_proto_msgTypes[80]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5596,7 +6476,7 @@ func (x *FsListDirsResponse) String() string {
func (*FsListDirsResponse) ProtoMessage() {}
func (x *FsListDirsResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[73]
+ mi := &file_proto_v1_gateway_proto_msgTypes[80]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5609,7 +6489,7 @@ func (x *FsListDirsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsListDirsResponse.ProtoReflect.Descriptor instead.
func (*FsListDirsResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{73}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{80}
}
func (x *FsListDirsResponse) GetPath() string {
@@ -5643,7 +6523,7 @@ type FsCreateProjectFolderRequest struct {
func (x *FsCreateProjectFolderRequest) Reset() {
*x = FsCreateProjectFolderRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[74]
+ mi := &file_proto_v1_gateway_proto_msgTypes[81]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5655,7 +6535,7 @@ func (x *FsCreateProjectFolderRequest) String() string {
func (*FsCreateProjectFolderRequest) ProtoMessage() {}
func (x *FsCreateProjectFolderRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[74]
+ mi := &file_proto_v1_gateway_proto_msgTypes[81]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5668,7 +6548,7 @@ func (x *FsCreateProjectFolderRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsCreateProjectFolderRequest.ProtoReflect.Descriptor instead.
func (*FsCreateProjectFolderRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{74}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{81}
}
func (x *FsCreateProjectFolderRequest) GetParent() string {
@@ -5694,7 +6574,7 @@ type FsCreateProjectFolderResponse struct {
func (x *FsCreateProjectFolderResponse) Reset() {
*x = FsCreateProjectFolderResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[75]
+ mi := &file_proto_v1_gateway_proto_msgTypes[82]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5706,7 +6586,7 @@ func (x *FsCreateProjectFolderResponse) String() string {
func (*FsCreateProjectFolderResponse) ProtoMessage() {}
func (x *FsCreateProjectFolderResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[75]
+ mi := &file_proto_v1_gateway_proto_msgTypes[82]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5719,7 +6599,7 @@ func (x *FsCreateProjectFolderResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsCreateProjectFolderResponse.ProtoReflect.Descriptor instead.
func (*FsCreateProjectFolderResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{75}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{82}
}
func (x *FsCreateProjectFolderResponse) GetPath() string {
@@ -5742,7 +6622,7 @@ type FsListRequest struct {
func (x *FsListRequest) Reset() {
*x = FsListRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[76]
+ mi := &file_proto_v1_gateway_proto_msgTypes[83]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5754,7 +6634,7 @@ func (x *FsListRequest) String() string {
func (*FsListRequest) ProtoMessage() {}
func (x *FsListRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[76]
+ mi := &file_proto_v1_gateway_proto_msgTypes[83]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5767,7 +6647,7 @@ func (x *FsListRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsListRequest.ProtoReflect.Descriptor instead.
func (*FsListRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{76}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{83}
}
func (x *FsListRequest) GetWorkdir() string {
@@ -5815,7 +6695,7 @@ type FsListEntry struct {
func (x *FsListEntry) Reset() {
*x = FsListEntry{}
- mi := &file_proto_v1_gateway_proto_msgTypes[77]
+ mi := &file_proto_v1_gateway_proto_msgTypes[84]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5827,7 +6707,7 @@ func (x *FsListEntry) String() string {
func (*FsListEntry) ProtoMessage() {}
func (x *FsListEntry) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[77]
+ mi := &file_proto_v1_gateway_proto_msgTypes[84]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5840,7 +6720,7 @@ func (x *FsListEntry) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsListEntry.ProtoReflect.Descriptor instead.
func (*FsListEntry) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{77}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{84}
}
func (x *FsListEntry) GetPath() string {
@@ -5873,7 +6753,7 @@ type FsListResponse struct {
func (x *FsListResponse) Reset() {
*x = FsListResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[78]
+ mi := &file_proto_v1_gateway_proto_msgTypes[85]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5885,7 +6765,7 @@ func (x *FsListResponse) String() string {
func (*FsListResponse) ProtoMessage() {}
func (x *FsListResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[78]
+ mi := &file_proto_v1_gateway_proto_msgTypes[85]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5898,7 +6778,7 @@ func (x *FsListResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsListResponse.ProtoReflect.Descriptor instead.
func (*FsListResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{78}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{85}
}
func (x *FsListResponse) GetPath() string {
@@ -5967,7 +6847,7 @@ type FsReadEditableTextRequest struct {
func (x *FsReadEditableTextRequest) Reset() {
*x = FsReadEditableTextRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[79]
+ mi := &file_proto_v1_gateway_proto_msgTypes[86]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -5979,7 +6859,7 @@ func (x *FsReadEditableTextRequest) String() string {
func (*FsReadEditableTextRequest) ProtoMessage() {}
func (x *FsReadEditableTextRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[79]
+ mi := &file_proto_v1_gateway_proto_msgTypes[86]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5992,7 +6872,7 @@ func (x *FsReadEditableTextRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsReadEditableTextRequest.ProtoReflect.Descriptor instead.
func (*FsReadEditableTextRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{79}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{86}
}
func (x *FsReadEditableTextRequest) GetWorkdir() string {
@@ -6023,7 +6903,7 @@ type FsReadEditableTextResponse struct {
func (x *FsReadEditableTextResponse) Reset() {
*x = FsReadEditableTextResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[80]
+ mi := &file_proto_v1_gateway_proto_msgTypes[87]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6035,7 +6915,7 @@ func (x *FsReadEditableTextResponse) String() string {
func (*FsReadEditableTextResponse) ProtoMessage() {}
func (x *FsReadEditableTextResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[80]
+ mi := &file_proto_v1_gateway_proto_msgTypes[87]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6048,7 +6928,7 @@ func (x *FsReadEditableTextResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsReadEditableTextResponse.ProtoReflect.Descriptor instead.
func (*FsReadEditableTextResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{80}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{87}
}
func (x *FsReadEditableTextResponse) GetPath() string {
@@ -6103,7 +6983,7 @@ type FsReadWorkspaceImageRequest struct {
func (x *FsReadWorkspaceImageRequest) Reset() {
*x = FsReadWorkspaceImageRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[81]
+ mi := &file_proto_v1_gateway_proto_msgTypes[88]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6115,7 +6995,7 @@ func (x *FsReadWorkspaceImageRequest) String() string {
func (*FsReadWorkspaceImageRequest) ProtoMessage() {}
func (x *FsReadWorkspaceImageRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[81]
+ mi := &file_proto_v1_gateway_proto_msgTypes[88]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6128,7 +7008,7 @@ func (x *FsReadWorkspaceImageRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsReadWorkspaceImageRequest.ProtoReflect.Descriptor instead.
func (*FsReadWorkspaceImageRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{81}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{88}
}
func (x *FsReadWorkspaceImageRequest) GetWorkdir() string {
@@ -6159,7 +7039,7 @@ type FsReadWorkspaceImageResponse struct {
func (x *FsReadWorkspaceImageResponse) Reset() {
*x = FsReadWorkspaceImageResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[82]
+ mi := &file_proto_v1_gateway_proto_msgTypes[89]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6171,7 +7051,7 @@ func (x *FsReadWorkspaceImageResponse) String() string {
func (*FsReadWorkspaceImageResponse) ProtoMessage() {}
func (x *FsReadWorkspaceImageResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[82]
+ mi := &file_proto_v1_gateway_proto_msgTypes[89]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6184,7 +7064,7 @@ func (x *FsReadWorkspaceImageResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsReadWorkspaceImageResponse.ProtoReflect.Descriptor instead.
func (*FsReadWorkspaceImageResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{82}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{89}
}
func (x *FsReadWorkspaceImageResponse) GetPath() string {
@@ -6245,7 +7125,7 @@ type FsWriteTextRequest struct {
func (x *FsWriteTextRequest) Reset() {
*x = FsWriteTextRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[83]
+ mi := &file_proto_v1_gateway_proto_msgTypes[90]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6257,7 +7137,7 @@ func (x *FsWriteTextRequest) String() string {
func (*FsWriteTextRequest) ProtoMessage() {}
func (x *FsWriteTextRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[83]
+ mi := &file_proto_v1_gateway_proto_msgTypes[90]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6270,7 +7150,7 @@ func (x *FsWriteTextRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsWriteTextRequest.ProtoReflect.Descriptor instead.
func (*FsWriteTextRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{83}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{90}
}
func (x *FsWriteTextRequest) GetWorkdir() string {
@@ -6344,7 +7224,7 @@ type FsWriteTextResponse struct {
func (x *FsWriteTextResponse) Reset() {
*x = FsWriteTextResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[84]
+ mi := &file_proto_v1_gateway_proto_msgTypes[91]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6356,7 +7236,7 @@ func (x *FsWriteTextResponse) String() string {
func (*FsWriteTextResponse) ProtoMessage() {}
func (x *FsWriteTextResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[84]
+ mi := &file_proto_v1_gateway_proto_msgTypes[91]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6369,7 +7249,7 @@ func (x *FsWriteTextResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsWriteTextResponse.ProtoReflect.Descriptor instead.
func (*FsWriteTextResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{84}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{91}
}
func (x *FsWriteTextResponse) GetPath() string {
@@ -6431,7 +7311,7 @@ type FsCreateDirRequest struct {
func (x *FsCreateDirRequest) Reset() {
*x = FsCreateDirRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[85]
+ mi := &file_proto_v1_gateway_proto_msgTypes[92]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6443,7 +7323,7 @@ func (x *FsCreateDirRequest) String() string {
func (*FsCreateDirRequest) ProtoMessage() {}
func (x *FsCreateDirRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[85]
+ mi := &file_proto_v1_gateway_proto_msgTypes[92]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6456,7 +7336,7 @@ func (x *FsCreateDirRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsCreateDirRequest.ProtoReflect.Descriptor instead.
func (*FsCreateDirRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{85}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{92}
}
func (x *FsCreateDirRequest) GetWorkdir() string {
@@ -6483,7 +7363,7 @@ type FsCreateDirResponse struct {
func (x *FsCreateDirResponse) Reset() {
*x = FsCreateDirResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[86]
+ mi := &file_proto_v1_gateway_proto_msgTypes[93]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6495,7 +7375,7 @@ func (x *FsCreateDirResponse) String() string {
func (*FsCreateDirResponse) ProtoMessage() {}
func (x *FsCreateDirResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[86]
+ mi := &file_proto_v1_gateway_proto_msgTypes[93]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6508,7 +7388,7 @@ func (x *FsCreateDirResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsCreateDirResponse.ProtoReflect.Descriptor instead.
func (*FsCreateDirResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{86}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{93}
}
func (x *FsCreateDirResponse) GetPath() string {
@@ -6536,7 +7416,7 @@ type FsRenameRequest struct {
func (x *FsRenameRequest) Reset() {
*x = FsRenameRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[87]
+ mi := &file_proto_v1_gateway_proto_msgTypes[94]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6548,7 +7428,7 @@ func (x *FsRenameRequest) String() string {
func (*FsRenameRequest) ProtoMessage() {}
func (x *FsRenameRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[87]
+ mi := &file_proto_v1_gateway_proto_msgTypes[94]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6561,7 +7441,7 @@ func (x *FsRenameRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsRenameRequest.ProtoReflect.Descriptor instead.
func (*FsRenameRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{87}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{94}
}
func (x *FsRenameRequest) GetWorkdir() string {
@@ -6596,7 +7476,7 @@ type FsRenameResponse struct {
func (x *FsRenameResponse) Reset() {
*x = FsRenameResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[88]
+ mi := &file_proto_v1_gateway_proto_msgTypes[95]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6608,7 +7488,7 @@ func (x *FsRenameResponse) String() string {
func (*FsRenameResponse) ProtoMessage() {}
func (x *FsRenameResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[88]
+ mi := &file_proto_v1_gateway_proto_msgTypes[95]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6621,7 +7501,7 @@ func (x *FsRenameResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsRenameResponse.ProtoReflect.Descriptor instead.
func (*FsRenameResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{88}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{95}
}
func (x *FsRenameResponse) GetFromPath() string {
@@ -6655,7 +7535,7 @@ type FsDeleteRequest struct {
func (x *FsDeleteRequest) Reset() {
*x = FsDeleteRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[89]
+ mi := &file_proto_v1_gateway_proto_msgTypes[96]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6667,7 +7547,7 @@ func (x *FsDeleteRequest) String() string {
func (*FsDeleteRequest) ProtoMessage() {}
func (x *FsDeleteRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[89]
+ mi := &file_proto_v1_gateway_proto_msgTypes[96]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6680,7 +7560,7 @@ func (x *FsDeleteRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsDeleteRequest.ProtoReflect.Descriptor instead.
func (*FsDeleteRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{89}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{96}
}
func (x *FsDeleteRequest) GetWorkdir() string {
@@ -6707,7 +7587,7 @@ type FsDeleteResponse struct {
func (x *FsDeleteResponse) Reset() {
*x = FsDeleteResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[90]
+ mi := &file_proto_v1_gateway_proto_msgTypes[97]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6719,7 +7599,7 @@ func (x *FsDeleteResponse) String() string {
func (*FsDeleteResponse) ProtoMessage() {}
func (x *FsDeleteResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[90]
+ mi := &file_proto_v1_gateway_proto_msgTypes[97]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6732,7 +7612,7 @@ func (x *FsDeleteResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use FsDeleteResponse.ProtoReflect.Descriptor instead.
func (*FsDeleteResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{90}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{97}
}
func (x *FsDeleteResponse) GetPath() string {
@@ -6758,7 +7638,7 @@ type PingRequest struct {
func (x *PingRequest) Reset() {
*x = PingRequest{}
- mi := &file_proto_v1_gateway_proto_msgTypes[91]
+ mi := &file_proto_v1_gateway_proto_msgTypes[98]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6770,7 +7650,7 @@ func (x *PingRequest) String() string {
func (*PingRequest) ProtoMessage() {}
func (x *PingRequest) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[91]
+ mi := &file_proto_v1_gateway_proto_msgTypes[98]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6783,7 +7663,7 @@ func (x *PingRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use PingRequest.ProtoReflect.Descriptor instead.
func (*PingRequest) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{91}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{98}
}
func (x *PingRequest) GetTimestamp() int64 {
@@ -6802,7 +7682,7 @@ type PongResponse struct {
func (x *PongResponse) Reset() {
*x = PongResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[92]
+ mi := &file_proto_v1_gateway_proto_msgTypes[99]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6814,7 +7694,7 @@ func (x *PongResponse) String() string {
func (*PongResponse) ProtoMessage() {}
func (x *PongResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[92]
+ mi := &file_proto_v1_gateway_proto_msgTypes[99]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6827,7 +7707,7 @@ func (x *PongResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use PongResponse.ProtoReflect.Descriptor instead.
func (*PongResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{92}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{99}
}
func (x *PongResponse) GetTimestamp() int64 {
@@ -6847,7 +7727,7 @@ type ErrorResponse struct {
func (x *ErrorResponse) Reset() {
*x = ErrorResponse{}
- mi := &file_proto_v1_gateway_proto_msgTypes[93]
+ mi := &file_proto_v1_gateway_proto_msgTypes[100]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -6859,7 +7739,7 @@ func (x *ErrorResponse) String() string {
func (*ErrorResponse) ProtoMessage() {}
func (x *ErrorResponse) ProtoReflect() protoreflect.Message {
- mi := &file_proto_v1_gateway_proto_msgTypes[93]
+ mi := &file_proto_v1_gateway_proto_msgTypes[100]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -6872,7 +7752,7 @@ func (x *ErrorResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ErrorResponse.ProtoReflect.Descriptor instead.
func (*ErrorResponse) Descriptor() ([]byte, []int) {
- return file_proto_v1_gateway_proto_rawDescGZIP(), []int{93}
+ return file_proto_v1_gateway_proto_rawDescGZIP(), []int{100}
}
func (x *ErrorResponse) GetCode() int32 {
@@ -6902,7 +7782,7 @@ const file_proto_v1_gateway_proto_rawDesc = "" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x18\n" +
"\amessage\x18\x02 \x01(\tR\amessage\x12\x1d\n" +
"\n" +
- "session_id\x18\x03 \x01(\tR\tsessionId\"\x89\x19\n" +
+ "session_id\x18\x03 \x01(\tR\tsessionId\"\x85\x1b\n" +
"\x0fGatewayEnvelope\x12\x1d\n" +
"\n" +
"request_id\x18\x01 \x01(\tR\trequestId\x12\x1c\n" +
@@ -6950,8 +7830,11 @@ const file_proto_v1_gateway_proto_rawDesc = "" +
"\vgit_request\x18= \x01(\v2 .liveagent.gateway.v1.GitRequestH\x00R\n" +
"gitRequest\x12d\n" +
"\x15fs_read_editable_text\x18> \x01(\v2/.liveagent.gateway.v1.FsReadEditableTextRequestH\x00R\x12fsReadEditableText\x12j\n" +
- "\x17fs_read_workspace_image\x18? \x01(\v21.liveagent.gateway.v1.FsReadWorkspaceImageRequestH\x00R\x14fsReadWorkspaceImageB\t\n" +
- "\apayload\"\xa4\x1d\n" +
+ "\x17fs_read_workspace_image\x18? \x01(\v21.liveagent.gateway.v1.FsReadWorkspaceImageRequestH\x00R\x14fsReadWorkspaceImage\x12S\n" +
+ "\x0etunnel_control\x18C \x01(\v2*.liveagent.gateway.v1.TunnelControlRequestH\x00R\rtunnelControl\x12]\n" +
+ "\x13tunnel_control_resp\x18D \x01(\v2+.liveagent.gateway.v1.TunnelControlResponseH\x00R\x11tunnelControlResp\x12F\n" +
+ "\ftunnel_frame\x18E \x01(\v2!.liveagent.gateway.v1.TunnelFrameH\x00R\vtunnelFrameB\t\n" +
+ "\apayload\"\xc0 \n" +
"\rAgentEnvelope\x12\x1d\n" +
"\n" +
"request_id\x18\x01 \x01(\tR\trequestId\x12\x1c\n" +
@@ -6997,7 +7880,12 @@ const file_proto_v1_gateway_proto_rawDesc = "" +
"\x0efs_delete_resp\x18? \x01(\v2&.liveagent.gateway.v1.FsDeleteResponseH\x00R\ffsDeleteResp\x12F\n" +
"\fgit_response\x18@ \x01(\v2!.liveagent.gateway.v1.GitResponseH\x00R\vgitResponse\x12n\n" +
"\x1afs_read_editable_text_resp\x18A \x01(\v20.liveagent.gateway.v1.FsReadEditableTextResponseH\x00R\x16fsReadEditableTextResp\x12t\n" +
- "\x1cfs_read_workspace_image_resp\x18B \x01(\v22.liveagent.gateway.v1.FsReadWorkspaceImageResponseH\x00R\x18fsReadWorkspaceImageResp\x12;\n" +
+ "\x1cfs_read_workspace_image_resp\x18B \x01(\v22.liveagent.gateway.v1.FsReadWorkspaceImageResponseH\x00R\x18fsReadWorkspaceImageResp\x12S\n" +
+ "\x0etunnel_control\x18C \x01(\v2*.liveagent.gateway.v1.TunnelControlRequestH\x00R\rtunnelControl\x12]\n" +
+ "\x13tunnel_control_resp\x18D \x01(\v2+.liveagent.gateway.v1.TunnelControlResponseH\x00R\x11tunnelControlResp\x12F\n" +
+ "\ftunnel_frame\x18E \x01(\v2!.liveagent.gateway.v1.TunnelFrameH\x00R\vtunnelFrame\x12K\n" +
+ "\fchat_control\x18F \x01(\v2&.liveagent.gateway.v1.ChatControlEventH\x00R\vchatControl\x12Q\n" +
+ "\x0eruntime_status\x18G \x01(\v2(.liveagent.gateway.v1.RuntimeStatusEventH\x00R\rruntimeStatus\x12;\n" +
"\x05error\x18c \x01(\v2#.liveagent.gateway.v1.ErrorResponseH\x00R\x05errorB\t\n" +
"\apayload\"|\n" +
"\x11ChatSelectedModel\x12,\n" +
@@ -7030,7 +7918,65 @@ const file_proto_v1_gateway_proto_rawDesc = "" +
"\rabsolute_path\x18\x02 \x01(\tR\fabsolutePath\"O\n" +
"\x1cUploadedImagePreviewResponse\x12\x1b\n" +
"\tmime_type\x18\x01 \x01(\tR\bmimeType\x12\x12\n" +
- "\x04data\x18\x02 \x01(\tR\x04data\"L\n" +
+ "\x04data\x18\x02 \x01(\tR\x04data\"\xc3\x02\n" +
+ "\x14TunnelControlRequest\x12\x16\n" +
+ "\x06action\x18\x01 \x01(\tR\x06action\x12\x1b\n" +
+ "\ttunnel_id\x18\x02 \x01(\tR\btunnelId\x12\x12\n" +
+ "\x04slug\x18\x03 \x01(\tR\x04slug\x12\x1d\n" +
+ "\n" +
+ "target_url\x18\x04 \x01(\tR\ttargetUrl\x12\x12\n" +
+ "\x04name\x18\x05 \x01(\tR\x04name\x12\x1f\n" +
+ "\vttl_seconds\x18\x06 \x01(\rR\n" +
+ "ttlSeconds\x12\x1d\n" +
+ "\n" +
+ "expires_at\x18\a \x01(\x03R\texpiresAt\x12\x1d\n" +
+ "\n" +
+ "public_url\x18\b \x01(\tR\tpublicUrl\x12&\n" +
+ "\x0fpublic_base_url\x18\t \x01(\tR\rpublicBaseUrl\x12(\n" +
+ "\x10project_path_key\x18\n" +
+ " \x01(\tR\x0eprojectPathKey\"\xef\x01\n" +
+ "\x15TunnelControlResponse\x12\x16\n" +
+ "\x06action\x18\x01 \x01(\tR\x06action\x12=\n" +
+ "\atunnels\x18\x02 \x03(\v2#.liveagent.gateway.v1.TunnelSummaryR\atunnels\x12;\n" +
+ "\x06tunnel\x18\x03 \x01(\v2#.liveagent.gateway.v1.TunnelSummaryR\x06tunnel\x12\x1d\n" +
+ "\n" +
+ "error_code\x18\x04 \x01(\tR\terrorCode\x12#\n" +
+ "\rerror_message\x18\x05 \x01(\tR\ferrorMessage\"\xb4\x02\n" +
+ "\rTunnelSummary\x12\x0e\n" +
+ "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
+ "\x04slug\x18\x02 \x01(\tR\x04slug\x12\x12\n" +
+ "\x04name\x18\x03 \x01(\tR\x04name\x12\x1d\n" +
+ "\n" +
+ "target_url\x18\x04 \x01(\tR\ttargetUrl\x12\x1d\n" +
+ "\n" +
+ "public_url\x18\x05 \x01(\tR\tpublicUrl\x12\x1d\n" +
+ "\n" +
+ "created_at\x18\x06 \x01(\x03R\tcreatedAt\x12\x1d\n" +
+ "\n" +
+ "expires_at\x18\a \x01(\x03R\texpiresAt\x12-\n" +
+ "\x12active_connections\x18\b \x01(\rR\x11activeConnections\x12\x16\n" +
+ "\x06status\x18\t \x01(\tR\x06status\x12(\n" +
+ "\x10project_path_key\x18\n" +
+ " \x01(\tR\x0eprojectPathKey\"8\n" +
+ "\fTunnelHeader\x12\x12\n" +
+ "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" +
+ "\x05value\x18\x02 \x01(\tR\x05value\"\x92\x03\n" +
+ "\vTunnelFrame\x12\x1b\n" +
+ "\tstream_id\x18\x01 \x01(\tR\bstreamId\x12\x1b\n" +
+ "\ttunnel_id\x18\x02 \x01(\tR\btunnelId\x12\x12\n" +
+ "\x04slug\x18\x03 \x01(\tR\x04slug\x129\n" +
+ "\x04kind\x18\x04 \x01(\x0e2%.liveagent.gateway.v1.TunnelFrameKindR\x04kind\x12\x16\n" +
+ "\x06method\x18\x05 \x01(\tR\x06method\x12\x12\n" +
+ "\x04path\x18\x06 \x01(\tR\x04path\x12<\n" +
+ "\aheaders\x18\a \x03(\v2\".liveagent.gateway.v1.TunnelHeaderR\aheaders\x12\x1f\n" +
+ "\vstatus_code\x18\b \x01(\rR\n" +
+ "statusCode\x12\x12\n" +
+ "\x04body\x18\t \x01(\fR\x04body\x12\x1d\n" +
+ "\n" +
+ "end_stream\x18\n" +
+ " \x01(\bR\tendStream\x12\x14\n" +
+ "\x05error\x18\v \x01(\tR\x05error\x12&\n" +
+ "\x0fws_message_type\x18\f \x01(\tR\rwsMessageType\"L\n" +
"\x13MemoryManageRequest\x12\x18\n" +
"\acommand\x18\x01 \x01(\tR\acommand\x12\x1b\n" +
"\targs_json\x18\x02 \x01(\tR\bargsJson\"7\n" +
@@ -7124,7 +8070,25 @@ const file_proto_v1_gateway_proto_rawDesc = "" +
"\x04DONE\x10\x04\x12\t\n" +
"\x05ERROR\x10\x05\x12\x0f\n" +
"\vTOOL_STATUS\x10\x06\x12\x11\n" +
- "\rHOSTED_SEARCH\x10\a\"a\n" +
+ "\rHOSTED_SEARCH\x10\a\"\x98\x02\n" +
+ "\x10ChatControlEvent\x12\x1d\n" +
+ "\n" +
+ "request_id\x18\x01 \x01(\tR\trequestId\x12*\n" +
+ "\x11client_request_id\x18\x02 \x01(\tR\x0fclientRequestId\x12'\n" +
+ "\x0fconversation_id\x18\x03 \x01(\tR\x0econversationId\x12\x1b\n" +
+ "\trun_epoch\x18\x04 \x01(\x03R\brunEpoch\x12\x12\n" +
+ "\x04type\x18\x05 \x01(\tR\x04type\x12\x14\n" +
+ "\x05state\x18\x06 \x01(\tR\x05state\x12\x1d\n" +
+ "\n" +
+ "error_code\x18\a \x01(\tR\terrorCode\x12\x18\n" +
+ "\amessage\x18\b \x01(\tR\amessage\x12\x10\n" +
+ "\x03seq\x18\t \x01(\x03R\x03seq\"\xa9\x01\n" +
+ "\x12RuntimeStatusEvent\x12\x1b\n" +
+ "\tworker_id\x18\x01 \x01(\tR\bworkerId\x12\x14\n" +
+ "\x05state\x18\x02 \x01(\tR\x05state\x12\x18\n" +
+ "\avisible\x18\x03 \x01(\bR\avisible\x12(\n" +
+ "\x10active_run_count\x18\x04 \x01(\rR\x0eactiveRunCount\x12\x1c\n" +
+ "\ttimestamp\x18\x05 \x01(\x03R\ttimestamp\"a\n" +
"\x11CronManageRequest\x12\x16\n" +
"\x06action\x18\x01 \x01(\tR\x06action\x12\x17\n" +
"\atask_id\x18\x02 \x01(\tR\x06taskId\x12\x1b\n" +
@@ -7390,7 +8354,21 @@ const file_proto_v1_gateway_proto_rawDesc = "" +
"\ttimestamp\x18\x01 \x01(\x03R\ttimestamp\"=\n" +
"\rErrorResponse\x12\x12\n" +
"\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" +
- "\amessage\x18\x02 \x01(\tR\amessage2\xc5\x01\n" +
+ "\amessage\x18\x02 \x01(\tR\amessage*\xc7\x03\n" +
+ "\x0fTunnelFrameKind\x12!\n" +
+ "\x1dTUNNEL_FRAME_KIND_UNSPECIFIED\x10\x00\x12(\n" +
+ "$TUNNEL_FRAME_KIND_HTTP_REQUEST_START\x10\x01\x12'\n" +
+ "#TUNNEL_FRAME_KIND_HTTP_REQUEST_BODY\x10\x02\x12&\n" +
+ "\"TUNNEL_FRAME_KIND_HTTP_REQUEST_END\x10\x03\x12)\n" +
+ "%TUNNEL_FRAME_KIND_HTTP_RESPONSE_START\x10\x04\x12(\n" +
+ "$TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY\x10\x05\x12'\n" +
+ "#TUNNEL_FRAME_KIND_HTTP_RESPONSE_END\x10\x06\x12\x1d\n" +
+ "\x19TUNNEL_FRAME_KIND_WS_OPEN\x10\a\x12\x1e\n" +
+ "\x1aTUNNEL_FRAME_KIND_WS_FRAME\x10\b\x12\x1e\n" +
+ "\x1aTUNNEL_FRAME_KIND_WS_CLOSE\x10\t\x12\x1b\n" +
+ "\x17TUNNEL_FRAME_KIND_ERROR\x10\n" +
+ "\x12\x1c\n" +
+ "\x18TUNNEL_FRAME_KIND_CANCEL\x10\v2\xc5\x01\n" +
"\fAgentGateway\x12^\n" +
"\fAgentConnect\x12#.liveagent.gateway.v1.AgentEnvelope\x1a%.liveagent.gateway.v1.GatewayEnvelope(\x010\x01\x12U\n" +
"\fAuthenticate\x12!.liveagent.gateway.v1.AuthRequest\x1a\".liveagent.gateway.v1.AuthResponseB@Z>github.com/liveagent/agent-gateway/internal/proto/v1;gatewayv1b\x06proto3"
@@ -7407,216 +8385,236 @@ func file_proto_v1_gateway_proto_rawDescGZIP() []byte {
return file_proto_v1_gateway_proto_rawDescData
}
-var file_proto_v1_gateway_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
-var file_proto_v1_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 94)
+var file_proto_v1_gateway_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
+var file_proto_v1_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 101)
var file_proto_v1_gateway_proto_goTypes = []any{
- (ChatEvent_ChatEventType)(0), // 0: liveagent.gateway.v1.ChatEvent.ChatEventType
- (*AuthRequest)(nil), // 1: liveagent.gateway.v1.AuthRequest
- (*AuthResponse)(nil), // 2: liveagent.gateway.v1.AuthResponse
- (*GatewayEnvelope)(nil), // 3: liveagent.gateway.v1.GatewayEnvelope
- (*AgentEnvelope)(nil), // 4: liveagent.gateway.v1.AgentEnvelope
- (*ChatSelectedModel)(nil), // 5: liveagent.gateway.v1.ChatSelectedModel
- (*ChatRuntimeControls)(nil), // 6: liveagent.gateway.v1.ChatRuntimeControls
- (*ChatUploadedFile)(nil), // 7: liveagent.gateway.v1.ChatUploadedFile
- (*UploadReadableFile)(nil), // 8: liveagent.gateway.v1.UploadReadableFile
- (*UploadReadableFilesRequest)(nil), // 9: liveagent.gateway.v1.UploadReadableFilesRequest
- (*UploadReadableFilesResponse)(nil), // 10: liveagent.gateway.v1.UploadReadableFilesResponse
- (*UploadedImagePreviewRequest)(nil), // 11: liveagent.gateway.v1.UploadedImagePreviewRequest
- (*UploadedImagePreviewResponse)(nil), // 12: liveagent.gateway.v1.UploadedImagePreviewResponse
- (*MemoryManageRequest)(nil), // 13: liveagent.gateway.v1.MemoryManageRequest
- (*MemoryManageResponse)(nil), // 14: liveagent.gateway.v1.MemoryManageResponse
- (*TerminalRequest)(nil), // 15: liveagent.gateway.v1.TerminalRequest
- (*TerminalSession)(nil), // 16: liveagent.gateway.v1.TerminalSession
- (*TerminalShellOption)(nil), // 17: liveagent.gateway.v1.TerminalShellOption
- (*TerminalResponse)(nil), // 18: liveagent.gateway.v1.TerminalResponse
- (*TerminalEvent)(nil), // 19: liveagent.gateway.v1.TerminalEvent
- (*GitRequest)(nil), // 20: liveagent.gateway.v1.GitRequest
- (*GitResponse)(nil), // 21: liveagent.gateway.v1.GitResponse
- (*ChatRequest)(nil), // 22: liveagent.gateway.v1.ChatRequest
- (*CancelChatRequest)(nil), // 23: liveagent.gateway.v1.CancelChatRequest
- (*ChatEvent)(nil), // 24: liveagent.gateway.v1.ChatEvent
- (*CronManageRequest)(nil), // 25: liveagent.gateway.v1.CronManageRequest
- (*CronManageResponse)(nil), // 26: liveagent.gateway.v1.CronManageResponse
- (*HistoryListRequest)(nil), // 27: liveagent.gateway.v1.HistoryListRequest
- (*HistoryListResponse)(nil), // 28: liveagent.gateway.v1.HistoryListResponse
- (*ConversationSummary)(nil), // 29: liveagent.gateway.v1.ConversationSummary
- (*HistoryGetRequest)(nil), // 30: liveagent.gateway.v1.HistoryGetRequest
- (*HistoryGetResponse)(nil), // 31: liveagent.gateway.v1.HistoryGetResponse
- (*HistoryRenameRequest)(nil), // 32: liveagent.gateway.v1.HistoryRenameRequest
- (*HistoryRenameResponse)(nil), // 33: liveagent.gateway.v1.HistoryRenameResponse
- (*HistoryPinRequest)(nil), // 34: liveagent.gateway.v1.HistoryPinRequest
- (*HistoryPinResponse)(nil), // 35: liveagent.gateway.v1.HistoryPinResponse
- (*HistoryShareStatus)(nil), // 36: liveagent.gateway.v1.HistoryShareStatus
- (*HistoryShareGetRequest)(nil), // 37: liveagent.gateway.v1.HistoryShareGetRequest
- (*HistoryShareGetResponse)(nil), // 38: liveagent.gateway.v1.HistoryShareGetResponse
- (*HistoryShareSetRequest)(nil), // 39: liveagent.gateway.v1.HistoryShareSetRequest
- (*HistoryShareSetResponse)(nil), // 40: liveagent.gateway.v1.HistoryShareSetResponse
- (*HistoryShareResolveRequest)(nil), // 41: liveagent.gateway.v1.HistoryShareResolveRequest
- (*HistoryShareResolveResponse)(nil), // 42: liveagent.gateway.v1.HistoryShareResolveResponse
- (*HistoryWorkdirsRequest)(nil), // 43: liveagent.gateway.v1.HistoryWorkdirsRequest
- (*HistoryWorkdirSummary)(nil), // 44: liveagent.gateway.v1.HistoryWorkdirSummary
- (*HistoryWorkdirsResponse)(nil), // 45: liveagent.gateway.v1.HistoryWorkdirsResponse
- (*HistoryDeleteRequest)(nil), // 46: liveagent.gateway.v1.HistoryDeleteRequest
- (*HistoryDeleteResponse)(nil), // 47: liveagent.gateway.v1.HistoryDeleteResponse
- (*HistoryTruncateRequest)(nil), // 48: liveagent.gateway.v1.HistoryTruncateRequest
- (*HistoryTruncateResponse)(nil), // 49: liveagent.gateway.v1.HistoryTruncateResponse
- (*HistorySyncEvent)(nil), // 50: liveagent.gateway.v1.HistorySyncEvent
- (*ProviderListRequest)(nil), // 51: liveagent.gateway.v1.ProviderListRequest
- (*ProviderListResponse)(nil), // 52: liveagent.gateway.v1.ProviderListResponse
- (*SettingsGetRequest)(nil), // 53: liveagent.gateway.v1.SettingsGetRequest
- (*SettingsGetResponse)(nil), // 54: liveagent.gateway.v1.SettingsGetResponse
- (*SettingsUpdateRequest)(nil), // 55: liveagent.gateway.v1.SettingsUpdateRequest
- (*SettingsUpdateResponse)(nil), // 56: liveagent.gateway.v1.SettingsUpdateResponse
- (*SettingsSyncEvent)(nil), // 57: liveagent.gateway.v1.SettingsSyncEvent
- (*SkillFilesListRequest)(nil), // 58: liveagent.gateway.v1.SkillFilesListRequest
- (*SkillFilesListResponse)(nil), // 59: liveagent.gateway.v1.SkillFilesListResponse
- (*SkillMetadataReadRequest)(nil), // 60: liveagent.gateway.v1.SkillMetadataReadRequest
- (*SkillMetadataReadResponse)(nil), // 61: liveagent.gateway.v1.SkillMetadataReadResponse
- (*SkillTextReadRequest)(nil), // 62: liveagent.gateway.v1.SkillTextReadRequest
- (*SkillTextReadResponse)(nil), // 63: liveagent.gateway.v1.SkillTextReadResponse
- (*SkillManageRequest)(nil), // 64: liveagent.gateway.v1.SkillManageRequest
- (*SkillManageResponse)(nil), // 65: liveagent.gateway.v1.SkillManageResponse
- (*FileMentionListRequest)(nil), // 66: liveagent.gateway.v1.FileMentionListRequest
- (*FileMentionEntry)(nil), // 67: liveagent.gateway.v1.FileMentionEntry
- (*FileMentionListResponse)(nil), // 68: liveagent.gateway.v1.FileMentionListResponse
- (*FsRoot)(nil), // 69: liveagent.gateway.v1.FsRoot
- (*FsRootsRequest)(nil), // 70: liveagent.gateway.v1.FsRootsRequest
- (*FsRootsResponse)(nil), // 71: liveagent.gateway.v1.FsRootsResponse
- (*FsListDirsRequest)(nil), // 72: liveagent.gateway.v1.FsListDirsRequest
- (*FsDirEntry)(nil), // 73: liveagent.gateway.v1.FsDirEntry
- (*FsListDirsResponse)(nil), // 74: liveagent.gateway.v1.FsListDirsResponse
- (*FsCreateProjectFolderRequest)(nil), // 75: liveagent.gateway.v1.FsCreateProjectFolderRequest
- (*FsCreateProjectFolderResponse)(nil), // 76: liveagent.gateway.v1.FsCreateProjectFolderResponse
- (*FsListRequest)(nil), // 77: liveagent.gateway.v1.FsListRequest
- (*FsListEntry)(nil), // 78: liveagent.gateway.v1.FsListEntry
- (*FsListResponse)(nil), // 79: liveagent.gateway.v1.FsListResponse
- (*FsReadEditableTextRequest)(nil), // 80: liveagent.gateway.v1.FsReadEditableTextRequest
- (*FsReadEditableTextResponse)(nil), // 81: liveagent.gateway.v1.FsReadEditableTextResponse
- (*FsReadWorkspaceImageRequest)(nil), // 82: liveagent.gateway.v1.FsReadWorkspaceImageRequest
- (*FsReadWorkspaceImageResponse)(nil), // 83: liveagent.gateway.v1.FsReadWorkspaceImageResponse
- (*FsWriteTextRequest)(nil), // 84: liveagent.gateway.v1.FsWriteTextRequest
- (*FsWriteTextResponse)(nil), // 85: liveagent.gateway.v1.FsWriteTextResponse
- (*FsCreateDirRequest)(nil), // 86: liveagent.gateway.v1.FsCreateDirRequest
- (*FsCreateDirResponse)(nil), // 87: liveagent.gateway.v1.FsCreateDirResponse
- (*FsRenameRequest)(nil), // 88: liveagent.gateway.v1.FsRenameRequest
- (*FsRenameResponse)(nil), // 89: liveagent.gateway.v1.FsRenameResponse
- (*FsDeleteRequest)(nil), // 90: liveagent.gateway.v1.FsDeleteRequest
- (*FsDeleteResponse)(nil), // 91: liveagent.gateway.v1.FsDeleteResponse
- (*PingRequest)(nil), // 92: liveagent.gateway.v1.PingRequest
- (*PongResponse)(nil), // 93: liveagent.gateway.v1.PongResponse
- (*ErrorResponse)(nil), // 94: liveagent.gateway.v1.ErrorResponse
+ (TunnelFrameKind)(0), // 0: liveagent.gateway.v1.TunnelFrameKind
+ (ChatEvent_ChatEventType)(0), // 1: liveagent.gateway.v1.ChatEvent.ChatEventType
+ (*AuthRequest)(nil), // 2: liveagent.gateway.v1.AuthRequest
+ (*AuthResponse)(nil), // 3: liveagent.gateway.v1.AuthResponse
+ (*GatewayEnvelope)(nil), // 4: liveagent.gateway.v1.GatewayEnvelope
+ (*AgentEnvelope)(nil), // 5: liveagent.gateway.v1.AgentEnvelope
+ (*ChatSelectedModel)(nil), // 6: liveagent.gateway.v1.ChatSelectedModel
+ (*ChatRuntimeControls)(nil), // 7: liveagent.gateway.v1.ChatRuntimeControls
+ (*ChatUploadedFile)(nil), // 8: liveagent.gateway.v1.ChatUploadedFile
+ (*UploadReadableFile)(nil), // 9: liveagent.gateway.v1.UploadReadableFile
+ (*UploadReadableFilesRequest)(nil), // 10: liveagent.gateway.v1.UploadReadableFilesRequest
+ (*UploadReadableFilesResponse)(nil), // 11: liveagent.gateway.v1.UploadReadableFilesResponse
+ (*UploadedImagePreviewRequest)(nil), // 12: liveagent.gateway.v1.UploadedImagePreviewRequest
+ (*UploadedImagePreviewResponse)(nil), // 13: liveagent.gateway.v1.UploadedImagePreviewResponse
+ (*TunnelControlRequest)(nil), // 14: liveagent.gateway.v1.TunnelControlRequest
+ (*TunnelControlResponse)(nil), // 15: liveagent.gateway.v1.TunnelControlResponse
+ (*TunnelSummary)(nil), // 16: liveagent.gateway.v1.TunnelSummary
+ (*TunnelHeader)(nil), // 17: liveagent.gateway.v1.TunnelHeader
+ (*TunnelFrame)(nil), // 18: liveagent.gateway.v1.TunnelFrame
+ (*MemoryManageRequest)(nil), // 19: liveagent.gateway.v1.MemoryManageRequest
+ (*MemoryManageResponse)(nil), // 20: liveagent.gateway.v1.MemoryManageResponse
+ (*TerminalRequest)(nil), // 21: liveagent.gateway.v1.TerminalRequest
+ (*TerminalSession)(nil), // 22: liveagent.gateway.v1.TerminalSession
+ (*TerminalShellOption)(nil), // 23: liveagent.gateway.v1.TerminalShellOption
+ (*TerminalResponse)(nil), // 24: liveagent.gateway.v1.TerminalResponse
+ (*TerminalEvent)(nil), // 25: liveagent.gateway.v1.TerminalEvent
+ (*GitRequest)(nil), // 26: liveagent.gateway.v1.GitRequest
+ (*GitResponse)(nil), // 27: liveagent.gateway.v1.GitResponse
+ (*ChatRequest)(nil), // 28: liveagent.gateway.v1.ChatRequest
+ (*CancelChatRequest)(nil), // 29: liveagent.gateway.v1.CancelChatRequest
+ (*ChatEvent)(nil), // 30: liveagent.gateway.v1.ChatEvent
+ (*ChatControlEvent)(nil), // 31: liveagent.gateway.v1.ChatControlEvent
+ (*RuntimeStatusEvent)(nil), // 32: liveagent.gateway.v1.RuntimeStatusEvent
+ (*CronManageRequest)(nil), // 33: liveagent.gateway.v1.CronManageRequest
+ (*CronManageResponse)(nil), // 34: liveagent.gateway.v1.CronManageResponse
+ (*HistoryListRequest)(nil), // 35: liveagent.gateway.v1.HistoryListRequest
+ (*HistoryListResponse)(nil), // 36: liveagent.gateway.v1.HistoryListResponse
+ (*ConversationSummary)(nil), // 37: liveagent.gateway.v1.ConversationSummary
+ (*HistoryGetRequest)(nil), // 38: liveagent.gateway.v1.HistoryGetRequest
+ (*HistoryGetResponse)(nil), // 39: liveagent.gateway.v1.HistoryGetResponse
+ (*HistoryRenameRequest)(nil), // 40: liveagent.gateway.v1.HistoryRenameRequest
+ (*HistoryRenameResponse)(nil), // 41: liveagent.gateway.v1.HistoryRenameResponse
+ (*HistoryPinRequest)(nil), // 42: liveagent.gateway.v1.HistoryPinRequest
+ (*HistoryPinResponse)(nil), // 43: liveagent.gateway.v1.HistoryPinResponse
+ (*HistoryShareStatus)(nil), // 44: liveagent.gateway.v1.HistoryShareStatus
+ (*HistoryShareGetRequest)(nil), // 45: liveagent.gateway.v1.HistoryShareGetRequest
+ (*HistoryShareGetResponse)(nil), // 46: liveagent.gateway.v1.HistoryShareGetResponse
+ (*HistoryShareSetRequest)(nil), // 47: liveagent.gateway.v1.HistoryShareSetRequest
+ (*HistoryShareSetResponse)(nil), // 48: liveagent.gateway.v1.HistoryShareSetResponse
+ (*HistoryShareResolveRequest)(nil), // 49: liveagent.gateway.v1.HistoryShareResolveRequest
+ (*HistoryShareResolveResponse)(nil), // 50: liveagent.gateway.v1.HistoryShareResolveResponse
+ (*HistoryWorkdirsRequest)(nil), // 51: liveagent.gateway.v1.HistoryWorkdirsRequest
+ (*HistoryWorkdirSummary)(nil), // 52: liveagent.gateway.v1.HistoryWorkdirSummary
+ (*HistoryWorkdirsResponse)(nil), // 53: liveagent.gateway.v1.HistoryWorkdirsResponse
+ (*HistoryDeleteRequest)(nil), // 54: liveagent.gateway.v1.HistoryDeleteRequest
+ (*HistoryDeleteResponse)(nil), // 55: liveagent.gateway.v1.HistoryDeleteResponse
+ (*HistoryTruncateRequest)(nil), // 56: liveagent.gateway.v1.HistoryTruncateRequest
+ (*HistoryTruncateResponse)(nil), // 57: liveagent.gateway.v1.HistoryTruncateResponse
+ (*HistorySyncEvent)(nil), // 58: liveagent.gateway.v1.HistorySyncEvent
+ (*ProviderListRequest)(nil), // 59: liveagent.gateway.v1.ProviderListRequest
+ (*ProviderListResponse)(nil), // 60: liveagent.gateway.v1.ProviderListResponse
+ (*SettingsGetRequest)(nil), // 61: liveagent.gateway.v1.SettingsGetRequest
+ (*SettingsGetResponse)(nil), // 62: liveagent.gateway.v1.SettingsGetResponse
+ (*SettingsUpdateRequest)(nil), // 63: liveagent.gateway.v1.SettingsUpdateRequest
+ (*SettingsUpdateResponse)(nil), // 64: liveagent.gateway.v1.SettingsUpdateResponse
+ (*SettingsSyncEvent)(nil), // 65: liveagent.gateway.v1.SettingsSyncEvent
+ (*SkillFilesListRequest)(nil), // 66: liveagent.gateway.v1.SkillFilesListRequest
+ (*SkillFilesListResponse)(nil), // 67: liveagent.gateway.v1.SkillFilesListResponse
+ (*SkillMetadataReadRequest)(nil), // 68: liveagent.gateway.v1.SkillMetadataReadRequest
+ (*SkillMetadataReadResponse)(nil), // 69: liveagent.gateway.v1.SkillMetadataReadResponse
+ (*SkillTextReadRequest)(nil), // 70: liveagent.gateway.v1.SkillTextReadRequest
+ (*SkillTextReadResponse)(nil), // 71: liveagent.gateway.v1.SkillTextReadResponse
+ (*SkillManageRequest)(nil), // 72: liveagent.gateway.v1.SkillManageRequest
+ (*SkillManageResponse)(nil), // 73: liveagent.gateway.v1.SkillManageResponse
+ (*FileMentionListRequest)(nil), // 74: liveagent.gateway.v1.FileMentionListRequest
+ (*FileMentionEntry)(nil), // 75: liveagent.gateway.v1.FileMentionEntry
+ (*FileMentionListResponse)(nil), // 76: liveagent.gateway.v1.FileMentionListResponse
+ (*FsRoot)(nil), // 77: liveagent.gateway.v1.FsRoot
+ (*FsRootsRequest)(nil), // 78: liveagent.gateway.v1.FsRootsRequest
+ (*FsRootsResponse)(nil), // 79: liveagent.gateway.v1.FsRootsResponse
+ (*FsListDirsRequest)(nil), // 80: liveagent.gateway.v1.FsListDirsRequest
+ (*FsDirEntry)(nil), // 81: liveagent.gateway.v1.FsDirEntry
+ (*FsListDirsResponse)(nil), // 82: liveagent.gateway.v1.FsListDirsResponse
+ (*FsCreateProjectFolderRequest)(nil), // 83: liveagent.gateway.v1.FsCreateProjectFolderRequest
+ (*FsCreateProjectFolderResponse)(nil), // 84: liveagent.gateway.v1.FsCreateProjectFolderResponse
+ (*FsListRequest)(nil), // 85: liveagent.gateway.v1.FsListRequest
+ (*FsListEntry)(nil), // 86: liveagent.gateway.v1.FsListEntry
+ (*FsListResponse)(nil), // 87: liveagent.gateway.v1.FsListResponse
+ (*FsReadEditableTextRequest)(nil), // 88: liveagent.gateway.v1.FsReadEditableTextRequest
+ (*FsReadEditableTextResponse)(nil), // 89: liveagent.gateway.v1.FsReadEditableTextResponse
+ (*FsReadWorkspaceImageRequest)(nil), // 90: liveagent.gateway.v1.FsReadWorkspaceImageRequest
+ (*FsReadWorkspaceImageResponse)(nil), // 91: liveagent.gateway.v1.FsReadWorkspaceImageResponse
+ (*FsWriteTextRequest)(nil), // 92: liveagent.gateway.v1.FsWriteTextRequest
+ (*FsWriteTextResponse)(nil), // 93: liveagent.gateway.v1.FsWriteTextResponse
+ (*FsCreateDirRequest)(nil), // 94: liveagent.gateway.v1.FsCreateDirRequest
+ (*FsCreateDirResponse)(nil), // 95: liveagent.gateway.v1.FsCreateDirResponse
+ (*FsRenameRequest)(nil), // 96: liveagent.gateway.v1.FsRenameRequest
+ (*FsRenameResponse)(nil), // 97: liveagent.gateway.v1.FsRenameResponse
+ (*FsDeleteRequest)(nil), // 98: liveagent.gateway.v1.FsDeleteRequest
+ (*FsDeleteResponse)(nil), // 99: liveagent.gateway.v1.FsDeleteResponse
+ (*PingRequest)(nil), // 100: liveagent.gateway.v1.PingRequest
+ (*PongResponse)(nil), // 101: liveagent.gateway.v1.PongResponse
+ (*ErrorResponse)(nil), // 102: liveagent.gateway.v1.ErrorResponse
}
var file_proto_v1_gateway_proto_depIdxs = []int32{
- 22, // 0: liveagent.gateway.v1.GatewayEnvelope.chat_request:type_name -> liveagent.gateway.v1.ChatRequest
- 23, // 1: liveagent.gateway.v1.GatewayEnvelope.cancel_chat:type_name -> liveagent.gateway.v1.CancelChatRequest
- 25, // 2: liveagent.gateway.v1.GatewayEnvelope.cron_manage:type_name -> liveagent.gateway.v1.CronManageRequest
- 27, // 3: liveagent.gateway.v1.GatewayEnvelope.history_list:type_name -> liveagent.gateway.v1.HistoryListRequest
- 30, // 4: liveagent.gateway.v1.GatewayEnvelope.history_get:type_name -> liveagent.gateway.v1.HistoryGetRequest
- 32, // 5: liveagent.gateway.v1.GatewayEnvelope.history_rename:type_name -> liveagent.gateway.v1.HistoryRenameRequest
- 46, // 6: liveagent.gateway.v1.GatewayEnvelope.history_delete:type_name -> liveagent.gateway.v1.HistoryDeleteRequest
- 48, // 7: liveagent.gateway.v1.GatewayEnvelope.history_truncate:type_name -> liveagent.gateway.v1.HistoryTruncateRequest
- 34, // 8: liveagent.gateway.v1.GatewayEnvelope.history_pin:type_name -> liveagent.gateway.v1.HistoryPinRequest
- 37, // 9: liveagent.gateway.v1.GatewayEnvelope.history_share_get:type_name -> liveagent.gateway.v1.HistoryShareGetRequest
- 39, // 10: liveagent.gateway.v1.GatewayEnvelope.history_share_set:type_name -> liveagent.gateway.v1.HistoryShareSetRequest
- 41, // 11: liveagent.gateway.v1.GatewayEnvelope.history_share_resolve:type_name -> liveagent.gateway.v1.HistoryShareResolveRequest
- 43, // 12: liveagent.gateway.v1.GatewayEnvelope.history_workdirs:type_name -> liveagent.gateway.v1.HistoryWorkdirsRequest
- 51, // 13: liveagent.gateway.v1.GatewayEnvelope.provider_list:type_name -> liveagent.gateway.v1.ProviderListRequest
- 53, // 14: liveagent.gateway.v1.GatewayEnvelope.settings_get:type_name -> liveagent.gateway.v1.SettingsGetRequest
- 55, // 15: liveagent.gateway.v1.GatewayEnvelope.settings_update:type_name -> liveagent.gateway.v1.SettingsUpdateRequest
- 58, // 16: liveagent.gateway.v1.GatewayEnvelope.skill_files_list:type_name -> liveagent.gateway.v1.SkillFilesListRequest
- 60, // 17: liveagent.gateway.v1.GatewayEnvelope.skill_metadata_read:type_name -> liveagent.gateway.v1.SkillMetadataReadRequest
- 62, // 18: liveagent.gateway.v1.GatewayEnvelope.skill_text_read:type_name -> liveagent.gateway.v1.SkillTextReadRequest
- 66, // 19: liveagent.gateway.v1.GatewayEnvelope.file_mention_list:type_name -> liveagent.gateway.v1.FileMentionListRequest
- 9, // 20: liveagent.gateway.v1.GatewayEnvelope.upload_readable_files:type_name -> liveagent.gateway.v1.UploadReadableFilesRequest
- 70, // 21: liveagent.gateway.v1.GatewayEnvelope.fs_roots:type_name -> liveagent.gateway.v1.FsRootsRequest
- 72, // 22: liveagent.gateway.v1.GatewayEnvelope.fs_list_dirs:type_name -> liveagent.gateway.v1.FsListDirsRequest
- 92, // 23: liveagent.gateway.v1.GatewayEnvelope.ping:type_name -> liveagent.gateway.v1.PingRequest
- 11, // 24: liveagent.gateway.v1.GatewayEnvelope.uploaded_image_preview:type_name -> liveagent.gateway.v1.UploadedImagePreviewRequest
- 13, // 25: liveagent.gateway.v1.GatewayEnvelope.memory_manage:type_name -> liveagent.gateway.v1.MemoryManageRequest
- 64, // 26: liveagent.gateway.v1.GatewayEnvelope.skill_manage:type_name -> liveagent.gateway.v1.SkillManageRequest
- 75, // 27: liveagent.gateway.v1.GatewayEnvelope.fs_create_project_folder:type_name -> liveagent.gateway.v1.FsCreateProjectFolderRequest
- 15, // 28: liveagent.gateway.v1.GatewayEnvelope.terminal_request:type_name -> liveagent.gateway.v1.TerminalRequest
- 77, // 29: liveagent.gateway.v1.GatewayEnvelope.fs_list:type_name -> liveagent.gateway.v1.FsListRequest
- 84, // 30: liveagent.gateway.v1.GatewayEnvelope.fs_write_text:type_name -> liveagent.gateway.v1.FsWriteTextRequest
- 86, // 31: liveagent.gateway.v1.GatewayEnvelope.fs_create_dir:type_name -> liveagent.gateway.v1.FsCreateDirRequest
- 88, // 32: liveagent.gateway.v1.GatewayEnvelope.fs_rename:type_name -> liveagent.gateway.v1.FsRenameRequest
- 90, // 33: liveagent.gateway.v1.GatewayEnvelope.fs_delete:type_name -> liveagent.gateway.v1.FsDeleteRequest
- 20, // 34: liveagent.gateway.v1.GatewayEnvelope.git_request:type_name -> liveagent.gateway.v1.GitRequest
- 80, // 35: liveagent.gateway.v1.GatewayEnvelope.fs_read_editable_text:type_name -> liveagent.gateway.v1.FsReadEditableTextRequest
- 82, // 36: liveagent.gateway.v1.GatewayEnvelope.fs_read_workspace_image:type_name -> liveagent.gateway.v1.FsReadWorkspaceImageRequest
- 24, // 37: liveagent.gateway.v1.AgentEnvelope.chat_event:type_name -> liveagent.gateway.v1.ChatEvent
- 26, // 38: liveagent.gateway.v1.AgentEnvelope.cron_manage_resp:type_name -> liveagent.gateway.v1.CronManageResponse
- 28, // 39: liveagent.gateway.v1.AgentEnvelope.history_list_resp:type_name -> liveagent.gateway.v1.HistoryListResponse
- 31, // 40: liveagent.gateway.v1.AgentEnvelope.history_get_resp:type_name -> liveagent.gateway.v1.HistoryGetResponse
- 33, // 41: liveagent.gateway.v1.AgentEnvelope.history_rename_resp:type_name -> liveagent.gateway.v1.HistoryRenameResponse
- 47, // 42: liveagent.gateway.v1.AgentEnvelope.history_delete_resp:type_name -> liveagent.gateway.v1.HistoryDeleteResponse
- 50, // 43: liveagent.gateway.v1.AgentEnvelope.history_sync:type_name -> liveagent.gateway.v1.HistorySyncEvent
- 49, // 44: liveagent.gateway.v1.AgentEnvelope.history_truncate_resp:type_name -> liveagent.gateway.v1.HistoryTruncateResponse
- 35, // 45: liveagent.gateway.v1.AgentEnvelope.history_pin_resp:type_name -> liveagent.gateway.v1.HistoryPinResponse
- 38, // 46: liveagent.gateway.v1.AgentEnvelope.history_share_get_resp:type_name -> liveagent.gateway.v1.HistoryShareGetResponse
- 40, // 47: liveagent.gateway.v1.AgentEnvelope.history_share_set_resp:type_name -> liveagent.gateway.v1.HistoryShareSetResponse
- 42, // 48: liveagent.gateway.v1.AgentEnvelope.history_share_resolve_resp:type_name -> liveagent.gateway.v1.HistoryShareResolveResponse
- 45, // 49: liveagent.gateway.v1.AgentEnvelope.history_workdirs_resp:type_name -> liveagent.gateway.v1.HistoryWorkdirsResponse
- 52, // 50: liveagent.gateway.v1.AgentEnvelope.provider_list_resp:type_name -> liveagent.gateway.v1.ProviderListResponse
- 54, // 51: liveagent.gateway.v1.AgentEnvelope.settings_get_resp:type_name -> liveagent.gateway.v1.SettingsGetResponse
- 56, // 52: liveagent.gateway.v1.AgentEnvelope.settings_update_resp:type_name -> liveagent.gateway.v1.SettingsUpdateResponse
- 57, // 53: liveagent.gateway.v1.AgentEnvelope.settings_sync:type_name -> liveagent.gateway.v1.SettingsSyncEvent
- 59, // 54: liveagent.gateway.v1.AgentEnvelope.skill_files_list_resp:type_name -> liveagent.gateway.v1.SkillFilesListResponse
- 61, // 55: liveagent.gateway.v1.AgentEnvelope.skill_metadata_read_resp:type_name -> liveagent.gateway.v1.SkillMetadataReadResponse
- 63, // 56: liveagent.gateway.v1.AgentEnvelope.skill_text_read_resp:type_name -> liveagent.gateway.v1.SkillTextReadResponse
- 68, // 57: liveagent.gateway.v1.AgentEnvelope.file_mention_list_resp:type_name -> liveagent.gateway.v1.FileMentionListResponse
- 10, // 58: liveagent.gateway.v1.AgentEnvelope.upload_readable_files_resp:type_name -> liveagent.gateway.v1.UploadReadableFilesResponse
- 71, // 59: liveagent.gateway.v1.AgentEnvelope.fs_roots_resp:type_name -> liveagent.gateway.v1.FsRootsResponse
- 93, // 60: liveagent.gateway.v1.AgentEnvelope.pong:type_name -> liveagent.gateway.v1.PongResponse
- 74, // 61: liveagent.gateway.v1.AgentEnvelope.fs_list_dirs_resp:type_name -> liveagent.gateway.v1.FsListDirsResponse
- 12, // 62: liveagent.gateway.v1.AgentEnvelope.uploaded_image_preview_resp:type_name -> liveagent.gateway.v1.UploadedImagePreviewResponse
- 14, // 63: liveagent.gateway.v1.AgentEnvelope.memory_manage_resp:type_name -> liveagent.gateway.v1.MemoryManageResponse
- 65, // 64: liveagent.gateway.v1.AgentEnvelope.skill_manage_resp:type_name -> liveagent.gateway.v1.SkillManageResponse
- 76, // 65: liveagent.gateway.v1.AgentEnvelope.fs_create_project_folder_resp:type_name -> liveagent.gateway.v1.FsCreateProjectFolderResponse
- 18, // 66: liveagent.gateway.v1.AgentEnvelope.terminal_response:type_name -> liveagent.gateway.v1.TerminalResponse
- 19, // 67: liveagent.gateway.v1.AgentEnvelope.terminal_event:type_name -> liveagent.gateway.v1.TerminalEvent
- 79, // 68: liveagent.gateway.v1.AgentEnvelope.fs_list_resp:type_name -> liveagent.gateway.v1.FsListResponse
- 85, // 69: liveagent.gateway.v1.AgentEnvelope.fs_write_text_resp:type_name -> liveagent.gateway.v1.FsWriteTextResponse
- 87, // 70: liveagent.gateway.v1.AgentEnvelope.fs_create_dir_resp:type_name -> liveagent.gateway.v1.FsCreateDirResponse
- 89, // 71: liveagent.gateway.v1.AgentEnvelope.fs_rename_resp:type_name -> liveagent.gateway.v1.FsRenameResponse
- 91, // 72: liveagent.gateway.v1.AgentEnvelope.fs_delete_resp:type_name -> liveagent.gateway.v1.FsDeleteResponse
- 21, // 73: liveagent.gateway.v1.AgentEnvelope.git_response:type_name -> liveagent.gateway.v1.GitResponse
- 81, // 74: liveagent.gateway.v1.AgentEnvelope.fs_read_editable_text_resp:type_name -> liveagent.gateway.v1.FsReadEditableTextResponse
- 83, // 75: liveagent.gateway.v1.AgentEnvelope.fs_read_workspace_image_resp:type_name -> liveagent.gateway.v1.FsReadWorkspaceImageResponse
- 94, // 76: liveagent.gateway.v1.AgentEnvelope.error:type_name -> liveagent.gateway.v1.ErrorResponse
- 8, // 77: liveagent.gateway.v1.UploadReadableFilesRequest.files:type_name -> liveagent.gateway.v1.UploadReadableFile
- 7, // 78: liveagent.gateway.v1.UploadReadableFilesResponse.files:type_name -> liveagent.gateway.v1.ChatUploadedFile
- 16, // 79: liveagent.gateway.v1.TerminalResponse.sessions:type_name -> liveagent.gateway.v1.TerminalSession
- 16, // 80: liveagent.gateway.v1.TerminalResponse.session:type_name -> liveagent.gateway.v1.TerminalSession
- 17, // 81: liveagent.gateway.v1.TerminalResponse.shell_options:type_name -> liveagent.gateway.v1.TerminalShellOption
- 16, // 82: liveagent.gateway.v1.TerminalEvent.session:type_name -> liveagent.gateway.v1.TerminalSession
- 5, // 83: liveagent.gateway.v1.ChatRequest.selected_model:type_name -> liveagent.gateway.v1.ChatSelectedModel
- 7, // 84: liveagent.gateway.v1.ChatRequest.uploaded_files:type_name -> liveagent.gateway.v1.ChatUploadedFile
- 6, // 85: liveagent.gateway.v1.ChatRequest.runtime_controls:type_name -> liveagent.gateway.v1.ChatRuntimeControls
- 0, // 86: liveagent.gateway.v1.ChatEvent.type:type_name -> liveagent.gateway.v1.ChatEvent.ChatEventType
- 29, // 87: liveagent.gateway.v1.HistoryListResponse.conversations:type_name -> liveagent.gateway.v1.ConversationSummary
- 29, // 88: liveagent.gateway.v1.HistoryGetResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
- 29, // 89: liveagent.gateway.v1.HistoryRenameResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
- 29, // 90: liveagent.gateway.v1.HistoryPinResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
- 36, // 91: liveagent.gateway.v1.HistoryShareGetResponse.share:type_name -> liveagent.gateway.v1.HistoryShareStatus
- 36, // 92: liveagent.gateway.v1.HistoryShareSetResponse.share:type_name -> liveagent.gateway.v1.HistoryShareStatus
- 29, // 93: liveagent.gateway.v1.HistoryShareResolveResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
- 44, // 94: liveagent.gateway.v1.HistoryWorkdirsResponse.workdirs:type_name -> liveagent.gateway.v1.HistoryWorkdirSummary
- 29, // 95: liveagent.gateway.v1.HistoryTruncateResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
- 29, // 96: liveagent.gateway.v1.HistorySyncEvent.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
- 67, // 97: liveagent.gateway.v1.FileMentionListResponse.entries:type_name -> liveagent.gateway.v1.FileMentionEntry
- 69, // 98: liveagent.gateway.v1.FsRootsResponse.roots:type_name -> liveagent.gateway.v1.FsRoot
- 73, // 99: liveagent.gateway.v1.FsListDirsResponse.entries:type_name -> liveagent.gateway.v1.FsDirEntry
- 78, // 100: liveagent.gateway.v1.FsListResponse.entries:type_name -> liveagent.gateway.v1.FsListEntry
- 4, // 101: liveagent.gateway.v1.AgentGateway.AgentConnect:input_type -> liveagent.gateway.v1.AgentEnvelope
- 1, // 102: liveagent.gateway.v1.AgentGateway.Authenticate:input_type -> liveagent.gateway.v1.AuthRequest
- 3, // 103: liveagent.gateway.v1.AgentGateway.AgentConnect:output_type -> liveagent.gateway.v1.GatewayEnvelope
- 2, // 104: liveagent.gateway.v1.AgentGateway.Authenticate:output_type -> liveagent.gateway.v1.AuthResponse
- 103, // [103:105] is the sub-list for method output_type
- 101, // [101:103] is the sub-list for method input_type
- 101, // [101:101] is the sub-list for extension type_name
- 101, // [101:101] is the sub-list for extension extendee
- 0, // [0:101] is the sub-list for field type_name
+ 28, // 0: liveagent.gateway.v1.GatewayEnvelope.chat_request:type_name -> liveagent.gateway.v1.ChatRequest
+ 29, // 1: liveagent.gateway.v1.GatewayEnvelope.cancel_chat:type_name -> liveagent.gateway.v1.CancelChatRequest
+ 33, // 2: liveagent.gateway.v1.GatewayEnvelope.cron_manage:type_name -> liveagent.gateway.v1.CronManageRequest
+ 35, // 3: liveagent.gateway.v1.GatewayEnvelope.history_list:type_name -> liveagent.gateway.v1.HistoryListRequest
+ 38, // 4: liveagent.gateway.v1.GatewayEnvelope.history_get:type_name -> liveagent.gateway.v1.HistoryGetRequest
+ 40, // 5: liveagent.gateway.v1.GatewayEnvelope.history_rename:type_name -> liveagent.gateway.v1.HistoryRenameRequest
+ 54, // 6: liveagent.gateway.v1.GatewayEnvelope.history_delete:type_name -> liveagent.gateway.v1.HistoryDeleteRequest
+ 56, // 7: liveagent.gateway.v1.GatewayEnvelope.history_truncate:type_name -> liveagent.gateway.v1.HistoryTruncateRequest
+ 42, // 8: liveagent.gateway.v1.GatewayEnvelope.history_pin:type_name -> liveagent.gateway.v1.HistoryPinRequest
+ 45, // 9: liveagent.gateway.v1.GatewayEnvelope.history_share_get:type_name -> liveagent.gateway.v1.HistoryShareGetRequest
+ 47, // 10: liveagent.gateway.v1.GatewayEnvelope.history_share_set:type_name -> liveagent.gateway.v1.HistoryShareSetRequest
+ 49, // 11: liveagent.gateway.v1.GatewayEnvelope.history_share_resolve:type_name -> liveagent.gateway.v1.HistoryShareResolveRequest
+ 51, // 12: liveagent.gateway.v1.GatewayEnvelope.history_workdirs:type_name -> liveagent.gateway.v1.HistoryWorkdirsRequest
+ 59, // 13: liveagent.gateway.v1.GatewayEnvelope.provider_list:type_name -> liveagent.gateway.v1.ProviderListRequest
+ 61, // 14: liveagent.gateway.v1.GatewayEnvelope.settings_get:type_name -> liveagent.gateway.v1.SettingsGetRequest
+ 63, // 15: liveagent.gateway.v1.GatewayEnvelope.settings_update:type_name -> liveagent.gateway.v1.SettingsUpdateRequest
+ 66, // 16: liveagent.gateway.v1.GatewayEnvelope.skill_files_list:type_name -> liveagent.gateway.v1.SkillFilesListRequest
+ 68, // 17: liveagent.gateway.v1.GatewayEnvelope.skill_metadata_read:type_name -> liveagent.gateway.v1.SkillMetadataReadRequest
+ 70, // 18: liveagent.gateway.v1.GatewayEnvelope.skill_text_read:type_name -> liveagent.gateway.v1.SkillTextReadRequest
+ 74, // 19: liveagent.gateway.v1.GatewayEnvelope.file_mention_list:type_name -> liveagent.gateway.v1.FileMentionListRequest
+ 10, // 20: liveagent.gateway.v1.GatewayEnvelope.upload_readable_files:type_name -> liveagent.gateway.v1.UploadReadableFilesRequest
+ 78, // 21: liveagent.gateway.v1.GatewayEnvelope.fs_roots:type_name -> liveagent.gateway.v1.FsRootsRequest
+ 80, // 22: liveagent.gateway.v1.GatewayEnvelope.fs_list_dirs:type_name -> liveagent.gateway.v1.FsListDirsRequest
+ 100, // 23: liveagent.gateway.v1.GatewayEnvelope.ping:type_name -> liveagent.gateway.v1.PingRequest
+ 12, // 24: liveagent.gateway.v1.GatewayEnvelope.uploaded_image_preview:type_name -> liveagent.gateway.v1.UploadedImagePreviewRequest
+ 19, // 25: liveagent.gateway.v1.GatewayEnvelope.memory_manage:type_name -> liveagent.gateway.v1.MemoryManageRequest
+ 72, // 26: liveagent.gateway.v1.GatewayEnvelope.skill_manage:type_name -> liveagent.gateway.v1.SkillManageRequest
+ 83, // 27: liveagent.gateway.v1.GatewayEnvelope.fs_create_project_folder:type_name -> liveagent.gateway.v1.FsCreateProjectFolderRequest
+ 21, // 28: liveagent.gateway.v1.GatewayEnvelope.terminal_request:type_name -> liveagent.gateway.v1.TerminalRequest
+ 85, // 29: liveagent.gateway.v1.GatewayEnvelope.fs_list:type_name -> liveagent.gateway.v1.FsListRequest
+ 92, // 30: liveagent.gateway.v1.GatewayEnvelope.fs_write_text:type_name -> liveagent.gateway.v1.FsWriteTextRequest
+ 94, // 31: liveagent.gateway.v1.GatewayEnvelope.fs_create_dir:type_name -> liveagent.gateway.v1.FsCreateDirRequest
+ 96, // 32: liveagent.gateway.v1.GatewayEnvelope.fs_rename:type_name -> liveagent.gateway.v1.FsRenameRequest
+ 98, // 33: liveagent.gateway.v1.GatewayEnvelope.fs_delete:type_name -> liveagent.gateway.v1.FsDeleteRequest
+ 26, // 34: liveagent.gateway.v1.GatewayEnvelope.git_request:type_name -> liveagent.gateway.v1.GitRequest
+ 88, // 35: liveagent.gateway.v1.GatewayEnvelope.fs_read_editable_text:type_name -> liveagent.gateway.v1.FsReadEditableTextRequest
+ 90, // 36: liveagent.gateway.v1.GatewayEnvelope.fs_read_workspace_image:type_name -> liveagent.gateway.v1.FsReadWorkspaceImageRequest
+ 14, // 37: liveagent.gateway.v1.GatewayEnvelope.tunnel_control:type_name -> liveagent.gateway.v1.TunnelControlRequest
+ 15, // 38: liveagent.gateway.v1.GatewayEnvelope.tunnel_control_resp:type_name -> liveagent.gateway.v1.TunnelControlResponse
+ 18, // 39: liveagent.gateway.v1.GatewayEnvelope.tunnel_frame:type_name -> liveagent.gateway.v1.TunnelFrame
+ 30, // 40: liveagent.gateway.v1.AgentEnvelope.chat_event:type_name -> liveagent.gateway.v1.ChatEvent
+ 34, // 41: liveagent.gateway.v1.AgentEnvelope.cron_manage_resp:type_name -> liveagent.gateway.v1.CronManageResponse
+ 36, // 42: liveagent.gateway.v1.AgentEnvelope.history_list_resp:type_name -> liveagent.gateway.v1.HistoryListResponse
+ 39, // 43: liveagent.gateway.v1.AgentEnvelope.history_get_resp:type_name -> liveagent.gateway.v1.HistoryGetResponse
+ 41, // 44: liveagent.gateway.v1.AgentEnvelope.history_rename_resp:type_name -> liveagent.gateway.v1.HistoryRenameResponse
+ 55, // 45: liveagent.gateway.v1.AgentEnvelope.history_delete_resp:type_name -> liveagent.gateway.v1.HistoryDeleteResponse
+ 58, // 46: liveagent.gateway.v1.AgentEnvelope.history_sync:type_name -> liveagent.gateway.v1.HistorySyncEvent
+ 57, // 47: liveagent.gateway.v1.AgentEnvelope.history_truncate_resp:type_name -> liveagent.gateway.v1.HistoryTruncateResponse
+ 43, // 48: liveagent.gateway.v1.AgentEnvelope.history_pin_resp:type_name -> liveagent.gateway.v1.HistoryPinResponse
+ 46, // 49: liveagent.gateway.v1.AgentEnvelope.history_share_get_resp:type_name -> liveagent.gateway.v1.HistoryShareGetResponse
+ 48, // 50: liveagent.gateway.v1.AgentEnvelope.history_share_set_resp:type_name -> liveagent.gateway.v1.HistoryShareSetResponse
+ 50, // 51: liveagent.gateway.v1.AgentEnvelope.history_share_resolve_resp:type_name -> liveagent.gateway.v1.HistoryShareResolveResponse
+ 53, // 52: liveagent.gateway.v1.AgentEnvelope.history_workdirs_resp:type_name -> liveagent.gateway.v1.HistoryWorkdirsResponse
+ 60, // 53: liveagent.gateway.v1.AgentEnvelope.provider_list_resp:type_name -> liveagent.gateway.v1.ProviderListResponse
+ 62, // 54: liveagent.gateway.v1.AgentEnvelope.settings_get_resp:type_name -> liveagent.gateway.v1.SettingsGetResponse
+ 64, // 55: liveagent.gateway.v1.AgentEnvelope.settings_update_resp:type_name -> liveagent.gateway.v1.SettingsUpdateResponse
+ 65, // 56: liveagent.gateway.v1.AgentEnvelope.settings_sync:type_name -> liveagent.gateway.v1.SettingsSyncEvent
+ 67, // 57: liveagent.gateway.v1.AgentEnvelope.skill_files_list_resp:type_name -> liveagent.gateway.v1.SkillFilesListResponse
+ 69, // 58: liveagent.gateway.v1.AgentEnvelope.skill_metadata_read_resp:type_name -> liveagent.gateway.v1.SkillMetadataReadResponse
+ 71, // 59: liveagent.gateway.v1.AgentEnvelope.skill_text_read_resp:type_name -> liveagent.gateway.v1.SkillTextReadResponse
+ 76, // 60: liveagent.gateway.v1.AgentEnvelope.file_mention_list_resp:type_name -> liveagent.gateway.v1.FileMentionListResponse
+ 11, // 61: liveagent.gateway.v1.AgentEnvelope.upload_readable_files_resp:type_name -> liveagent.gateway.v1.UploadReadableFilesResponse
+ 79, // 62: liveagent.gateway.v1.AgentEnvelope.fs_roots_resp:type_name -> liveagent.gateway.v1.FsRootsResponse
+ 101, // 63: liveagent.gateway.v1.AgentEnvelope.pong:type_name -> liveagent.gateway.v1.PongResponse
+ 82, // 64: liveagent.gateway.v1.AgentEnvelope.fs_list_dirs_resp:type_name -> liveagent.gateway.v1.FsListDirsResponse
+ 13, // 65: liveagent.gateway.v1.AgentEnvelope.uploaded_image_preview_resp:type_name -> liveagent.gateway.v1.UploadedImagePreviewResponse
+ 20, // 66: liveagent.gateway.v1.AgentEnvelope.memory_manage_resp:type_name -> liveagent.gateway.v1.MemoryManageResponse
+ 73, // 67: liveagent.gateway.v1.AgentEnvelope.skill_manage_resp:type_name -> liveagent.gateway.v1.SkillManageResponse
+ 84, // 68: liveagent.gateway.v1.AgentEnvelope.fs_create_project_folder_resp:type_name -> liveagent.gateway.v1.FsCreateProjectFolderResponse
+ 24, // 69: liveagent.gateway.v1.AgentEnvelope.terminal_response:type_name -> liveagent.gateway.v1.TerminalResponse
+ 25, // 70: liveagent.gateway.v1.AgentEnvelope.terminal_event:type_name -> liveagent.gateway.v1.TerminalEvent
+ 87, // 71: liveagent.gateway.v1.AgentEnvelope.fs_list_resp:type_name -> liveagent.gateway.v1.FsListResponse
+ 93, // 72: liveagent.gateway.v1.AgentEnvelope.fs_write_text_resp:type_name -> liveagent.gateway.v1.FsWriteTextResponse
+ 95, // 73: liveagent.gateway.v1.AgentEnvelope.fs_create_dir_resp:type_name -> liveagent.gateway.v1.FsCreateDirResponse
+ 97, // 74: liveagent.gateway.v1.AgentEnvelope.fs_rename_resp:type_name -> liveagent.gateway.v1.FsRenameResponse
+ 99, // 75: liveagent.gateway.v1.AgentEnvelope.fs_delete_resp:type_name -> liveagent.gateway.v1.FsDeleteResponse
+ 27, // 76: liveagent.gateway.v1.AgentEnvelope.git_response:type_name -> liveagent.gateway.v1.GitResponse
+ 89, // 77: liveagent.gateway.v1.AgentEnvelope.fs_read_editable_text_resp:type_name -> liveagent.gateway.v1.FsReadEditableTextResponse
+ 91, // 78: liveagent.gateway.v1.AgentEnvelope.fs_read_workspace_image_resp:type_name -> liveagent.gateway.v1.FsReadWorkspaceImageResponse
+ 14, // 79: liveagent.gateway.v1.AgentEnvelope.tunnel_control:type_name -> liveagent.gateway.v1.TunnelControlRequest
+ 15, // 80: liveagent.gateway.v1.AgentEnvelope.tunnel_control_resp:type_name -> liveagent.gateway.v1.TunnelControlResponse
+ 18, // 81: liveagent.gateway.v1.AgentEnvelope.tunnel_frame:type_name -> liveagent.gateway.v1.TunnelFrame
+ 31, // 82: liveagent.gateway.v1.AgentEnvelope.chat_control:type_name -> liveagent.gateway.v1.ChatControlEvent
+ 32, // 83: liveagent.gateway.v1.AgentEnvelope.runtime_status:type_name -> liveagent.gateway.v1.RuntimeStatusEvent
+ 102, // 84: liveagent.gateway.v1.AgentEnvelope.error:type_name -> liveagent.gateway.v1.ErrorResponse
+ 9, // 85: liveagent.gateway.v1.UploadReadableFilesRequest.files:type_name -> liveagent.gateway.v1.UploadReadableFile
+ 8, // 86: liveagent.gateway.v1.UploadReadableFilesResponse.files:type_name -> liveagent.gateway.v1.ChatUploadedFile
+ 16, // 87: liveagent.gateway.v1.TunnelControlResponse.tunnels:type_name -> liveagent.gateway.v1.TunnelSummary
+ 16, // 88: liveagent.gateway.v1.TunnelControlResponse.tunnel:type_name -> liveagent.gateway.v1.TunnelSummary
+ 0, // 89: liveagent.gateway.v1.TunnelFrame.kind:type_name -> liveagent.gateway.v1.TunnelFrameKind
+ 17, // 90: liveagent.gateway.v1.TunnelFrame.headers:type_name -> liveagent.gateway.v1.TunnelHeader
+ 22, // 91: liveagent.gateway.v1.TerminalResponse.sessions:type_name -> liveagent.gateway.v1.TerminalSession
+ 22, // 92: liveagent.gateway.v1.TerminalResponse.session:type_name -> liveagent.gateway.v1.TerminalSession
+ 23, // 93: liveagent.gateway.v1.TerminalResponse.shell_options:type_name -> liveagent.gateway.v1.TerminalShellOption
+ 22, // 94: liveagent.gateway.v1.TerminalEvent.session:type_name -> liveagent.gateway.v1.TerminalSession
+ 6, // 95: liveagent.gateway.v1.ChatRequest.selected_model:type_name -> liveagent.gateway.v1.ChatSelectedModel
+ 8, // 96: liveagent.gateway.v1.ChatRequest.uploaded_files:type_name -> liveagent.gateway.v1.ChatUploadedFile
+ 7, // 97: liveagent.gateway.v1.ChatRequest.runtime_controls:type_name -> liveagent.gateway.v1.ChatRuntimeControls
+ 1, // 98: liveagent.gateway.v1.ChatEvent.type:type_name -> liveagent.gateway.v1.ChatEvent.ChatEventType
+ 37, // 99: liveagent.gateway.v1.HistoryListResponse.conversations:type_name -> liveagent.gateway.v1.ConversationSummary
+ 37, // 100: liveagent.gateway.v1.HistoryGetResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
+ 37, // 101: liveagent.gateway.v1.HistoryRenameResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
+ 37, // 102: liveagent.gateway.v1.HistoryPinResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
+ 44, // 103: liveagent.gateway.v1.HistoryShareGetResponse.share:type_name -> liveagent.gateway.v1.HistoryShareStatus
+ 44, // 104: liveagent.gateway.v1.HistoryShareSetResponse.share:type_name -> liveagent.gateway.v1.HistoryShareStatus
+ 37, // 105: liveagent.gateway.v1.HistoryShareResolveResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
+ 52, // 106: liveagent.gateway.v1.HistoryWorkdirsResponse.workdirs:type_name -> liveagent.gateway.v1.HistoryWorkdirSummary
+ 37, // 107: liveagent.gateway.v1.HistoryTruncateResponse.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
+ 37, // 108: liveagent.gateway.v1.HistorySyncEvent.conversation:type_name -> liveagent.gateway.v1.ConversationSummary
+ 75, // 109: liveagent.gateway.v1.FileMentionListResponse.entries:type_name -> liveagent.gateway.v1.FileMentionEntry
+ 77, // 110: liveagent.gateway.v1.FsRootsResponse.roots:type_name -> liveagent.gateway.v1.FsRoot
+ 81, // 111: liveagent.gateway.v1.FsListDirsResponse.entries:type_name -> liveagent.gateway.v1.FsDirEntry
+ 86, // 112: liveagent.gateway.v1.FsListResponse.entries:type_name -> liveagent.gateway.v1.FsListEntry
+ 5, // 113: liveagent.gateway.v1.AgentGateway.AgentConnect:input_type -> liveagent.gateway.v1.AgentEnvelope
+ 2, // 114: liveagent.gateway.v1.AgentGateway.Authenticate:input_type -> liveagent.gateway.v1.AuthRequest
+ 4, // 115: liveagent.gateway.v1.AgentGateway.AgentConnect:output_type -> liveagent.gateway.v1.GatewayEnvelope
+ 3, // 116: liveagent.gateway.v1.AgentGateway.Authenticate:output_type -> liveagent.gateway.v1.AuthResponse
+ 115, // [115:117] is the sub-list for method output_type
+ 113, // [113:115] is the sub-list for method input_type
+ 113, // [113:113] is the sub-list for extension type_name
+ 113, // [113:113] is the sub-list for extension extendee
+ 0, // [0:113] is the sub-list for field type_name
}
func init() { file_proto_v1_gateway_proto_init() }
@@ -7662,6 +8660,9 @@ func file_proto_v1_gateway_proto_init() {
(*GatewayEnvelope_GitRequest)(nil),
(*GatewayEnvelope_FsReadEditableText)(nil),
(*GatewayEnvelope_FsReadWorkspaceImage)(nil),
+ (*GatewayEnvelope_TunnelControl)(nil),
+ (*GatewayEnvelope_TunnelControlResp)(nil),
+ (*GatewayEnvelope_TunnelFrame)(nil),
}
file_proto_v1_gateway_proto_msgTypes[3].OneofWrappers = []any{
(*AgentEnvelope_ChatEvent)(nil),
@@ -7703,16 +8704,21 @@ func file_proto_v1_gateway_proto_init() {
(*AgentEnvelope_GitResponse)(nil),
(*AgentEnvelope_FsReadEditableTextResp)(nil),
(*AgentEnvelope_FsReadWorkspaceImageResp)(nil),
+ (*AgentEnvelope_TunnelControl)(nil),
+ (*AgentEnvelope_TunnelControlResp)(nil),
+ (*AgentEnvelope_TunnelFrame)(nil),
+ (*AgentEnvelope_ChatControl)(nil),
+ (*AgentEnvelope_RuntimeStatus)(nil),
(*AgentEnvelope_Error)(nil),
}
- file_proto_v1_gateway_proto_msgTypes[38].OneofWrappers = []any{}
+ file_proto_v1_gateway_proto_msgTypes[45].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_v1_gateway_proto_rawDesc), len(file_proto_v1_gateway_proto_rawDesc)),
- NumEnums: 1,
- NumMessages: 94,
+ NumEnums: 2,
+ NumMessages: 101,
NumExtensions: 0,
NumServices: 1,
},
diff --git a/crates/agent-gateway/internal/server/grpc.go b/crates/agent-gateway/internal/server/grpc.go
index a51f0a494..624ff2993 100644
--- a/crates/agent-gateway/internal/server/grpc.go
+++ b/crates/agent-gateway/internal/server/grpc.go
@@ -59,6 +59,13 @@ func (s *GRPCServer) AgentConnect(stream gatewayv1.AgentGateway_AgentConnectServ
defer cancel()
go s.heartbeatLoop(ctx, sess)
+ go func() {
+ select {
+ case <-ctx.Done():
+ case <-sess.Done():
+ cancel()
+ }
+ }()
sendErrCh := make(chan error, 1)
go func() {
@@ -71,12 +78,23 @@ func (s *GRPCServer) AgentConnect(stream gatewayv1.AgentGateway_AgentConnectServ
sendErrCh <- nil
cancel()
return
- case env := <-toAgent:
- if err := stream.Send(env); err != nil {
+ case outbound := <-toAgent:
+ if outbound == nil || outbound.GatewayEnvelope == nil {
+ continue
+ }
+ select {
+ case <-outbound.Context().Done():
+ outbound.Ack(outbound.Context().Err())
+ continue
+ default:
+ }
+ if err := stream.Send(outbound.GatewayEnvelope); err != nil {
+ outbound.Ack(err)
sendErrCh <- err
cancel()
return
}
+ outbound.Ack(nil)
}
}
}()
@@ -111,13 +129,15 @@ func (s *GRPCServer) AgentConnect(stream gatewayv1.AgentGateway_AgentConnectServ
}
func (s *GRPCServer) heartbeatLoop(ctx context.Context, sess *session.AgentSession) {
- ticker := time.NewTicker(s.cfg.HeartbeatPeriod)
+ period := s.heartbeatPeriod()
+ ticker := time.NewTicker(period)
defer ticker.Stop()
if !s.sendHeartbeat(sess) {
return
}
+ timeout := period * 3
for {
select {
case <-ctx.Done():
@@ -125,6 +145,9 @@ func (s *GRPCServer) heartbeatLoop(ctx context.Context, sess *session.AgentSessi
case <-sess.Done():
return
case <-ticker.C:
+ if s.sm.ClearSessionIfHeartbeatStale(sess, timeout) {
+ return
+ }
if !s.sendHeartbeat(sess) {
return
}
@@ -132,6 +155,13 @@ func (s *GRPCServer) heartbeatLoop(ctx context.Context, sess *session.AgentSessi
}
}
+func (s *GRPCServer) heartbeatPeriod() time.Duration {
+ if s.cfg == nil || s.cfg.HeartbeatPeriod <= 0 {
+ return 30 * time.Second
+ }
+ return s.cfg.HeartbeatPeriod
+}
+
func (s *GRPCServer) sendHeartbeat(sess *session.AgentSession) bool {
ok, err := sess.TrySendToAgent(&gatewayv1.GatewayEnvelope{
RequestId: "ping-" + uuid.NewString(),
diff --git a/crates/agent-gateway/internal/server/http.go b/crates/agent-gateway/internal/server/http.go
index 89e96eebb..446e0481b 100644
--- a/crates/agent-gateway/internal/server/http.go
+++ b/crates/agent-gateway/internal/server/http.go
@@ -24,6 +24,7 @@ func NewHTTPServer(cfg *config.Config, sm *session.Manager) http.Handler {
rootMux := http.NewServeMux()
rootMux.HandleFunc("GET /healthz", handler.Health())
rootMux.Handle("/ws", NewWebSocketServer(cfg, sm))
+ rootMux.HandleFunc("/t/", publicTunnelProxy(sm))
rootMux.HandleFunc("GET /image-proxy", handler.ImageProxy(cfg.RequestTimeout))
rootMux.HandleFunc("GET /api/public/history-shares/{token}", publicHistoryShare(cfg, sm))
@@ -42,6 +43,7 @@ func NewHTTPServer(cfg *config.Config, sm *session.Manager) http.Handler {
}
fileServer := http.FileServer(http.FS(webFS))
serveIndex := func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
http.ServeContent(w, r, "index.html", time.Time{}, bytes.NewReader(indexHTML))
}
@@ -57,18 +59,34 @@ func NewHTTPServer(cfg *config.Config, sm *session.Manager) http.Handler {
if err == nil {
if stat, statErr := file.Stat(); statErr == nil && !stat.IsDir() {
_ = file.Close()
+ if strings.HasPrefix(cleanPath, "assets/") {
+ w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
+ }
fileServer.ServeHTTP(w, r)
return
}
_ = file.Close()
}
+ if isWebUIStaticAssetPath(cleanPath) {
+ http.NotFound(w, r)
+ return
+ }
+
serveIndex(w, r)
})
return rootMux
}
+func isWebUIStaticAssetPath(cleanPath string) bool {
+ cleanPath = strings.TrimSpace(cleanPath)
+ if cleanPath == "" || cleanPath == "." || cleanPath == "index.html" {
+ return false
+ }
+ return strings.HasPrefix(cleanPath, "assets/") || path.Ext(cleanPath) != ""
+}
+
func publicHistoryShare(cfg *config.Config, sm *session.Manager) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := strings.TrimSpace(r.PathValue("token"))
diff --git a/crates/agent-gateway/internal/server/http_test.go b/crates/agent-gateway/internal/server/http_test.go
index 2f780b98d..7909fc6d8 100644
--- a/crates/agent-gateway/internal/server/http_test.go
+++ b/crates/agent-gateway/internal/server/http_test.go
@@ -29,6 +29,9 @@ func TestNewHTTPServerServesRootWithoutRedirect(t *testing.T) {
if !strings.Contains(rec.Body.String(), "
LiveAgent Gateway") {
t.Fatalf("expected WebUI index.html, got body %q", rec.Body.String())
}
+ if cacheControl := rec.Header().Get("Cache-Control"); !strings.Contains(cacheControl, "no-store") {
+ t.Fatalf("Cache-Control = %q, want no-store for index.html", cacheControl)
+ }
}
func TestNewHTTPServerServesSpaFallbackWithoutRedirect(t *testing.T) {
@@ -49,6 +52,30 @@ func TestNewHTTPServerServesSpaFallbackWithoutRedirect(t *testing.T) {
}
}
+func TestNewHTTPServerDoesNotFallbackMissingStaticAssetsToIndex(t *testing.T) {
+ handler := NewHTTPServer(&config.Config{Token: "dev-token"}, session.NewManager())
+
+ for _, target := range []string{
+ "http://gateway.test/assets/missing-module.js",
+ "http://gateway.test/assets/missing-style.css",
+ "http://gateway.test/missing-icon.svg",
+ } {
+ req := httptest.NewRequest(http.MethodGet, target, nil)
+ rec := httptest.NewRecorder()
+ handler.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusNotFound {
+ t.Fatalf("%s status = %d, want %d", target, rec.Code, http.StatusNotFound)
+ }
+ if strings.Contains(rec.Body.String(), "LiveAgent Gateway") {
+ t.Fatalf("%s returned SPA index fallback for a missing static asset", target)
+ }
+ if contentType := rec.Header().Get("Content-Type"); strings.Contains(contentType, "text/html") {
+ t.Fatalf("%s Content-Type = %q, want non-html 404", target, contentType)
+ }
+ }
+}
+
func TestPublicHistoryShareResolvesWithoutAuthorization(t *testing.T) {
sm := session.NewManager()
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
@@ -69,7 +96,9 @@ func TestPublicHistoryShareResolvesWithoutAuthorization(t *testing.T) {
var outbound *gatewayv1.GatewayEnvelope
select {
- case outbound = <-agentSession.Outbound():
+ case delivered := <-agentSession.Outbound():
+ delivered.Ack(nil)
+ outbound = delivered.GatewayEnvelope
case <-time.After(time.Second):
t.Fatal("timed out waiting for public share request")
}
@@ -164,7 +193,9 @@ func publicHistoryShareErrorStatusForTest(t *testing.T, code int, message string
var outbound *gatewayv1.GatewayEnvelope
select {
- case outbound = <-agentSession.Outbound():
+ case delivered := <-agentSession.Outbound():
+ delivered.Ack(nil)
+ outbound = delivered.GatewayEnvelope
case <-time.After(time.Second):
t.Fatal("timed out waiting for public share request")
}
diff --git a/crates/agent-gateway/internal/server/tunnel.go b/crates/agent-gateway/internal/server/tunnel.go
new file mode 100644
index 000000000..33fbc97ab
--- /dev/null
+++ b/crates/agent-gateway/internal/server/tunnel.go
@@ -0,0 +1,722 @@
+package server
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+ "unicode/utf8"
+
+ "github.com/google/uuid"
+ gatewayv1 "github.com/liveagent/agent-gateway/internal/proto/v1"
+ "github.com/liveagent/agent-gateway/internal/session"
+ "golang.org/x/net/websocket"
+)
+
+const tunnelBodyChunkSize = 64 * 1024
+
+func publicTunnelProxy(sm *session.Manager) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if slug, ok := parseTunnelPublicPathWithoutTrailingSlash(r.URL.Path); ok {
+ target := "/t/" + slug + "/"
+ if r.URL.RawQuery != "" {
+ target += "?" + r.URL.RawQuery
+ }
+ http.Redirect(w, r, target, http.StatusPermanentRedirect)
+ return
+ }
+
+ slug, restPath, ok := parseTunnelPublicPath(r.URL.Path)
+ if !ok {
+ writeTunnelError(w, http.StatusNotFound, "tunnel not found")
+ return
+ }
+ if r.URL.RawQuery != "" {
+ restPath += "?" + r.URL.RawQuery
+ }
+
+ if isWebSocketUpgrade(r) {
+ serveTunnelWebSocket(w, r, sm, slug, restPath)
+ return
+ }
+ serveTunnelHTTP(w, r, sm, slug, restPath)
+ }
+}
+
+func serveTunnelHTTP(
+ w http.ResponseWriter,
+ r *http.Request,
+ sm *session.Manager,
+ slug string,
+ restPath string,
+) {
+ streamID := "http-" + uuid.NewString()
+ lease, err := sm.AcquireTunnel(slug, streamID)
+ if err != nil {
+ writeTunnelAcquireError(w, err)
+ return
+ }
+
+ ctx, cancel := context.WithCancel(r.Context())
+ completed := false
+ defer func() {
+ cancel()
+ if !completed {
+ _ = sm.SendTunnelFrameToAgent(&gatewayv1.TunnelFrame{
+ StreamId: streamID,
+ TunnelId: lease.TunnelID(),
+ Slug: slug,
+ Kind: gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL,
+ })
+ }
+ lease.Release()
+ }()
+
+ start := &gatewayv1.TunnelFrame{
+ StreamId: streamID,
+ TunnelId: lease.TunnelID(),
+ Slug: slug,
+ Kind: gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_REQUEST_START,
+ Method: r.Method,
+ Path: restPath,
+ Headers: filteredTunnelRequestHeaders(r.Header),
+ }
+ if err := sm.SendTunnelFrameToAgent(start); err != nil {
+ writeTunnelAcquireError(w, err)
+ return
+ }
+
+ bodyDone := make(chan struct{})
+ go streamTunnelHTTPRequestBody(ctx, sm, lease.TunnelID(), slug, streamID, r.Body, bodyDone)
+
+ responseStarted := false
+ responseHeadersWritten := false
+ responseStatus := http.StatusOK
+ responseHeaders := http.Header{}
+ responseRewriteKind := tunnelResponseRewriteNone
+ var responseBody []byte
+ writeResponseHeaders := func() {
+ if responseHeadersWritten {
+ return
+ }
+ writeTunnelHTTPHeaders(w, responseHeaders)
+ w.WriteHeader(responseStatus)
+ responseHeadersWritten = true
+ }
+ writeBufferedResponse := func() {
+ if responseRewriteKind == tunnelResponseRewriteNone {
+ return
+ }
+ writeResponseHeaders()
+ if len(responseBody) > 0 {
+ _, _ = w.Write(responseBody)
+ responseBody = nil
+ }
+ flushTunnelResponse(w)
+ }
+ for {
+ select {
+ case <-r.Context().Done():
+ return
+ case <-lease.Done():
+ if !responseStarted {
+ writeTunnelError(w, http.StatusBadGateway, "tunnel stream closed")
+ } else if !responseHeadersWritten {
+ writeBufferedResponse()
+ }
+ return
+ case <-bodyDone:
+ bodyDone = nil
+ case frame := <-lease.Frames():
+ if frame == nil {
+ continue
+ }
+ switch frame.GetKind() {
+ case gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_START:
+ if responseStarted {
+ continue
+ }
+ responseStarted = true
+ status := int(frame.GetStatusCode())
+ if status <= 0 {
+ status = http.StatusOK
+ }
+ responseStatus = status
+ responseHeaders = tunnelResponseHeaders(frame, lease.Tunnel())
+ responseRewriteKind = tunnelResponseRewriteKindFor(r.Method, responseStatus, responseHeaders)
+ if responseRewriteKind == tunnelResponseRewriteNone {
+ writeResponseHeaders()
+ flushTunnelResponse(w)
+ }
+ case gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY:
+ if !responseStarted {
+ responseStarted = true
+ responseStatus = http.StatusOK
+ responseHeaders = http.Header{}
+ responseRewriteKind = tunnelResponseRewriteNone
+ writeResponseHeaders()
+ }
+ if body := frame.GetBody(); len(body) > 0 {
+ if responseRewriteKind != tunnelResponseRewriteNone {
+ if len(responseBody)+len(body) <= tunnelRewriteBodyMaxBytes {
+ responseBody = append(responseBody, body...)
+ continue
+ }
+ responseRewriteKind = tunnelResponseRewriteNone
+ writeResponseHeaders()
+ if len(responseBody) > 0 {
+ if _, err := w.Write(responseBody); err != nil {
+ return
+ }
+ responseBody = nil
+ }
+ }
+ writeResponseHeaders()
+ if _, err := w.Write(body); err != nil {
+ return
+ }
+ flushTunnelResponse(w)
+ }
+ case gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_RESPONSE_END:
+ if responseRewriteKind != tunnelResponseRewriteNone {
+ body := responseBody
+ if rewritten, changed := rewriteTunnelResponseBody(body, lease.Tunnel(), responseRewriteKind); changed {
+ body = rewritten
+ responseHeaders.Del("Content-Length")
+ responseHeaders.Del("Etag")
+ responseHeaders.Del("ETag")
+ }
+ writeResponseHeaders()
+ if len(body) > 0 {
+ if _, err := w.Write(body); err != nil {
+ return
+ }
+ }
+ responseBody = nil
+ } else if responseStarted && !responseHeadersWritten {
+ writeResponseHeaders()
+ }
+ completed = true
+ flushTunnelResponse(w)
+ return
+ case gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_ERROR:
+ if !responseStarted {
+ writeTunnelError(w, http.StatusBadGateway, frame.GetError())
+ } else if !responseHeadersWritten {
+ writeBufferedResponse()
+ }
+ return
+ case gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL:
+ if !responseStarted {
+ writeTunnelError(w, http.StatusServiceUnavailable, "tunnel canceled")
+ } else if !responseHeadersWritten {
+ writeBufferedResponse()
+ }
+ return
+ }
+ }
+ }
+}
+
+func serveTunnelWebSocket(
+ w http.ResponseWriter,
+ r *http.Request,
+ sm *session.Manager,
+ slug string,
+ restPath string,
+) {
+ streamID := "ws-" + uuid.NewString()
+ lease, err := sm.AcquireTunnel(slug, streamID)
+ if err != nil {
+ writeTunnelAcquireError(w, err)
+ return
+ }
+ handlerStarted := false
+ handler := websocket.Handler(func(ws *websocket.Conn) {
+ handlerStarted = true
+ defer lease.Release()
+ handleTunnelWebSocket(ws, r, sm, lease, slug, streamID, restPath)
+ })
+ handler.ServeHTTP(w, r)
+ if !handlerStarted {
+ lease.Release()
+ }
+}
+
+func handleTunnelWebSocket(
+ ws *websocket.Conn,
+ r *http.Request,
+ sm *session.Manager,
+ lease *session.TunnelStreamLease,
+ slug string,
+ streamID string,
+ restPath string,
+) {
+ ws.MaxPayloadBytes = 16 * 1024 * 1024
+ closed := false
+ defer func() {
+ if !closed {
+ _ = sm.SendTunnelFrameToAgent(&gatewayv1.TunnelFrame{
+ StreamId: streamID,
+ TunnelId: lease.TunnelID(),
+ Slug: slug,
+ Kind: gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_CLOSE,
+ })
+ }
+ _ = ws.Close()
+ }()
+
+ if err := sm.SendTunnelFrameToAgent(&gatewayv1.TunnelFrame{
+ StreamId: streamID,
+ TunnelId: lease.TunnelID(),
+ Slug: slug,
+ Kind: gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_OPEN,
+ Method: r.Method,
+ Path: restPath,
+ Headers: filteredTunnelRequestHeaders(r.Header),
+ }); err != nil {
+ return
+ }
+
+ if !awaitTunnelWebSocketOpen(ws, lease) {
+ closed = true
+ return
+ }
+
+ browserFrames := make(chan *gatewayv1.TunnelFrame, 64)
+ readerDone := make(chan struct{})
+ go func() {
+ defer close(readerDone)
+ for {
+ var body []byte
+ if err := websocket.Message.Receive(ws, &body); err != nil {
+ return
+ }
+ messageType := "binary"
+ if utf8.Valid(body) {
+ messageType = "text"
+ }
+ frame := &gatewayv1.TunnelFrame{
+ StreamId: streamID,
+ TunnelId: lease.TunnelID(),
+ Slug: slug,
+ Kind: gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_FRAME,
+ Body: body,
+ WsMessageType: messageType,
+ }
+ select {
+ case browserFrames <- frame:
+ case <-lease.Done():
+ return
+ }
+ }
+ }()
+
+ for {
+ select {
+ case <-lease.Done():
+ closed = true
+ return
+ case <-readerDone:
+ closed = true
+ _ = sm.SendTunnelFrameToAgent(&gatewayv1.TunnelFrame{
+ StreamId: streamID,
+ TunnelId: lease.TunnelID(),
+ Slug: slug,
+ Kind: gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_CLOSE,
+ })
+ return
+ case frame := <-browserFrames:
+ if frame != nil {
+ _ = sm.SendTunnelFrameToAgent(frame)
+ }
+ case frame := <-lease.Frames():
+ if frame == nil {
+ continue
+ }
+ switch frame.GetKind() {
+ case gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_FRAME:
+ if strings.EqualFold(frame.GetWsMessageType(), "text") {
+ if err := websocket.Message.Send(ws, string(frame.GetBody())); err != nil {
+ return
+ }
+ } else {
+ if err := websocket.Message.Send(ws, frame.GetBody()); err != nil {
+ return
+ }
+ }
+ case gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_CLOSE,
+ gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL,
+ gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_ERROR:
+ closed = true
+ return
+ }
+ }
+ }
+}
+
+func awaitTunnelWebSocketOpen(ws *websocket.Conn, lease *session.TunnelStreamLease) bool {
+ timer := time.NewTimer(30 * time.Second)
+ defer timer.Stop()
+ for {
+ select {
+ case <-timer.C:
+ return false
+ case <-lease.Done():
+ return false
+ case frame := <-lease.Frames():
+ if frame == nil {
+ continue
+ }
+ switch frame.GetKind() {
+ case gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_OPEN:
+ return true
+ case gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_ERROR,
+ gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL,
+ gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_WS_CLOSE:
+ if frame.GetError() != "" {
+ _ = websocket.Message.Send(ws, frame.GetError())
+ }
+ return false
+ }
+ }
+ }
+}
+
+func streamTunnelHTTPRequestBody(
+ ctx context.Context,
+ sm *session.Manager,
+ tunnelID string,
+ slug string,
+ streamID string,
+ body io.ReadCloser,
+ done chan<- struct{},
+) {
+ defer close(done)
+ defer body.Close()
+
+ buffer := make([]byte, tunnelBodyChunkSize)
+ for {
+ n, err := body.Read(buffer)
+ if n > 0 {
+ chunk := make([]byte, n)
+ copy(chunk, buffer[:n])
+ if sendErr := sm.SendTunnelFrameToAgent(&gatewayv1.TunnelFrame{
+ StreamId: streamID,
+ TunnelId: tunnelID,
+ Slug: slug,
+ Kind: gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_REQUEST_BODY,
+ Body: chunk,
+ }); sendErr != nil {
+ return
+ }
+ }
+ if errors.Is(err, io.EOF) {
+ _ = sm.SendTunnelFrameToAgent(&gatewayv1.TunnelFrame{
+ StreamId: streamID,
+ TunnelId: tunnelID,
+ Slug: slug,
+ Kind: gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_HTTP_REQUEST_END,
+ })
+ return
+ }
+ if err != nil {
+ _ = sm.SendTunnelFrameToAgent(&gatewayv1.TunnelFrame{
+ StreamId: streamID,
+ TunnelId: tunnelID,
+ Slug: slug,
+ Kind: gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL,
+ Error: err.Error(),
+ })
+ return
+ }
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ }
+ }
+}
+
+func parseTunnelPublicPath(rawPath string) (string, string, bool) {
+ if !strings.HasPrefix(rawPath, "/t/") {
+ return "", "", false
+ }
+ trimmed := strings.TrimPrefix(rawPath, "/t/")
+ parts := strings.SplitN(trimmed, "/", 2)
+ slug := strings.TrimSpace(parts[0])
+ if slug == "" {
+ return "", "", false
+ }
+ if len(parts) == 1 || parts[1] == "" {
+ return slug, "/", true
+ }
+ return slug, "/" + parts[1], true
+}
+
+func parseTunnelPublicPathWithoutTrailingSlash(rawPath string) (string, bool) {
+ if !strings.HasPrefix(rawPath, "/t/") {
+ return "", false
+ }
+ trimmed := strings.TrimPrefix(rawPath, "/t/")
+ if trimmed == "" || strings.Contains(trimmed, "/") {
+ return "", false
+ }
+ return strings.TrimSpace(trimmed), strings.TrimSpace(trimmed) != ""
+}
+
+func writeTunnelAcquireError(w http.ResponseWriter, err error) {
+ switch {
+ case errors.Is(err, session.ErrTunnelNotFound), errors.Is(err, session.ErrTunnelExpired):
+ writeTunnelError(w, http.StatusNotFound, "tunnel not found")
+ case errors.Is(err, session.ErrAgentOffline):
+ writeTunnelError(w, http.StatusServiceUnavailable, "agent offline")
+ case errors.Is(err, session.ErrTunnelOverLimit):
+ writeTunnelError(w, http.StatusTooManyRequests, "tunnel connection limit exceeded")
+ default:
+ writeTunnelError(w, http.StatusBadGateway, err.Error())
+ }
+}
+
+func writeTunnelError(w http.ResponseWriter, status int, message string) {
+ message = strings.TrimSpace(message)
+ if message == "" {
+ message = http.StatusText(status)
+ }
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ w.WriteHeader(status)
+ _, _ = w.Write([]byte(message))
+}
+
+func isWebSocketUpgrade(r *http.Request) bool {
+ return strings.EqualFold(r.Header.Get("Upgrade"), "websocket") &&
+ strings.Contains(strings.ToLower(r.Header.Get("Connection")), "upgrade")
+}
+
+func filteredTunnelRequestHeaders(headers http.Header) []*gatewayv1.TunnelHeader {
+ return filteredTunnelHeaders(headers, true)
+}
+
+func filteredTunnelResponseHeaders(headers []*gatewayv1.TunnelHeader) http.Header {
+ out := http.Header{}
+ for _, header := range headers {
+ name := http.CanonicalHeaderKey(strings.TrimSpace(header.GetName()))
+ if name == "" || shouldDropTunnelHeader(name, false) {
+ continue
+ }
+ out.Add(name, header.GetValue())
+ }
+ return out
+}
+
+func filteredTunnelHeaders(headers http.Header, request bool) []*gatewayv1.TunnelHeader {
+ out := make([]*gatewayv1.TunnelHeader, 0, len(headers))
+ for name, values := range headers {
+ canonical := http.CanonicalHeaderKey(strings.TrimSpace(name))
+ if canonical == "" || shouldDropTunnelHeader(canonical, request) {
+ continue
+ }
+ for _, value := range values {
+ out = append(out, &gatewayv1.TunnelHeader{
+ Name: canonical,
+ Value: value,
+ })
+ }
+ }
+ return out
+}
+
+func shouldDropTunnelHeader(name string, request bool) bool {
+ switch strings.ToLower(name) {
+ case "connection",
+ "keep-alive",
+ "proxy-authenticate",
+ "proxy-authorization",
+ "proxy-connection",
+ "te",
+ "trailer",
+ "transfer-encoding",
+ "upgrade":
+ return true
+ case "host":
+ return request
+ default:
+ return false
+ }
+}
+
+func writeTunnelResponseHeaders(
+ w http.ResponseWriter,
+ frame *gatewayv1.TunnelFrame,
+ tunnel *gatewayv1.TunnelSummary,
+) {
+ writeTunnelHTTPHeaders(w, tunnelResponseHeaders(frame, tunnel))
+}
+
+func tunnelResponseHeaders(
+ frame *gatewayv1.TunnelFrame,
+ tunnel *gatewayv1.TunnelSummary,
+) http.Header {
+ headers := filteredTunnelResponseHeaders(frame.GetHeaders())
+ for name, values := range headers {
+ rewritten := make([]string, 0, len(values))
+ for _, value := range values {
+ if strings.EqualFold(name, "Location") {
+ value = rewriteTunnelLocation(value, tunnel)
+ }
+ if strings.EqualFold(name, "Set-Cookie") {
+ value = rewriteTunnelSetCookiePath(value, tunnel)
+ }
+ rewritten = append(rewritten, value)
+ }
+ headers[name] = rewritten
+ }
+ return headers
+}
+
+func writeTunnelHTTPHeaders(w http.ResponseWriter, headers http.Header) {
+ for name, values := range headers {
+ for _, value := range values {
+ w.Header().Add(name, value)
+ }
+ }
+}
+
+func flushTunnelResponse(w http.ResponseWriter) {
+ if flusher, ok := w.(http.Flusher); ok {
+ flusher.Flush()
+ }
+}
+
+func rewriteTunnelLocation(value string, tunnel *gatewayv1.TunnelSummary) string {
+ if tunnel == nil {
+ return value
+ }
+ target, err := url.Parse(tunnel.GetTargetUrl())
+ if err != nil || target.Host == "" {
+ return value
+ }
+ publicPrefix := "/t/" + tunnel.GetSlug()
+ parsed, err := url.Parse(value)
+ if err != nil {
+ return value
+ }
+ if parsed.IsAbs() {
+ if !strings.EqualFold(parsed.Scheme, target.Scheme) || !strings.EqualFold(parsed.Host, target.Host) {
+ return value
+ }
+ path := stripTunnelTargetBasePath(parsed.EscapedPath(), target.EscapedPath())
+ if path == "" {
+ path = "/"
+ }
+ if parsed.RawQuery != "" {
+ path += "?" + parsed.RawQuery
+ }
+ if parsed.Fragment != "" {
+ path += "#" + parsed.EscapedFragment()
+ }
+ return publicPrefix + path
+ }
+ if strings.HasPrefix(value, "/") {
+ path := stripTunnelTargetBasePath(parsed.EscapedPath(), target.EscapedPath())
+ if path == "" {
+ path = "/"
+ }
+ if parsed.RawQuery != "" {
+ path += "?" + parsed.RawQuery
+ }
+ if parsed.Fragment != "" {
+ path += "#" + parsed.EscapedFragment()
+ }
+ return publicPrefix + path
+ }
+ return value
+}
+
+func rewriteTunnelSetCookiePath(value string, tunnel *gatewayv1.TunnelSummary) string {
+ if tunnel == nil || tunnel.GetSlug() == "" {
+ return value
+ }
+ parts := strings.Split(value, ";")
+ targetBasePath := "/"
+ if target, err := url.Parse(tunnel.GetTargetUrl()); err == nil {
+ targetBasePath = target.EscapedPath()
+ }
+ for index, part := range parts {
+ trimmed := strings.TrimSpace(part)
+ if !strings.HasPrefix(strings.ToLower(trimmed), "path=") {
+ continue
+ }
+ cookiePath := strings.TrimSpace(trimmed[len("path="):])
+ if cookiePath == "" {
+ cookiePath = "/"
+ }
+ rest := stripTunnelTargetBasePath(cookiePath, targetBasePath)
+ if rest == "" {
+ rest = "/"
+ }
+ prefix := ""
+ if leading := len(part) - len(strings.TrimLeft(part, " \t")); leading > 0 {
+ prefix = part[:leading]
+ }
+ parts[index] = fmt.Sprintf("%sPath=/t/%s%s", prefix, tunnel.GetSlug(), rest)
+ }
+ return strings.Join(parts, ";")
+}
+
+func stripTunnelTargetBasePath(pathValue string, basePath string) string {
+ pathValue = normalizeTunnelPath(pathValue)
+ basePath = normalizeTunnelPath(basePath)
+ if basePath == "/" {
+ return pathValue
+ }
+ if pathValue == basePath {
+ return "/"
+ }
+ if strings.HasPrefix(pathValue, strings.TrimRight(basePath, "/")+"/") {
+ return strings.TrimPrefix(pathValue, strings.TrimRight(basePath, "/"))
+ }
+ return pathValue
+}
+
+func normalizeTunnelPath(value string) string {
+ value = strings.TrimSpace(value)
+ if value == "" {
+ return "/"
+ }
+ if !strings.HasPrefix(value, "/") {
+ value = "/" + value
+ }
+ return value
+}
+
+func publicBaseURLFromHTTPRequest(r *http.Request) string {
+ if r == nil {
+ return ""
+ }
+ scheme := forwardedHeaderFirst(r.Header.Get("X-Forwarded-Proto"))
+ if scheme == "" {
+ if r.TLS != nil {
+ scheme = "https"
+ } else {
+ scheme = "http"
+ }
+ }
+ host := forwardedHeaderFirst(r.Header.Get("X-Forwarded-Host"))
+ if host == "" {
+ host = r.Host
+ }
+ if host == "" {
+ return ""
+ }
+ return scheme + "://" + host
+}
+
+func forwardedHeaderFirst(value string) string {
+ first := strings.TrimSpace(strings.Split(value, ",")[0])
+ return strings.TrimSpace(first)
+}
diff --git a/crates/agent-gateway/internal/server/tunnel_rewrite.go b/crates/agent-gateway/internal/server/tunnel_rewrite.go
new file mode 100644
index 000000000..b6b743be5
--- /dev/null
+++ b/crates/agent-gateway/internal/server/tunnel_rewrite.go
@@ -0,0 +1,229 @@
+package server
+
+import (
+ "mime"
+ "net/http"
+ "net/url"
+ "regexp"
+ "strings"
+ "unicode/utf8"
+
+ gatewayv1 "github.com/liveagent/agent-gateway/internal/proto/v1"
+)
+
+const tunnelRewriteBodyMaxBytes = 4 * 1024 * 1024
+
+type tunnelResponseRewriteKind int
+
+const (
+ tunnelResponseRewriteNone tunnelResponseRewriteKind = iota
+ tunnelResponseRewriteHTML
+ tunnelResponseRewriteCSS
+ tunnelResponseRewriteJavaScript
+)
+
+var (
+ tunnelHTMLQuotedAttrURLPattern = regexp.MustCompile(`(?i)(\b(?:href|src|action|poster|data|formaction)\s*=\s*)(["'])([^"']+)(["'])`)
+ tunnelHTMLBareAttrURLPattern = regexp.MustCompile(`(?i)(\b(?:href|src|action|poster|data|formaction)\s*=\s*)([^\s"'<>]+)`)
+ tunnelJSQuotedURLPattern = regexp.MustCompile(`(["'])(/[^"'\\]*)(["'])`)
+ tunnelCSSURLPattern = regexp.MustCompile(`(?i)(url\(\s*)(["']?)([^"')]+)(["']?\s*\))`)
+)
+
+func tunnelResponseRewriteKindFor(
+ method string,
+ status int,
+ headers http.Header,
+) tunnelResponseRewriteKind {
+ if strings.EqualFold(strings.TrimSpace(method), http.MethodHead) {
+ return tunnelResponseRewriteNone
+ }
+ if status < http.StatusOK ||
+ status == http.StatusNoContent ||
+ status == http.StatusNotModified {
+ return tunnelResponseRewriteNone
+ }
+ if strings.TrimSpace(headers.Get("Content-Encoding")) != "" {
+ return tunnelResponseRewriteNone
+ }
+
+ contentType := strings.TrimSpace(headers.Get("Content-Type"))
+ if contentType == "" {
+ return tunnelResponseRewriteNone
+ }
+ mediaType, _, err := mime.ParseMediaType(contentType)
+ if err != nil {
+ mediaType = contentType
+ }
+ mediaType = strings.ToLower(strings.TrimSpace(mediaType))
+
+ switch {
+ case mediaType == "text/html" || mediaType == "application/xhtml+xml":
+ return tunnelResponseRewriteHTML
+ case mediaType == "text/css":
+ return tunnelResponseRewriteCSS
+ case mediaType == "text/javascript",
+ mediaType == "application/javascript",
+ mediaType == "application/x-javascript",
+ mediaType == "text/ecmascript",
+ mediaType == "application/ecmascript",
+ strings.HasSuffix(mediaType, "+javascript"):
+ return tunnelResponseRewriteJavaScript
+ default:
+ return tunnelResponseRewriteNone
+ }
+}
+
+func rewriteTunnelResponseBody(
+ body []byte,
+ tunnel *gatewayv1.TunnelSummary,
+ kind tunnelResponseRewriteKind,
+) ([]byte, bool) {
+ if len(body) == 0 || kind == tunnelResponseRewriteNone || tunnelPublicPathPrefix(tunnel) == "" {
+ return body, false
+ }
+ if !utf8.Valid(body) {
+ return body, false
+ }
+
+ original := string(body)
+ rewritten := original
+ switch kind {
+ case tunnelResponseRewriteHTML:
+ rewritten = rewriteTunnelHTMLBody(rewritten, tunnel)
+ case tunnelResponseRewriteCSS:
+ rewritten = rewriteTunnelCSSBody(rewritten, tunnel)
+ case tunnelResponseRewriteJavaScript:
+ rewritten = rewriteTunnelJavaScriptBody(rewritten, tunnel)
+ }
+ if rewritten == original {
+ return body, false
+ }
+ return []byte(rewritten), true
+}
+
+func rewriteTunnelHTMLBody(input string, tunnel *gatewayv1.TunnelSummary) string {
+ input = tunnelHTMLQuotedAttrURLPattern.ReplaceAllStringFunc(input, func(match string) string {
+ parts := tunnelHTMLQuotedAttrURLPattern.FindStringSubmatch(match)
+ if len(parts) != 5 {
+ return match
+ }
+ rewritten := rewriteTunnelBodyURL(parts[3], tunnel)
+ if rewritten == parts[3] {
+ return match
+ }
+ return parts[1] + parts[2] + rewritten + parts[4]
+ })
+
+ return tunnelHTMLBareAttrURLPattern.ReplaceAllStringFunc(input, func(match string) string {
+ parts := tunnelHTMLBareAttrURLPattern.FindStringSubmatch(match)
+ if len(parts) != 3 {
+ return match
+ }
+ rewritten := rewriteTunnelBodyURL(parts[2], tunnel)
+ if rewritten == parts[2] {
+ return match
+ }
+ return parts[1] + rewritten
+ })
+}
+
+func rewriteTunnelCSSBody(input string, tunnel *gatewayv1.TunnelSummary) string {
+ return tunnelCSSURLPattern.ReplaceAllStringFunc(input, func(match string) string {
+ parts := tunnelCSSURLPattern.FindStringSubmatch(match)
+ if len(parts) != 5 {
+ return match
+ }
+ rewritten := rewriteTunnelBodyURL(strings.TrimSpace(parts[3]), tunnel)
+ if rewritten == strings.TrimSpace(parts[3]) {
+ return match
+ }
+ return parts[1] + parts[2] + rewritten + parts[4]
+ })
+}
+
+func rewriteTunnelJavaScriptBody(input string, tunnel *gatewayv1.TunnelSummary) string {
+ return tunnelJSQuotedURLPattern.ReplaceAllStringFunc(input, func(match string) string {
+ parts := tunnelJSQuotedURLPattern.FindStringSubmatch(match)
+ if len(parts) != 4 || parts[1] != parts[3] {
+ return match
+ }
+ rewritten := rewriteTunnelBodyURL(parts[2], tunnel)
+ if rewritten == parts[2] {
+ return match
+ }
+ return parts[1] + rewritten + parts[1]
+ })
+}
+
+func rewriteTunnelBodyURL(value string, tunnel *gatewayv1.TunnelSummary) string {
+ prefix := tunnelPublicPathPrefix(tunnel)
+ if prefix == "" {
+ return value
+ }
+ trimmed := strings.TrimSpace(value)
+ if trimmed == "" ||
+ strings.HasPrefix(trimmed, "#") ||
+ strings.HasPrefix(trimmed, "//") {
+ return value
+ }
+
+ parsed, err := url.Parse(trimmed)
+ if err != nil {
+ return value
+ }
+ target, targetErr := url.Parse(tunnel.GetTargetUrl())
+ if parsed.IsAbs() {
+ if targetErr != nil || target.Host == "" {
+ return value
+ }
+ if !strings.EqualFold(parsed.Scheme, target.Scheme) ||
+ !strings.EqualFold(parsed.Host, target.Host) {
+ return value
+ }
+ path := stripTunnelTargetBasePath(parsed.EscapedPath(), target.EscapedPath())
+ return appendTunnelURLQueryAndFragment(prefix+pathOrRoot(path), parsed)
+ }
+ if !strings.HasPrefix(trimmed, "/") {
+ return value
+ }
+ if trimmed == prefix || strings.HasPrefix(trimmed, prefix+"/") {
+ return value
+ }
+
+ path := parsed.EscapedPath()
+ if targetErr == nil && target.Host != "" {
+ path = stripTunnelTargetBasePath(path, target.EscapedPath())
+ }
+ return appendTunnelURLQueryAndFragment(prefix+pathOrRoot(path), parsed)
+}
+
+func tunnelPublicPathPrefix(tunnel *gatewayv1.TunnelSummary) string {
+ if tunnel == nil {
+ return ""
+ }
+ slug := strings.TrimSpace(tunnel.GetSlug())
+ if slug == "" {
+ return ""
+ }
+ return "/t/" + slug
+}
+
+func pathOrRoot(path string) string {
+ if strings.TrimSpace(path) == "" {
+ return "/"
+ }
+ return path
+}
+
+func appendTunnelURLQueryAndFragment(path string, parsed *url.URL) string {
+ if parsed == nil {
+ return path
+ }
+ if parsed.RawQuery != "" {
+ path += "?" + parsed.RawQuery
+ }
+ if parsed.Fragment != "" {
+ path += "#" + parsed.EscapedFragment()
+ }
+ return path
+}
diff --git a/crates/agent-gateway/internal/server/tunnel_rewrite_test.go b/crates/agent-gateway/internal/server/tunnel_rewrite_test.go
new file mode 100644
index 000000000..3d9d45136
--- /dev/null
+++ b/crates/agent-gateway/internal/server/tunnel_rewrite_test.go
@@ -0,0 +1,269 @@
+package server
+
+import (
+ "net/http"
+ "strings"
+ "testing"
+
+ gatewayv1 "github.com/liveagent/agent-gateway/internal/proto/v1"
+)
+
+func TestTunnelResponseRewriteKindFor(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ method string
+ status int
+ headers http.Header
+ want tunnelResponseRewriteKind
+ }{
+ {
+ name: "html",
+ method: http.MethodGet,
+ status: http.StatusOK,
+ headers: http.Header{
+ "Content-Type": []string{"text/html; charset=utf-8"},
+ },
+ want: tunnelResponseRewriteHTML,
+ },
+ {
+ name: "javascript",
+ method: http.MethodGet,
+ status: http.StatusOK,
+ headers: http.Header{
+ "Content-Type": []string{"application/javascript"},
+ },
+ want: tunnelResponseRewriteJavaScript,
+ },
+ {
+ name: "css",
+ method: http.MethodGet,
+ status: http.StatusOK,
+ headers: http.Header{
+ "Content-Type": []string{"text/css"},
+ },
+ want: tunnelResponseRewriteCSS,
+ },
+ {
+ name: "compressed response",
+ method: http.MethodGet,
+ status: http.StatusOK,
+ headers: http.Header{
+ "Content-Type": []string{"text/html; charset=utf-8"},
+ "Content-Encoding": []string{"gzip"},
+ },
+ want: tunnelResponseRewriteNone,
+ },
+ {
+ name: "head request",
+ method: http.MethodHead,
+ status: http.StatusOK,
+ headers: http.Header{
+ "Content-Type": []string{"text/html; charset=utf-8"},
+ },
+ want: tunnelResponseRewriteNone,
+ },
+ {
+ name: "json",
+ method: http.MethodGet,
+ status: http.StatusOK,
+ headers: http.Header{
+ "Content-Type": []string{"application/json"},
+ },
+ want: tunnelResponseRewriteNone,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ if got := tunnelResponseRewriteKindFor(tt.method, tt.status, tt.headers); got != tt.want {
+ t.Fatalf("tunnelResponseRewriteKindFor() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestRewriteTunnelHTMLBodyPrefixesRootRelativeAttributes(t *testing.T) {
+ t.Parallel()
+
+ tunnel := tunnelRewriteTestSummary()
+ input := strings.Join([]string{
+ ``,
+ ``,
+ ``,
+ `health`,
+ `cdn`,
+ `external`,
+ `already`,
+ `absolute target`,
+ }, "\n")
+
+ body, changed := rewriteTunnelResponseBody([]byte(input), tunnel, tunnelResponseRewriteHTML)
+ if !changed {
+ t.Fatal("rewriteTunnelResponseBody() did not report a change")
+ }
+ output := string(body)
+
+ assertContains(t, output, `href="/t/test-slug/styles.css"`)
+ assertContains(t, output, `src='/t/test-slug/app.js'`)
+ assertContains(t, output, `action=/t/test-slug/api/messages`)
+ assertContains(t, output, `href="/t/test-slug/api/health?check=1#ready"`)
+ assertContains(t, output, `href="//cdn.example.com/lib.js"`)
+ assertContains(t, output, `href="https://example.com/page"`)
+ assertContains(t, output, `href="/t/test-slug/already"`)
+ assertContains(t, output, `href="/t/test-slug/api/showcase"`)
+ assertNotContains(t, output, `/t/test-slug/t/test-slug`)
+}
+
+func TestRewriteTunnelBodyStripsTargetBasePath(t *testing.T) {
+ t.Parallel()
+
+ tunnel := &gatewayv1.TunnelSummary{
+ Slug: "base-slug",
+ TargetUrl: "http://127.0.0.1:3100/app",
+ }
+ input := strings.Join([]string{
+ ``,
+ ``,
+ `root api`,
+ }, "\n")
+
+ body, changed := rewriteTunnelResponseBody([]byte(input), tunnel, tunnelResponseRewriteHTML)
+ if !changed {
+ t.Fatal("rewriteTunnelResponseBody() did not report a change")
+ }
+ output := string(body)
+
+ assertContains(t, output, `src="/t/base-slug/assets/main.js"`)
+ assertContains(t, output, `href="/t/base-slug/styles.css"`)
+ assertContains(t, output, `href="/t/base-slug/api/health"`)
+ assertNotContains(t, output, `/t/base-slug/app/`)
+
+ jsBody, changed := rewriteTunnelResponseBody(
+ []byte(`fetch("/app/api/health?check=1#ready")`),
+ tunnel,
+ tunnelResponseRewriteJavaScript,
+ )
+ if !changed {
+ t.Fatal("rewriteTunnelResponseBody() did not report a JavaScript change")
+ }
+ assertContains(t, string(jsBody), `fetch("/t/base-slug/api/health?check=1#ready")`)
+
+ cssBody, changed := rewriteTunnelResponseBody(
+ []byte(`body { background: url(/app/images/bg.png); }`),
+ tunnel,
+ tunnelResponseRewriteCSS,
+ )
+ if !changed {
+ t.Fatal("rewriteTunnelResponseBody() did not report a CSS change")
+ }
+ assertContains(t, string(cssBody), `url(/t/base-slug/images/bg.png)`)
+}
+
+func TestRewriteTunnelJavaScriptBodyPrefixesRootRelativeStrings(t *testing.T) {
+ t.Parallel()
+
+ tunnel := tunnelRewriteTestSummary()
+ input := strings.Join([]string{
+ `requestJson('/api/showcase')`,
+ `fetch("/api/health?check=1")`,
+ `const root = "/"`,
+ `const external = "https://example.com/api"`,
+ `const cdn = "//cdn.example.com/app.js"`,
+ `const already = "/t/test-slug/api/health"`,
+ }, "\n")
+
+ body, changed := rewriteTunnelResponseBody([]byte(input), tunnel, tunnelResponseRewriteJavaScript)
+ if !changed {
+ t.Fatal("rewriteTunnelResponseBody() did not report a change")
+ }
+ output := string(body)
+
+ assertContains(t, output, `requestJson('/t/test-slug/api/showcase')`)
+ assertContains(t, output, `fetch("/t/test-slug/api/health?check=1")`)
+ assertContains(t, output, `const root = "/t/test-slug/"`)
+ assertContains(t, output, `const external = "https://example.com/api"`)
+ assertContains(t, output, `const cdn = "//cdn.example.com/app.js"`)
+ assertContains(t, output, `const already = "/t/test-slug/api/health"`)
+ assertNotContains(t, output, `/t/test-slug/t/test-slug`)
+}
+
+func TestRewriteTunnelCSSBodyPrefixesRootRelativeURLs(t *testing.T) {
+ t.Parallel()
+
+ tunnel := tunnelRewriteTestSummary()
+ input := strings.Join([]string{
+ `body { background: url(/images/bg.png); }`,
+ `.icon { mask-image: url('/icons/check.svg'); }`,
+ `.remote { background: url("https://example.com/bg.png"); }`,
+ `.cdn { background: url("//cdn.example.com/bg.png"); }`,
+ `.already { background: url(/t/test-slug/images/bg.png); }`,
+ }, "\n")
+
+ body, changed := rewriteTunnelResponseBody([]byte(input), tunnel, tunnelResponseRewriteCSS)
+ if !changed {
+ t.Fatal("rewriteTunnelResponseBody() did not report a change")
+ }
+ output := string(body)
+
+ assertContains(t, output, `url(/t/test-slug/images/bg.png)`)
+ assertContains(t, output, `url('/t/test-slug/icons/check.svg')`)
+ assertContains(t, output, `url("https://example.com/bg.png")`)
+ assertContains(t, output, `url("//cdn.example.com/bg.png")`)
+ assertContains(t, output, `url(/t/test-slug/images/bg.png)`)
+ assertNotContains(t, output, `/t/test-slug/t/test-slug`)
+}
+
+func TestParseTunnelPublicPathWithoutTrailingSlash(t *testing.T) {
+ t.Parallel()
+
+ slug, ok := parseTunnelPublicPathWithoutTrailingSlash("/t/test-slug")
+ if !ok || slug != "test-slug" {
+ t.Fatalf("parseTunnelPublicPathWithoutTrailingSlash() = %q, %v", slug, ok)
+ }
+
+ for _, path := range []string{"/t/test-slug/", "/t/test-slug/api", "/t/", "/api/test-slug"} {
+ if slug, ok := parseTunnelPublicPathWithoutTrailingSlash(path); ok {
+ t.Fatalf("parseTunnelPublicPathWithoutTrailingSlash(%q) = %q, true; want false", path, slug)
+ }
+ }
+}
+
+func TestRewriteTunnelLocationPreservesQueryAndFragment(t *testing.T) {
+ t.Parallel()
+
+ tunnel := tunnelRewriteTestSummary()
+ if got := rewriteTunnelLocation("/api/health?check=1#ready", tunnel); got != "/t/test-slug/api/health?check=1#ready" {
+ t.Fatalf("rewriteTunnelLocation root path = %q", got)
+ }
+ if got := rewriteTunnelLocation("http://127.0.0.1:3100/api/showcase#item", tunnel); got != "/t/test-slug/api/showcase#item" {
+ t.Fatalf("rewriteTunnelLocation absolute target = %q", got)
+ }
+ if got := rewriteTunnelLocation("https://example.com/api#item", tunnel); got != "https://example.com/api#item" {
+ t.Fatalf("rewriteTunnelLocation external = %q", got)
+ }
+}
+
+func tunnelRewriteTestSummary() *gatewayv1.TunnelSummary {
+ return &gatewayv1.TunnelSummary{
+ Slug: "test-slug",
+ TargetUrl: "http://127.0.0.1:3100",
+ }
+}
+
+func assertContains(t *testing.T, value string, needle string) {
+ t.Helper()
+ if !strings.Contains(value, needle) {
+ t.Fatalf("expected output to contain %q, got:\n%s", needle, value)
+ }
+}
+
+func assertNotContains(t *testing.T, value string, needle string) {
+ t.Helper()
+ if strings.Contains(value, needle) {
+ t.Fatalf("expected output not to contain %q, got:\n%s", needle, value)
+ }
+}
diff --git a/crates/agent-gateway/internal/server/websocket.go b/crates/agent-gateway/internal/server/websocket.go
index 56f87c440..844e291ad 100644
--- a/crates/agent-gateway/internal/server/websocket.go
+++ b/crates/agent-gateway/internal/server/websocket.go
@@ -269,9 +269,19 @@ func (c *websocketConnection) startChatEventForwarder() {
if c.hasActiveChatRequest(event.RequestID) {
continue
}
- if err := c.writeConversationEvent(websocketChatEventPayload(event.Event, event.Seq, event.Workdir)); err != nil {
- c.close()
- return
+ if event.Control != nil {
+ if err := c.writeEnvelope(websocketEnvelope{
+ Type: "conversation.control",
+ Payload: websocketChatControlPayload(event.Control, event.Seq, event.Workdir),
+ }); err != nil {
+ c.close()
+ return
+ }
+ } else if event.Event != nil {
+ if err := c.writeConversationEvent(websocketChatEventPayload(event.Event, event.Seq, event.Workdir)); err != nil {
+ c.close()
+ return
+ }
}
}
}
@@ -431,6 +441,41 @@ func (c *websocketConnection) awaitAgentResponse(
return awaitAgentUnaryResponse(ctx, c.sm, requestID, envelope)
}
+func (c *websocketConnection) sendToAgent(envelope *gatewayv1.GatewayEnvelope) error {
+ timeout := c.cfg.WebSocketWriteTimeout
+ if timeout <= 0 {
+ timeout = 10 * time.Second
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), timeout)
+ defer cancel()
+
+ go func() {
+ select {
+ case <-c.done:
+ cancel()
+ case <-ctx.Done():
+ }
+ }()
+
+ return c.sm.SendToAgentContext(ctx, envelope)
+}
+
+func (c *websocketConnection) chatStartTimeout() time.Duration {
+ timeout := c.cfg.ChatStartTimeout
+ if timeout <= 0 {
+ timeout = 15 * time.Second
+ }
+ return timeout
+}
+
+func (c *websocketConnection) chatRenderStartTimeout() time.Duration {
+ timeout := c.cfg.ChatRenderStartTimeout
+ if timeout <= 0 {
+ timeout = 45 * time.Second
+ }
+ return timeout
+}
+
func (c *websocketConnection) writeResponse(requestID string, payload any) error {
return c.writeEnvelope(websocketEnvelope{
ID: requestID,
@@ -455,6 +500,14 @@ func (c *websocketConnection) writeChatEvent(requestID string, payload any) erro
})
}
+func (c *websocketConnection) writeChatControl(requestID string, payload any) error {
+ return c.writeEnvelope(websocketEnvelope{
+ ID: requestID,
+ Type: "chat.control",
+ Payload: payload,
+ })
+}
+
func (c *websocketConnection) writeHistoryEvent(payload any) error {
return c.writeEnvelope(websocketEnvelope{
Type: "history.event",
diff --git a/crates/agent-gateway/internal/server/websocket_chat_handlers.go b/crates/agent-gateway/internal/server/websocket_chat_handlers.go
index b050b3623..63abbea85 100644
--- a/crates/agent-gateway/internal/server/websocket_chat_handlers.go
+++ b/crates/agent-gateway/internal/server/websocket_chat_handlers.go
@@ -8,6 +8,7 @@ import (
"github.com/liveagent/agent-gateway/internal/handler"
gatewayv1 "github.com/liveagent/agent-gateway/internal/proto/v1"
+ "github.com/liveagent/agent-gateway/internal/session"
)
func (c *websocketConnection) handleChatStart(req websocketRequest) {
@@ -34,12 +35,17 @@ func (c *websocketConnection) handleChatStart(req websocketRequest) {
_ = c.writeError(req.ID, "message is required")
return
}
- if !c.sm.IsOnline() {
+ status := c.sm.Status()
+ if !status.Online {
_ = c.writeError(req.ID, "agent offline")
return
}
+ if !status.ChatRuntimeReady {
+ _ = c.writeError(req.ID, "Desktop chat runtime is not ready. Please retry.")
+ return
+ }
- snapshot, created, err := c.sm.StartChatRunWithClientRequest(
+ snapshot, created, err := c.sm.StartPendingChatRunWithClientRequest(
req.ID,
body.ConversationID,
body.ClientRequestID,
@@ -76,7 +82,7 @@ func (c *websocketConnection) handleChatStart(req websocketRequest) {
defer c.releaseActiveChat(responseID)
if created {
- if err := c.sm.SendToAgent(&gatewayv1.GatewayEnvelope{
+ if err := c.sendToAgent(&gatewayv1.GatewayEnvelope{
RequestId: sourceRequestID,
Timestamp: time.Now().Unix(),
Payload: &gatewayv1.GatewayEnvelope_ChatRequest{
@@ -99,6 +105,11 @@ func (c *websocketConnection) handleChatStart(req websocketRequest) {
}
}
+ startTimer := time.NewTimer(c.chatStartTimeout())
+ defer startTimer.Stop()
+ renderStartTimer := time.NewTimer(c.chatRenderStartTimeout())
+ defer renderStartTimer.Stop()
+
// Do not enforce a hard timeout for streaming chat requests. The GUI path can run
// multiple compaction rounds stably; WebUI should behave the same and only stop
// when the user cancels, the connection closes, or the agent returns done/error.
@@ -109,25 +120,32 @@ func (c *websocketConnection) handleChatStart(req websocketRequest) {
case <-ctx.Done():
_ = c.writeError(responseID, websocketErrorMessage(ctx.Err()))
return
+ case <-startTimer.C:
+ c.sm.FailStartingChatRun(
+ sourceRequestID,
+ "Desktop backend did not accept the remote chat request. Please retry.",
+ )
+ case <-renderStartTimer.C:
+ c.sm.FailUnstartedChatRun(
+ sourceRequestID,
+ "Desktop app accepted the remote chat request but did not start it. Please retry.",
+ )
case <-eventDone:
return
case event, ok := <-eventCh:
if !ok {
return
}
- chatEvent := event.Event
- if chatEvent == nil {
- continue
- }
- if chatEvent.GetConversationId() != "" {
- body.ConversationID = strings.TrimSpace(chatEvent.GetConversationId())
+ if eventConversationID(event) != "" {
+ body.ConversationID = eventConversationID(event)
c.updateActiveChatConversationID(responseID, body.ConversationID)
}
- if err := c.writeChatEvent(responseID, websocketChatEventPayload(chatEvent, event.Seq, event.Workdir)); err != nil {
+ terminal, err := c.writeChatBroadcastEvent(responseID, event)
+ if err != nil {
c.close()
return
}
- if chatEvent.GetType() == gatewayv1.ChatEvent_DONE || chatEvent.GetType() == gatewayv1.ChatEvent_ERROR {
+ if terminal {
return
}
}
@@ -178,6 +196,11 @@ func (c *websocketConnection) handleChatResume(req websocketRequest) {
c.registerActiveChat(responseID, snapshot.RequestID, snapshot.ConversationID, cancel)
defer c.releaseActiveChat(responseID)
+ startTimer := time.NewTimer(c.chatStartTimeout())
+ defer startTimer.Stop()
+ renderStartTimer := time.NewTimer(c.chatRenderStartTimeout())
+ defer renderStartTimer.Stop()
+
if snapshot.Done && snapshot.LatestSeq <= body.AfterSeq {
payload := map[string]any{
"type": "done",
@@ -199,24 +222,31 @@ func (c *websocketConnection) handleChatResume(req websocketRequest) {
case <-ctx.Done():
_ = c.writeError(responseID, websocketErrorMessage(ctx.Err()))
return
+ case <-startTimer.C:
+ c.sm.FailStartingChatRun(
+ snapshot.RequestID,
+ "Desktop backend did not accept the remote chat request. Please retry.",
+ )
+ case <-renderStartTimer.C:
+ c.sm.FailUnstartedChatRun(
+ snapshot.RequestID,
+ "Desktop app accepted the remote chat request but did not start it. Please retry.",
+ )
case <-eventDone:
return
case event, ok := <-eventCh:
if !ok {
return
}
- chatEvent := event.Event
- if chatEvent == nil {
- continue
- }
- if chatEvent.GetConversationId() != "" {
- c.updateActiveChatConversationID(responseID, strings.TrimSpace(chatEvent.GetConversationId()))
+ if conversationID := eventConversationID(event); conversationID != "" {
+ c.updateActiveChatConversationID(responseID, conversationID)
}
- if err := c.writeChatEvent(responseID, websocketChatEventPayload(chatEvent, event.Seq, event.Workdir)); err != nil {
+ terminal, err := c.writeChatBroadcastEvent(responseID, event)
+ if err != nil {
c.close()
return
}
- if chatEvent.GetType() == gatewayv1.ChatEvent_DONE || chatEvent.GetType() == gatewayv1.ChatEvent_ERROR {
+ if terminal {
return
}
}
@@ -279,15 +309,12 @@ func (c *websocketConnection) handleChatAttach(req websocketRequest) {
if !ok {
return
}
- chatEvent := event.Event
- if chatEvent == nil {
- continue
- }
- if err := c.writeChatEvent(req.ID, websocketChatEventPayload(chatEvent, event.Seq, event.Workdir)); err != nil {
+ terminal, err := c.writeChatBroadcastEvent(req.ID, event)
+ if err != nil {
c.close()
return
}
- if chatEvent.GetType() == gatewayv1.ChatEvent_DONE || chatEvent.GetType() == gatewayv1.ChatEvent_ERROR {
+ if terminal {
return
}
}
@@ -328,7 +355,7 @@ func (c *websocketConnection) handleChatCancel(req websocketRequest) {
return
}
- if err := c.sm.SendToAgent(&gatewayv1.GatewayEnvelope{
+ if err := c.sendToAgent(&gatewayv1.GatewayEnvelope{
RequestId: req.ID,
Timestamp: time.Now().Unix(),
Payload: &gatewayv1.GatewayEnvelope_CancelChat{
@@ -393,6 +420,87 @@ func (c *websocketConnection) cancelActiveChatsByConversation(conversationID str
}
}
+func (c *websocketConnection) writeChatBroadcastEvent(
+ requestID string,
+ event *session.ChatBroadcastEvent,
+) (bool, error) {
+ if event == nil {
+ return false, nil
+ }
+ if event.Control != nil {
+ payload := websocketChatControlPayload(event.Control, event.Seq, event.Workdir)
+ if err := c.writeChatControl(requestID, payload); err != nil {
+ return false, err
+ }
+ return isTerminalChatControlPayload(event.Control), nil
+ }
+ if event.Event == nil {
+ return false, nil
+ }
+ if err := c.writeChatEvent(
+ requestID,
+ websocketChatEventPayload(event.Event, event.Seq, event.Workdir),
+ ); err != nil {
+ return false, err
+ }
+ return event.Event.GetType() == gatewayv1.ChatEvent_DONE ||
+ event.Event.GetType() == gatewayv1.ChatEvent_ERROR, nil
+}
+
+func eventConversationID(event *session.ChatBroadcastEvent) string {
+ if event == nil {
+ return ""
+ }
+ if event.Control != nil {
+ return strings.TrimSpace(event.Control.GetConversationId())
+ }
+ if event.Event != nil {
+ return strings.TrimSpace(event.Event.GetConversationId())
+ }
+ return ""
+}
+
+func isTerminalChatControlPayload(control *gatewayv1.ChatControlEvent) bool {
+ switch strings.TrimSpace(control.GetState()) {
+ case "completed", "failed", "cancelled":
+ return true
+ default:
+ return false
+ }
+}
+
+func websocketChatControlPayload(
+ control *gatewayv1.ChatControlEvent,
+ seq int64,
+ workdirInput ...string,
+) map[string]any {
+ payload := map[string]any{
+ "type": strings.TrimSpace(control.GetType()),
+ "request_id": strings.TrimSpace(control.GetRequestId()),
+ "client_request_id": strings.TrimSpace(control.GetClientRequestId()),
+ "conversation_id": strings.TrimSpace(control.GetConversationId()),
+ "run_epoch": control.GetRunEpoch(),
+ "state": strings.TrimSpace(control.GetState()),
+ }
+ if seq > 0 {
+ payload["seq"] = seq
+ } else if control.GetSeq() > 0 {
+ payload["seq"] = control.GetSeq()
+ }
+ if errorCode := strings.TrimSpace(control.GetErrorCode()); errorCode != "" {
+ payload["error_code"] = errorCode
+ }
+ if message := strings.TrimSpace(control.GetMessage()); message != "" {
+ payload["message"] = message
+ }
+ if len(workdirInput) > 0 {
+ if workdir := strings.TrimSpace(workdirInput[0]); workdir != "" {
+ payload["workdir"] = workdir
+ }
+ }
+ return payload
+}
+
func websocketChatEventPayload(event *gatewayv1.ChatEvent, seq int64, workdirInput ...string) map[string]any {
payload := map[string]any{
"type": websocketChatEventType(event.GetType()),
diff --git a/crates/agent-gateway/internal/server/websocket_roundtrip.go b/crates/agent-gateway/internal/server/websocket_roundtrip.go
index 3d7836aa6..c2723c57f 100644
--- a/crates/agent-gateway/internal/server/websocket_roundtrip.go
+++ b/crates/agent-gateway/internal/server/websocket_roundtrip.go
@@ -53,7 +53,7 @@ func awaitAgentUnaryResponse(
}
defer cleanup()
- if err := sm.SendToAgent(envelope); err != nil {
+ if err := sm.SendToAgentContext(ctx, envelope); err != nil {
return nil, err
}
diff --git a/crates/agent-gateway/internal/server/websocket_routes.go b/crates/agent-gateway/internal/server/websocket_routes.go
index 823244479..60ad324dd 100644
--- a/crates/agent-gateway/internal/server/websocket_routes.go
+++ b/crates/agent-gateway/internal/server/websocket_routes.go
@@ -51,6 +51,10 @@ var websocketRequestHandlers = map[string]websocketRequestHandler{
"terminal.close": (*websocketConnection).handleTerminalRequest,
"terminal.close_project": (*websocketConnection).handleTerminalRequest,
"terminal.detach": (*websocketConnection).handleTerminalDetach,
+ "tunnel.list": (*websocketConnection).handleTunnelList,
+ "tunnel.create": (*websocketConnection).handleTunnelCreate,
+ "tunnel.update": (*websocketConnection).handleTunnelUpdate,
+ "tunnel.close": (*websocketConnection).handleTunnelClose,
"git.status": (*websocketConnection).handleGitRequest,
"git.branches": (*websocketConnection).handleGitRequest,
"git.init": (*websocketConnection).handleGitRequest,
diff --git a/crates/agent-gateway/internal/server/websocket_routes_test.go b/crates/agent-gateway/internal/server/websocket_routes_test.go
index e7e8a2d8e..3b275b626 100644
--- a/crates/agent-gateway/internal/server/websocket_routes_test.go
+++ b/crates/agent-gateway/internal/server/websocket_routes_test.go
@@ -1,6 +1,9 @@
package server
-import "testing"
+import (
+ "encoding/json"
+ "testing"
+)
func TestWebsocketRequestHandlersCoverKnownProtocolTypes(t *testing.T) {
t.Parallel()
@@ -52,6 +55,10 @@ func TestWebsocketRequestHandlersCoverKnownProtocolTypes(t *testing.T) {
"terminal.close",
"terminal.close_project",
"terminal.detach",
+ "tunnel.list",
+ "tunnel.create",
+ "tunnel.update",
+ "tunnel.close",
"git.status",
"git.branches",
"git.init",
@@ -103,3 +110,50 @@ func TestDecodeWebSocketPayloadRejectsUnknownFields(t *testing.T) {
t.Fatal("expected unknown payload field to be rejected")
}
}
+
+func TestTunnelTTLFromPayloadDefaultsOnlyWhenOmitted(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ raw json.RawMessage
+ camelValue uint32
+ snakeValue uint32
+ want uint32
+ }{
+ {
+ name: "omitted",
+ raw: json.RawMessage(`{"targetUrl":"http://localhost:3000"}`),
+ camelValue: 0,
+ snakeValue: 0,
+ want: websocketDefaultTunnelTTLSeconds,
+ },
+ {
+ name: "camel explicit infinite",
+ raw: json.RawMessage(`{"targetUrl":"http://localhost:3000","ttlSeconds":0}`),
+ camelValue: 0,
+ snakeValue: 0,
+ want: 0,
+ },
+ {
+ name: "snake explicit infinite",
+ raw: json.RawMessage(`{"target_url":"http://localhost:3000","ttl_seconds":0}`),
+ camelValue: 0,
+ snakeValue: 0,
+ want: 0,
+ },
+ {
+ name: "camel finite",
+ raw: json.RawMessage(`{"targetUrl":"http://localhost:3000","ttlSeconds":900}`),
+ camelValue: 900,
+ snakeValue: 0,
+ want: 900,
+ },
+ }
+
+ for _, tt := range tests {
+ if got := tunnelTTLFromPayload(tt.raw, tt.camelValue, tt.snakeValue); got != tt.want {
+ t.Fatalf("%s: tunnelTTLFromPayload = %d, want %d", tt.name, got, tt.want)
+ }
+ }
+}
diff --git a/crates/agent-gateway/internal/server/websocket_tunnel_handlers.go b/crates/agent-gateway/internal/server/websocket_tunnel_handlers.go
new file mode 100644
index 000000000..f01e3867e
--- /dev/null
+++ b/crates/agent-gateway/internal/server/websocket_tunnel_handlers.go
@@ -0,0 +1,322 @@
+package server
+
+import (
+ "encoding/json"
+ "strings"
+ "time"
+
+ gatewayv1 "github.com/liveagent/agent-gateway/internal/proto/v1"
+)
+
+const websocketDefaultTunnelTTLSeconds = 3600
+
+type websocketTunnelCreatePayload struct {
+ TargetURL string `json:"targetUrl"`
+ TargetUrl string `json:"target_url"`
+ Name string `json:"name"`
+ TTLSeconds uint32 `json:"ttlSeconds"`
+ TtlSeconds uint32 `json:"ttl_seconds"`
+ ProjectPathKey string `json:"projectPathKey"`
+ ProjectPathKeySnake string `json:"project_path_key"`
+}
+
+func tunnelTTLFromPayload(raw json.RawMessage, camelValue uint32, snakeValue uint32) uint32 {
+ var fields map[string]json.RawMessage
+ if err := json.Unmarshal(raw, &fields); err != nil {
+ return websocketDefaultTunnelTTLSeconds
+ }
+ if _, ok := fields["ttlSeconds"]; ok {
+ return camelValue
+ }
+ if _, ok := fields["ttl_seconds"]; ok {
+ return snakeValue
+ }
+ return websocketDefaultTunnelTTLSeconds
+}
+
+type websocketTunnelUpdatePayload struct {
+ ID string `json:"id"`
+ TunnelID string `json:"tunnelId"`
+ TunnelId string `json:"tunnel_id"`
+ Slug string `json:"slug"`
+ TargetURL string `json:"targetUrl"`
+ TargetUrl string `json:"target_url"`
+ Name string `json:"name"`
+ TTLSeconds uint32 `json:"ttlSeconds"`
+ TtlSeconds uint32 `json:"ttl_seconds"`
+ ProjectPathKey string `json:"projectPathKey"`
+ ProjectPathKeySnake string `json:"project_path_key"`
+}
+
+type websocketTunnelClosePayload struct {
+ ID string `json:"id"`
+ TunnelID string `json:"tunnelId"`
+ TunnelId string `json:"tunnel_id"`
+ Slug string `json:"slug"`
+}
+
+func (c *websocketConnection) handleTunnelList(req websocketRequest) {
+ _ = c.writeResponse(req.ID, map[string]any{
+ "tunnels": websocketTunnelSummariesPayload(c.sm.ListTunnels(), c.publicBaseURL()),
+ })
+}
+
+func (c *websocketConnection) handleTunnelCreate(req websocketRequest) {
+ if !c.sm.WebTunnelsEnabled() {
+ _ = c.writeError(req.ID, "web tunnels are disabled in desktop Remote settings")
+ return
+ }
+
+ var body websocketTunnelCreatePayload
+ if err := decodeWebSocketPayload(req.Payload, &body); err != nil {
+ _ = c.writeError(req.ID, "invalid tunnel.create payload")
+ return
+ }
+ targetURL := strings.TrimSpace(body.TargetURL)
+ if targetURL == "" {
+ targetURL = strings.TrimSpace(body.TargetUrl)
+ }
+ ttlSeconds := tunnelTTLFromPayload(req.Payload, body.TTLSeconds, body.TtlSeconds)
+ projectPathKey := strings.TrimSpace(body.ProjectPathKey)
+ if projectPathKey == "" {
+ projectPathKey = strings.TrimSpace(body.ProjectPathKeySnake)
+ }
+ prepared, err := c.sm.PrepareTunnelCreate(&gatewayv1.TunnelControlRequest{
+ Action: "create",
+ TargetUrl: targetURL,
+ Name: strings.TrimSpace(body.Name),
+ TtlSeconds: ttlSeconds,
+ ProjectPathKey: projectPathKey,
+ }, c.publicBaseURL())
+ if err != nil {
+ _ = c.writeError(req.ID, websocketErrorMessage(err))
+ return
+ }
+
+ response, err := c.awaitAgentResponse(req.ID, &gatewayv1.GatewayEnvelope{
+ RequestId: req.ID,
+ Timestamp: time.Now().Unix(),
+ Payload: &gatewayv1.GatewayEnvelope_TunnelControl{
+ TunnelControl: prepared,
+ },
+ })
+ if err != nil {
+ _ = c.writeError(req.ID, websocketErrorMessage(err))
+ return
+ }
+ if errResp := response.GetError(); errResp != nil {
+ _ = c.writeError(req.ID, errResp.GetMessage())
+ return
+ }
+ controlResp := response.GetTunnelControlResp()
+ if controlResp == nil {
+ _ = c.writeError(req.ID, "unexpected agent response")
+ return
+ }
+ if controlResp.GetErrorMessage() != "" {
+ _ = c.writeError(req.ID, controlResp.GetErrorMessage())
+ return
+ }
+ targetOverride := ""
+ if tunnel := controlResp.GetTunnel(); tunnel != nil {
+ targetOverride = tunnel.GetTargetUrl()
+ }
+ tunnel, err := c.sm.StorePreparedTunnel(prepared, targetOverride)
+ if err != nil {
+ _ = c.writeError(req.ID, websocketErrorMessage(err))
+ return
+ }
+ _ = c.writeResponse(req.ID, map[string]any{
+ "tunnel": websocketTunnelSummaryPayload(tunnel, c.publicBaseURL()),
+ "tunnels": websocketTunnelSummariesPayload(c.sm.ListTunnels(), c.publicBaseURL()),
+ })
+}
+
+func (c *websocketConnection) handleTunnelUpdate(req websocketRequest) {
+ if !c.sm.WebTunnelsEnabled() {
+ _ = c.writeError(req.ID, "web tunnels are disabled in desktop Remote settings")
+ return
+ }
+
+ var body websocketTunnelUpdatePayload
+ if err := decodeWebSocketPayload(req.Payload, &body); err != nil {
+ _ = c.writeError(req.ID, "invalid tunnel.update payload")
+ return
+ }
+ identifier := strings.TrimSpace(body.ID)
+ if identifier == "" {
+ identifier = strings.TrimSpace(body.TunnelID)
+ }
+ if identifier == "" {
+ identifier = strings.TrimSpace(body.TunnelId)
+ }
+ if identifier == "" {
+ identifier = strings.TrimSpace(body.Slug)
+ }
+ if identifier == "" {
+ _ = c.writeError(req.ID, "tunnel id is required")
+ return
+ }
+ targetURL := strings.TrimSpace(body.TargetURL)
+ if targetURL == "" {
+ targetURL = strings.TrimSpace(body.TargetUrl)
+ }
+ ttlSeconds := tunnelTTLFromPayload(req.Payload, body.TTLSeconds, body.TtlSeconds)
+ projectPathKey := strings.TrimSpace(body.ProjectPathKey)
+ if projectPathKey == "" {
+ projectPathKey = strings.TrimSpace(body.ProjectPathKeySnake)
+ }
+ prepared, err := c.sm.PrepareTunnelUpdate(&gatewayv1.TunnelControlRequest{
+ Action: "update",
+ TunnelId: identifier,
+ TargetUrl: targetURL,
+ Name: strings.TrimSpace(body.Name),
+ TtlSeconds: ttlSeconds,
+ ProjectPathKey: projectPathKey,
+ })
+ if err != nil {
+ _ = c.writeError(req.ID, websocketErrorMessage(err))
+ return
+ }
+
+ response, err := c.awaitAgentResponse(req.ID, &gatewayv1.GatewayEnvelope{
+ RequestId: req.ID,
+ Timestamp: time.Now().Unix(),
+ Payload: &gatewayv1.GatewayEnvelope_TunnelControl{
+ TunnelControl: prepared,
+ },
+ })
+ if err != nil {
+ _ = c.writeError(req.ID, websocketErrorMessage(err))
+ return
+ }
+ if errResp := response.GetError(); errResp != nil {
+ _ = c.writeError(req.ID, errResp.GetMessage())
+ return
+ }
+ controlResp := response.GetTunnelControlResp()
+ if controlResp == nil {
+ _ = c.writeError(req.ID, "unexpected agent response")
+ return
+ }
+ if controlResp.GetErrorMessage() != "" {
+ _ = c.writeError(req.ID, controlResp.GetErrorMessage())
+ return
+ }
+ tunnel, err := c.sm.ApplyTunnelUpdate(controlResp.GetTunnel())
+ if err != nil {
+ _ = c.writeError(req.ID, websocketErrorMessage(err))
+ return
+ }
+ _ = c.writeResponse(req.ID, map[string]any{
+ "tunnel": websocketTunnelSummaryPayload(tunnel, c.publicBaseURL()),
+ "tunnels": websocketTunnelSummariesPayload(c.sm.ListTunnels(), c.publicBaseURL()),
+ })
+}
+
+func (c *websocketConnection) handleTunnelClose(req websocketRequest) {
+ if !c.sm.WebTunnelsEnabled() {
+ _ = c.writeError(req.ID, "web tunnels are disabled in desktop Remote settings")
+ return
+ }
+
+ var body websocketTunnelClosePayload
+ if err := decodeWebSocketPayload(req.Payload, &body); err != nil {
+ _ = c.writeError(req.ID, "invalid tunnel.close payload")
+ return
+ }
+ identifier := strings.TrimSpace(body.ID)
+ if identifier == "" {
+ identifier = strings.TrimSpace(body.TunnelID)
+ }
+ if identifier == "" {
+ identifier = strings.TrimSpace(body.TunnelId)
+ }
+ if identifier == "" {
+ identifier = strings.TrimSpace(body.Slug)
+ }
+ if identifier == "" {
+ _ = c.writeError(req.ID, "tunnel id is required")
+ return
+ }
+
+ tunnel, err := c.sm.CloseTunnel(identifier)
+ if err != nil {
+ _ = c.writeError(req.ID, websocketErrorMessage(err))
+ return
+ }
+
+ _ = c.sendToAgent(&gatewayv1.GatewayEnvelope{
+ RequestId: "tunnel-close-" + tunnel.GetId(),
+ Timestamp: time.Now().Unix(),
+ Payload: &gatewayv1.GatewayEnvelope_TunnelControl{
+ TunnelControl: &gatewayv1.TunnelControlRequest{
+ Action: "close",
+ TunnelId: tunnel.GetId(),
+ Slug: tunnel.GetSlug(),
+ },
+ },
+ })
+
+ _ = c.writeResponse(req.ID, map[string]any{
+ "tunnel": websocketTunnelSummaryPayload(tunnel, c.publicBaseURL()),
+ "tunnels": websocketTunnelSummariesPayload(c.sm.ListTunnels(), c.publicBaseURL()),
+ })
+}
+
+func (c *websocketConnection) publicBaseURL() string {
+ return publicBaseURLFromHTTPRequest(c.conn.Request())
+}
+
+func websocketTunnelSummariesPayload(
+ summaries []*gatewayv1.TunnelSummary,
+ publicBaseURL string,
+) []map[string]any {
+ payload := make([]map[string]any, 0, len(summaries))
+ for _, summary := range summaries {
+ if item := websocketTunnelSummaryPayload(summary, publicBaseURL); item != nil {
+ payload = append(payload, item)
+ }
+ }
+ return payload
+}
+
+func websocketTunnelSummaryPayload(
+ summary *gatewayv1.TunnelSummary,
+ publicBaseURL string,
+) map[string]any {
+ if summary == nil {
+ return nil
+ }
+ publicURL := strings.TrimSpace(summary.GetPublicUrl())
+ if publicURL == "" {
+ publicURL = buildPublicTunnelURL(publicBaseURL, summary.GetSlug())
+ }
+ return map[string]any{
+ "id": strings.TrimSpace(summary.GetId()),
+ "slug": strings.TrimSpace(summary.GetSlug()),
+ "name": strings.TrimSpace(summary.GetName()),
+ "targetUrl": strings.TrimSpace(summary.GetTargetUrl()),
+ "target_url": strings.TrimSpace(summary.GetTargetUrl()),
+ "publicUrl": publicURL,
+ "public_url": publicURL,
+ "createdAt": summary.GetCreatedAt(),
+ "created_at": summary.GetCreatedAt(),
+ "expiresAt": summary.GetExpiresAt(),
+ "expires_at": summary.GetExpiresAt(),
+ "activeConnections": summary.GetActiveConnections(),
+ "active_connections": summary.GetActiveConnections(),
+ "status": strings.TrimSpace(summary.GetStatus()),
+ "projectPathKey": strings.TrimSpace(summary.GetProjectPathKey()),
+ "project_path_key": strings.TrimSpace(summary.GetProjectPathKey()),
+ }
+}
+
+func buildPublicTunnelURL(publicBaseURL string, slug string) string {
+ publicBaseURL = strings.TrimRight(strings.TrimSpace(publicBaseURL), "/")
+ slug = strings.TrimSpace(slug)
+ if publicBaseURL == "" || slug == "" {
+ return ""
+ }
+ return publicBaseURL + "/t/" + slug + "/"
+}
diff --git a/crates/agent-gateway/internal/session/agent_session.go b/crates/agent-gateway/internal/session/agent_session.go
index 790e6ae90..5bee6705c 100644
--- a/crates/agent-gateway/internal/session/agent_session.go
+++ b/crates/agent-gateway/internal/session/agent_session.go
@@ -1,6 +1,7 @@
package session
import (
+ "context"
"time"
gatewayv1 "github.com/liveagent/agent-gateway/internal/proto/v1"
@@ -13,13 +14,37 @@ func NewAgentSession(auth AuthSnapshot) *AgentSession {
SessionID: auth.SessionID,
ConnectedAt: time.Now(),
LastPing: time.Now(),
- toAgent: make(chan *gatewayv1.GatewayEnvelope, 64),
+ toAgent: make(chan *OutboundEnvelope, 64),
done: make(chan struct{}),
streams: make(map[string]*agentStream),
}
}
-func (s *AgentSession) Outbound() <-chan *gatewayv1.GatewayEnvelope {
+type OutboundEnvelope struct {
+ *gatewayv1.GatewayEnvelope
+
+ ctx context.Context
+ result chan error
+}
+
+func (e *OutboundEnvelope) Context() context.Context {
+ if e == nil || e.ctx == nil {
+ return context.Background()
+ }
+ return e.ctx
+}
+
+func (e *OutboundEnvelope) Ack(err error) {
+ if e == nil || e.result == nil {
+ return
+ }
+ select {
+ case e.result <- err:
+ default:
+ }
+}
+
+func (s *AgentSession) Outbound() <-chan *OutboundEnvelope {
return s.toAgent
}
@@ -41,6 +66,34 @@ func (s *AgentSession) Close() {
}
func (s *AgentSession) SendToAgent(env *gatewayv1.GatewayEnvelope) error {
+ return s.enqueueToAgent(context.Background(), env, nil)
+}
+
+func (s *AgentSession) SendToAgentContext(ctx context.Context, env *gatewayv1.GatewayEnvelope) error {
+ if ctx == nil {
+ ctx = context.Background()
+ }
+ result := make(chan error, 1)
+ if err := s.enqueueToAgent(ctx, env, result); err != nil {
+ return err
+ }
+
+ select {
+ case err := <-result:
+ return err
+ case <-ctx.Done():
+ s.Close()
+ return ctx.Err()
+ case <-s.done:
+ return ErrAgentOffline
+ }
+}
+
+func (s *AgentSession) enqueueToAgent(
+ ctx context.Context,
+ env *gatewayv1.GatewayEnvelope,
+ result chan error,
+) error {
s.streamsMu.Lock()
closed := s.closed
s.streamsMu.Unlock()
@@ -49,9 +102,15 @@ func (s *AgentSession) SendToAgent(env *gatewayv1.GatewayEnvelope) error {
}
select {
+ case <-ctx.Done():
+ return ctx.Err()
case <-s.done:
return ErrAgentOffline
- case s.toAgent <- env:
+ case s.toAgent <- &OutboundEnvelope{
+ GatewayEnvelope: env,
+ ctx: ctx,
+ result: result,
+ }:
return nil
}
}
@@ -73,7 +132,7 @@ func (s *AgentSession) TrySendToAgent(env *gatewayv1.GatewayEnvelope) (bool, err
select {
case <-s.done:
return false, ErrAgentOffline
- case s.toAgent <- env:
+ case s.toAgent <- &OutboundEnvelope{GatewayEnvelope: env}:
return true, nil
default:
return false, nil
diff --git a/crates/agent-gateway/internal/session/manager.go b/crates/agent-gateway/internal/session/manager.go
index c46bf6b1a..5a7479a9c 100644
--- a/crates/agent-gateway/internal/session/manager.go
+++ b/crates/agent-gateway/internal/session/manager.go
@@ -10,13 +10,22 @@ import (
var ErrAgentOffline = errors.New("agent offline")
var ErrChatRunNotFound = errors.New("chat run not found")
+var ErrTunnelNotFound = errors.New("tunnel not found")
+var ErrTunnelExpired = errors.New("tunnel expired")
+var ErrTunnelOverLimit = errors.New("tunnel connection limit exceeded")
+var ErrTunnelLimitExceeded = errors.New("tunnel limit exceeded")
const (
maxBufferedChatRunEvents = 50000
chatRunDoneRetention = time.Hour
+ chatRunStartRetention = 5 * time.Minute
chatRunStaleRetention = 12 * time.Hour
agentDisconnectedChatRunMessage = "Desktop agent disconnected. Please retry."
+
+ chatRuntimeReadyTTL = 15 * time.Second
+ agentSessionHeartbeatTTL = 90 * time.Second
+ defaultRuntimeReadyState = "ready"
)
type AuthSnapshot struct {
@@ -29,6 +38,7 @@ type Manager struct {
registry *sessionRegistry
syncHub *syncHub
chatStore *chatRunStore
+ tunnels *tunnelStore
}
type AgentSession struct {
@@ -38,7 +48,7 @@ type AgentSession struct {
ConnectedAt time.Time
LastPing time.Time
- toAgent chan *gatewayv1.GatewayEnvelope
+ toAgent chan *OutboundEnvelope
done chan struct{}
closeOnce sync.Once
@@ -57,6 +67,7 @@ type agentStream struct {
type ChatBroadcastEvent struct {
RequestID string
Event *gatewayv1.ChatEvent
+ Control *gatewayv1.ChatControlEvent
Seq int64
Workdir string
}
@@ -68,6 +79,9 @@ type ChatRunSnapshot struct {
Workdir string
FirstSeq int64
LatestSeq int64
+ RunEpoch int64
+ State string
+ ErrorCode string
Done bool
}
@@ -77,14 +91,30 @@ type ActiveChatRunSummary struct {
UpdatedAt int64
}
+const (
+ ChatRunStateQueued = "queued"
+ ChatRunStateDelivered = "delivered"
+ ChatRunStateClaimed = "claimed"
+ ChatRunStateStarting = "starting"
+ ChatRunStateRunning = "running"
+ ChatRunStateCompleted = "completed"
+ ChatRunStateFailed = "failed"
+ ChatRunStateCancelled = "cancelled"
+)
+
type chatRun struct {
requestID string
conversationID string
clientRequestID string
workdir string
sessionEpoch uint64
+ runEpoch int64
events []*ChatBroadcastEvent
nextSeq int64
+ state string
+ errorCode string
+ accepted bool
+ started bool
done bool
updatedAt time.Time
expiresAt time.Time
@@ -104,12 +134,19 @@ type chatRunSubscriber struct {
}
type Status struct {
- Online bool `json:"online"`
- AgentID string `json:"agent_id"`
- AgentVersion string `json:"agent_version"`
- SessionID string `json:"session_id,omitempty"`
- ConnectedSince int64 `json:"connected_since"`
- LastHeartbeat int64 `json:"last_heartbeat"`
+ Online bool `json:"online"`
+ AgentReady bool `json:"agent_ready"`
+ ChatRuntimeReady bool `json:"chat_runtime_ready"`
+ AgentID string `json:"agent_id"`
+ AgentVersion string `json:"agent_version"`
+ SessionID string `json:"session_id,omitempty"`
+ ConnectedSince int64 `json:"connected_since"`
+ LastHeartbeat int64 `json:"last_heartbeat"`
+ RuntimeState string `json:"runtime_state,omitempty"`
+ RuntimeLastHeartbeat int64 `json:"runtime_last_heartbeat,omitempty"`
+ RuntimeWorkerID string `json:"runtime_worker_id,omitempty"`
+ RuntimeVisible bool `json:"runtime_visible,omitempty"`
+ RuntimeActiveRunCount uint32 `json:"runtime_active_run_count,omitempty"`
}
func NewManager() *Manager {
@@ -117,5 +154,6 @@ func NewManager() *Manager {
registry: newSessionRegistry(),
syncHub: newSyncHub(),
chatStore: newChatRunStore(),
+ tunnels: newTunnelStore(),
}
}
diff --git a/crates/agent-gateway/internal/session/manager_chat_runs.go b/crates/agent-gateway/internal/session/manager_chat_runs.go
index ef8eb3e35..1cf07a609 100644
--- a/crates/agent-gateway/internal/session/manager_chat_runs.go
+++ b/crates/agent-gateway/internal/session/manager_chat_runs.go
@@ -42,6 +42,25 @@ func (m *Manager) StartChatRunWithClientRequest(
conversationID string,
clientRequestID string,
workdirInput ...string,
+) (ChatRunSnapshot, bool, error) {
+ return m.startChatRunWithClientRequest(requestID, conversationID, clientRequestID, true, workdirInput...)
+}
+
+func (m *Manager) StartPendingChatRunWithClientRequest(
+ requestID string,
+ conversationID string,
+ clientRequestID string,
+ workdirInput ...string,
+) (ChatRunSnapshot, bool, error) {
+ return m.startChatRunWithClientRequest(requestID, conversationID, clientRequestID, false, workdirInput...)
+}
+
+func (m *Manager) startChatRunWithClientRequest(
+ requestID string,
+ conversationID string,
+ clientRequestID string,
+ started bool,
+ workdirInput ...string,
) (ChatRunSnapshot, bool, error) {
requestID = strings.TrimSpace(requestID)
if requestID == "" {
@@ -68,6 +87,9 @@ func (m *Manager) StartChatRunWithClientRequest(
if workdir != "" && existing.workdir == "" {
existing.workdir = workdir
}
+ if started && existing.state != ChatRunStateRunning {
+ existing.applyState(ChatRunStateRunning)
+ }
return existing.snapshot(), false, nil
}
m.releaseCompletedChatRunLocked(existingRequestID, existing)
@@ -80,15 +102,23 @@ func (m *Manager) StartChatRunWithClientRequest(
m.removeChatRunLocked(requestID, existing)
}
+ m.chatStore.nextChatRunEpoch += 1
+ initialState := ChatRunStateQueued
+ if started {
+ initialState = ChatRunStateRunning
+ }
run := &chatRun{
requestID: requestID,
conversationID: conversationID,
clientRequestID: clientRequestID,
workdir: workdir,
sessionEpoch: sessionEpoch,
+ runEpoch: m.chatStore.nextChatRunEpoch,
+ state: initialState,
updatedAt: now,
subscribers: make(map[int]*chatRunSubscriber),
}
+ run.applyState(initialState)
m.chatStore.chatRuns[requestID] = run
if conversationID != "" {
m.chatStore.chatRunByConversation[conversationID] = requestID
@@ -149,7 +179,7 @@ func (m *Manager) ActiveChatRunSummaries() []ActiveChatRunSummary {
seen := make(map[string]int, len(m.chatStore.chatRuns)+len(m.chatStore.historyActiveRuns))
summaries := make([]ActiveChatRunSummary, 0, len(m.chatStore.chatRuns)+len(m.chatStore.historyActiveRuns))
for _, run := range m.chatStore.chatRuns {
- if run == nil || run.done {
+ if run == nil || run.done || normalizeChatRunState(run.state) != ChatRunStateRunning {
continue
}
conversationID := strings.TrimSpace(run.conversationID)
@@ -232,7 +262,7 @@ func (m *Manager) failOpenChatRunsForSessionEpoch(sessionEpoch uint64, message s
now := time.Now()
type broadcastTarget struct {
- event *ChatBroadcastEvent
+ events []*ChatBroadcastEvent
subscribers []*chatRunSubscriber
}
targets := make([]broadcastTarget, 0)
@@ -247,7 +277,8 @@ func (m *Manager) failOpenChatRunsForSessionEpoch(sessionEpoch uint64, message s
run.nextSeq += 1
run.updatedAt = now
- run.done = true
+ run.applyState(ChatRunStateFailed)
+ run.errorCode = "agent_disconnected"
run.expiresAt = now.Add(chatRunDoneRetention)
chatEvent := &gatewayv1.ChatEvent{
@@ -272,7 +303,7 @@ func (m *Manager) failOpenChatRunsForSessionEpoch(sessionEpoch uint64, message s
subscribers = append(subscribers, subscriber)
}
targets = append(targets, broadcastTarget{
- event: broadcast,
+ events: []*ChatBroadcastEvent{broadcast},
subscribers: subscribers,
})
}
@@ -283,18 +314,143 @@ func (m *Manager) failOpenChatRunsForSessionEpoch(sessionEpoch uint64, message s
for _, target := range targets {
for _, subscriber := range target.subscribers {
- select {
- case <-subscriber.done:
- case subscriber.ch <- cloneChatBroadcastEvent(target.event):
+ for _, event := range target.events {
+ select {
+ case <-subscriber.done:
+ case subscriber.ch <- cloneChatBroadcastEvent(event):
+ }
}
}
for _, ch := range globalSubscribers {
- select {
- case ch <- cloneChatBroadcastEvent(target.event):
- default:
+ for _, event := range target.events {
+ select {
+ case ch <- cloneChatBroadcastEvent(event):
+ default:
+ }
+ }
+ }
+ }
+}
+
+func (m *Manager) FailStartingChatRun(requestID string, message string) bool {
+ failed, sessionEpoch := m.failChatRunIf(
+ requestID,
+ message,
+ "Desktop backend did not accept the remote chat request. Please retry.",
+ func(run *chatRun) bool {
+ if run == nil || run.done {
+ return false
}
+ state := normalizeChatRunState(run.state)
+ return state == ChatRunStateQueued
+ },
+ )
+ if failed {
+ m.ClearSessionForEpoch(sessionEpoch)
+ }
+ return failed
+}
+
+func (m *Manager) FailUnstartedChatRun(requestID string, message string) bool {
+ failed, _ := m.failChatRunIf(
+ requestID,
+ message,
+ "Desktop app accepted the remote chat request but did not start it. Please retry.",
+ func(run *chatRun) bool {
+ if run == nil || run.done {
+ return false
+ }
+ state := normalizeChatRunState(run.state)
+ return state != ChatRunStateQueued &&
+ state != ChatRunStateRunning &&
+ !isTerminalChatRunState(state)
+ },
+ )
+ return failed
+}
+
+func (m *Manager) failChatRunIf(
+ requestID string,
+ message string,
+ defaultMessage string,
+ shouldFail func(*chatRun) bool,
+) (bool, uint64) {
+ requestID = strings.TrimSpace(requestID)
+ message = strings.TrimSpace(message)
+ if requestID == "" {
+ return false, 0
+ }
+ if message == "" {
+ message = defaultMessage
+ }
+
+ data, err := json.Marshal(map[string]string{"message": message})
+ if err != nil {
+ fallback, marshalErr := json.Marshal(map[string]string{"message": defaultMessage})
+ if marshalErr != nil {
+ fallback = []byte(`{"message":"Remote chat request failed. Please retry."}`)
+ }
+ data = fallback
+ }
+
+ now := time.Now()
+ var broadcast *ChatBroadcastEvent
+ var runSubscribers []*chatRunSubscriber
+ var subscribers []chan *ChatBroadcastEvent
+
+ m.chatStore.chatMu.Lock()
+ m.pruneExpiredChatRunsLocked(now)
+ run := m.chatStore.chatRuns[requestID]
+ if shouldFail == nil || !shouldFail(run) {
+ m.chatStore.chatMu.Unlock()
+ return false, 0
+ }
+ sessionEpoch := run.sessionEpoch
+
+ run.nextSeq += 1
+ run.updatedAt = now
+ run.applyState(ChatRunStateFailed)
+ run.errorCode = "desktop_runtime_unavailable"
+ run.expiresAt = now.Add(chatRunDoneRetention)
+ chatEvent := &gatewayv1.ChatEvent{
+ Type: gatewayv1.ChatEvent_ERROR,
+ ConversationId: strings.TrimSpace(run.conversationID),
+ Data: string(data),
+ }
+ broadcast = &ChatBroadcastEvent{
+ RequestID: requestID,
+ Event: chatEvent,
+ Seq: run.nextSeq,
+ Workdir: strings.TrimSpace(run.workdir),
+ }
+ run.events = append(run.events, cloneChatBroadcastEvent(broadcast))
+ if len(run.events) > maxBufferedChatRunEvents {
+ copy(run.events, run.events[len(run.events)-maxBufferedChatRunEvents:])
+ run.events = run.events[:maxBufferedChatRunEvents]
+ }
+ runSubscribers = make([]*chatRunSubscriber, 0, len(run.subscribers))
+ for _, subscriber := range run.subscribers {
+ runSubscribers = append(runSubscribers, subscriber)
+ }
+ subscribers = make([]chan *ChatBroadcastEvent, 0, len(m.chatStore.chatSubscribers))
+ for _, ch := range m.chatStore.chatSubscribers {
+ subscribers = append(subscribers, ch)
+ }
+ m.chatStore.chatMu.Unlock()
+
+ for _, subscriber := range runSubscribers {
+ select {
+ case <-subscriber.done:
+ case subscriber.ch <- cloneChatBroadcastEvent(broadcast):
}
}
+ for _, ch := range subscribers {
+ select {
+ case ch <- cloneChatBroadcastEvent(broadcast):
+ default:
+ }
+ }
+ return true, sessionEpoch
}
func (m *Manager) SubscribeChatRun(
@@ -381,6 +537,14 @@ func (m *Manager) broadcastChatEvent(requestID string, event *gatewayv1.ChatEven
conversationID := strings.TrimSpace(event.GetConversationId())
now := time.Now()
sessionEpoch := m.currentSessionEpoch()
+ if isChatAcceptedControlEvent(event) {
+ m.markChatRunStateSilent(requestID, conversationID, ChatRunStateDelivered, now)
+ return
+ }
+ if isChatStartedControlEvent(event) {
+ m.markChatRunStateSilent(requestID, conversationID, ChatRunStateRunning, now)
+ return
+ }
m.chatStore.chatMu.Lock()
m.pruneExpiredChatRunsLocked(now)
@@ -391,21 +555,27 @@ func (m *Manager) broadcastChatEvent(requestID string, event *gatewayv1.ChatEven
var runSubscribers []*chatRunSubscriber
run := m.chatStore.chatRuns[requestID]
if run == nil && requestID != "" {
+ m.chatStore.nextChatRunEpoch += 1
run = &chatRun{
requestID: requestID,
conversationID: conversationID,
sessionEpoch: sessionEpoch,
+ runEpoch: m.chatStore.nextChatRunEpoch,
+ state: ChatRunStateQueued,
updatedAt: now,
subscribers: make(map[int]*chatRunSubscriber),
}
+ run.applyState(ChatRunStateQueued)
m.chatStore.chatRuns[requestID] = run
if conversationID != "" {
m.chatStore.chatRunByConversation[conversationID] = requestID
}
}
if run != nil {
- run.nextSeq += 1
- run.updatedAt = now
+ if run.done {
+ m.chatStore.chatMu.Unlock()
+ return
+ }
if conversationID != "" {
if run.conversationID != "" && run.conversationID != conversationID {
if m.chatStore.chatRunByConversation[run.conversationID] == requestID {
@@ -420,6 +590,11 @@ func (m *Manager) broadcastChatEvent(requestID string, event *gatewayv1.ChatEven
}
}
}
+ if !run.done && normalizeChatRunState(run.state) != ChatRunStateRunning && !isTerminalChatEvent(event) {
+ run.applyState(ChatRunStateRunning)
+ }
+ run.nextSeq += 1
+ run.updatedAt = now
broadcast.Seq = run.nextSeq
broadcast.Workdir = strings.TrimSpace(run.workdir)
run.events = append(run.events, cloneChatBroadcastEvent(broadcast))
@@ -428,7 +603,14 @@ func (m *Manager) broadcastChatEvent(requestID string, event *gatewayv1.ChatEven
run.events = run.events[:maxBufferedChatRunEvents]
}
if isTerminalChatEvent(event) {
- run.done = true
+ if event.GetType() == gatewayv1.ChatEvent_DONE {
+ run.applyState(ChatRunStateCompleted)
+ } else {
+ run.applyState(ChatRunStateFailed)
+ if run.errorCode == "" {
+ run.errorCode = "desktop_error"
+ }
+ }
run.expiresAt = now.Add(chatRunDoneRetention)
}
runSubscribers = make([]*chatRunSubscriber, 0, len(run.subscribers))
@@ -456,6 +638,139 @@ func (m *Manager) broadcastChatEvent(requestID string, event *gatewayv1.ChatEven
}
}
+func (m *Manager) broadcastChatControl(requestID string, control *gatewayv1.ChatControlEvent) {
+ if control == nil {
+ return
+ }
+ requestID = strings.TrimSpace(requestID)
+ if requestID == "" {
+ requestID = strings.TrimSpace(control.GetRequestId())
+ }
+ conversationID := strings.TrimSpace(control.GetConversationId())
+ controlType := strings.TrimSpace(control.GetType())
+ state := normalizeChatRunState(control.GetState())
+ if state == "" {
+ state = chatRunStateForControlType(controlType)
+ }
+ errorCode := strings.TrimSpace(control.GetErrorCode())
+ message := strings.TrimSpace(control.GetMessage())
+ m.markChatRunControl(requestID, conversationID, controlType, state, errorCode, message, time.Now())
+}
+
+func (m *Manager) markChatRunStateSilent(
+ requestID string,
+ conversationID string,
+ state string,
+ now time.Time,
+) {
+ requestID = strings.TrimSpace(requestID)
+ conversationID = strings.TrimSpace(conversationID)
+ state = normalizeChatRunState(state)
+ if requestID == "" || state == "" {
+ return
+ }
+ m.chatStore.chatMu.Lock()
+ defer m.chatStore.chatMu.Unlock()
+ m.pruneExpiredChatRunsLocked(now)
+ run := m.chatStore.chatRuns[requestID]
+ if run == nil || run.done {
+ return
+ }
+ if conversationID != "" {
+ if run.conversationID != "" && run.conversationID != conversationID {
+ if m.chatStore.chatRunByConversation[run.conversationID] == requestID {
+ delete(m.chatStore.chatRunByConversation, run.conversationID)
+ }
+ }
+ run.conversationID = conversationID
+ m.chatStore.chatRunByConversation[conversationID] = requestID
+ }
+ run.applyState(state)
+ run.updatedAt = now
+ if isTerminalChatRunState(state) {
+ run.expiresAt = now.Add(chatRunDoneRetention)
+ }
+}
+
+func (m *Manager) markChatRunControl(
+ requestID string,
+ conversationID string,
+ controlType string,
+ state string,
+ errorCode string,
+ message string,
+ now time.Time,
+) {
+ requestID = strings.TrimSpace(requestID)
+ conversationID = strings.TrimSpace(conversationID)
+ if requestID == "" {
+ return
+ }
+
+ state = normalizeChatRunState(state)
+ controlType = strings.TrimSpace(controlType)
+ if controlType == "" {
+ controlType = chatControlTypeForState(state)
+ }
+
+ m.chatStore.chatMu.Lock()
+ m.pruneExpiredChatRunsLocked(now)
+ run := m.chatStore.chatRuns[requestID]
+ if run == nil {
+ m.chatStore.nextChatRunEpoch += 1
+ run = &chatRun{
+ requestID: requestID,
+ conversationID: conversationID,
+ sessionEpoch: m.currentSessionEpoch(),
+ runEpoch: m.chatStore.nextChatRunEpoch,
+ state: ChatRunStateQueued,
+ updatedAt: now,
+ subscribers: make(map[int]*chatRunSubscriber),
+ }
+ run.applyState(ChatRunStateQueued)
+ m.chatStore.chatRuns[requestID] = run
+ }
+ if run.done {
+ m.chatStore.chatMu.Unlock()
+ return
+ }
+ if conversationID != "" {
+ if run.conversationID != "" && run.conversationID != conversationID {
+ if m.chatStore.chatRunByConversation[run.conversationID] == requestID {
+ delete(m.chatStore.chatRunByConversation, run.conversationID)
+ }
+ }
+ run.conversationID = conversationID
+ m.chatStore.chatRunByConversation[conversationID] = requestID
+ }
+ broadcast := m.appendChatControlLocked(run, controlType, errorCode, message, now)
+ runSubscribers := make([]*chatRunSubscriber, 0, len(run.subscribers))
+ for _, subscriber := range run.subscribers {
+ runSubscribers = append(runSubscribers, subscriber)
+ }
+ subscribers := make([]chan *ChatBroadcastEvent, 0, len(m.chatStore.chatSubscribers))
+ for _, ch := range m.chatStore.chatSubscribers {
+ subscribers = append(subscribers, ch)
+ }
+ m.chatStore.chatMu.Unlock()
+
+ if broadcast == nil {
+ return
+ }
+ for _, subscriber := range runSubscribers {
+ select {
+ case <-subscriber.done:
+ case subscriber.ch <- cloneChatBroadcastEvent(broadcast):
+ }
+ }
+ for _, ch := range subscribers {
+ select {
+ case ch <- cloneChatBroadcastEvent(broadcast):
+ default:
+ }
+ }
+}
+
func (m *Manager) DispatchFromAgent(env *gatewayv1.AgentEnvelope) {
m.dispatchFromAgent(nil, env)
}
@@ -472,10 +787,19 @@ func (m *Manager) dispatchFromAgent(expected *AgentSession, env *gatewayv1.Agent
return
}
+ if runtimeStatus := env.GetRuntimeStatus(); runtimeStatus != nil {
+ m.UpdateRuntimeStatus(session, runtimeStatus)
+ return
+ }
+
if chatEvent := env.GetChatEvent(); chatEvent != nil {
m.broadcastChatEvent(env.GetRequestId(), chatEvent)
}
+ if chatControl := env.GetChatControl(); chatControl != nil {
+ m.broadcastChatControl(env.GetRequestId(), chatControl)
+ }
+
if historySync := env.GetHistorySync(); historySync != nil {
m.broadcastHistorySync(historySync)
return
@@ -491,6 +815,16 @@ func (m *Manager) dispatchFromAgent(expected *AgentSession, env *gatewayv1.Agent
return
}
+ if tunnelFrame := env.GetTunnelFrame(); tunnelFrame != nil {
+ m.dispatchTunnelFrame(tunnelFrame)
+ return
+ }
+
+ if tunnelControl := env.GetTunnelControl(); tunnelControl != nil {
+ m.handleAgentTunnelControl(session, env.GetRequestId(), tunnelControl)
+ return
+ }
+
session.dispatch(env)
}
@@ -499,6 +833,7 @@ func (r *chatRun) snapshot() ChatRunSnapshot {
if len(r.events) > 0 {
firstSeq = r.events[0].Seq
}
+ state := normalizeChatRunState(r.state)
return ChatRunSnapshot{
RequestID: r.requestID,
ConversationID: r.conversationID,
@@ -506,10 +841,27 @@ func (r *chatRun) snapshot() ChatRunSnapshot {
Workdir: r.workdir,
FirstSeq: firstSeq,
LatestSeq: r.nextSeq,
+ RunEpoch: r.runEpoch,
+ State: state,
+ ErrorCode: strings.TrimSpace(r.errorCode),
Done: r.done,
}
}
+func (r *chatRun) applyState(state string) {
+ state = normalizeChatRunState(state)
+ if state == "" {
+ state = ChatRunStateQueued
+ }
+ r.state = state
+ r.accepted = state != ChatRunStateQueued
+ r.started = state == ChatRunStateRunning || state == ChatRunStateCompleted
+ r.done = isTerminalChatRunState(state)
+ if state != ChatRunStateFailed {
+ r.errorCode = ""
+ }
+}
+
func (s *chatRunSubscriber) close() {
s.closeOnce.Do(func() {
close(s.done)
@@ -528,6 +880,10 @@ func (m *Manager) pruneExpiredChatRunsLocked(now time.Time) {
}
continue
}
+ if normalizeChatRunState(run.state) != ChatRunStateRunning && !run.updatedAt.IsZero() && now.Sub(run.updatedAt) > chatRunStartRetention {
+ m.removeChatRunLocked(requestID, run)
+ continue
+ }
if !run.updatedAt.IsZero() && now.Sub(run.updatedAt) > chatRunStaleRetention {
m.removeChatRunLocked(requestID, run)
}
@@ -564,14 +920,182 @@ func cloneChatBroadcastEvent(event *ChatBroadcastEvent) *ChatBroadcastEvent {
return &ChatBroadcastEvent{
RequestID: event.RequestID,
Event: event.Event,
+ Control: event.Control,
Seq: event.Seq,
Workdir: event.Workdir,
}
}
+func normalizeChatRunState(state string) string {
+ switch strings.TrimSpace(state) {
+ case ChatRunStateQueued:
+ return ChatRunStateQueued
+ case ChatRunStateDelivered:
+ return ChatRunStateDelivered
+ case ChatRunStateClaimed:
+ return ChatRunStateClaimed
+ case ChatRunStateStarting:
+ return ChatRunStateStarting
+ case ChatRunStateRunning:
+ return ChatRunStateRunning
+ case ChatRunStateCompleted:
+ return ChatRunStateCompleted
+ case ChatRunStateFailed:
+ return ChatRunStateFailed
+ case ChatRunStateCancelled:
+ return ChatRunStateCancelled
+ default:
+ return ""
+ }
+}
+
+func isTerminalChatRunState(state string) bool {
+ switch normalizeChatRunState(state) {
+ case ChatRunStateCompleted, ChatRunStateFailed, ChatRunStateCancelled:
+ return true
+ default:
+ return false
+ }
+}
+
+func ChatRunStateIsActive(state string) bool {
+ switch normalizeChatRunState(state) {
+ case ChatRunStateQueued, ChatRunStateDelivered, ChatRunStateClaimed, ChatRunStateStarting, ChatRunStateRunning:
+ return true
+ default:
+ return false
+ }
+}
+
+func chatRunStateForControlType(controlType string) string {
+ switch strings.TrimSpace(controlType) {
+ case "accepted":
+ return ChatRunStateQueued
+ case "delivered":
+ return ChatRunStateDelivered
+ case "claimed":
+ return ChatRunStateClaimed
+ case "starting":
+ return ChatRunStateStarting
+ case "started":
+ return ChatRunStateRunning
+ case "completed":
+ return ChatRunStateCompleted
+ case "failed":
+ return ChatRunStateFailed
+ case "cancelled":
+ return ChatRunStateCancelled
+ default:
+ return ""
+ }
+}
+
+func chatControlTypeForState(state string) string {
+ switch normalizeChatRunState(state) {
+ case ChatRunStateQueued:
+ return "accepted"
+ case ChatRunStateDelivered:
+ return "delivered"
+ case ChatRunStateClaimed:
+ return "claimed"
+ case ChatRunStateStarting:
+ return "starting"
+ case ChatRunStateRunning:
+ return "started"
+ case ChatRunStateCompleted:
+ return "completed"
+ case ChatRunStateFailed:
+ return "failed"
+ case ChatRunStateCancelled:
+ return "cancelled"
+ default:
+ return "progress"
+ }
+}
+
+func (m *Manager) appendChatControlLocked(
+ run *chatRun,
+ controlType string,
+ errorCode string,
+ message string,
+ now time.Time,
+) *ChatBroadcastEvent {
+ if run == nil {
+ return nil
+ }
+ controlType = strings.TrimSpace(controlType)
+ state := chatRunStateForControlType(controlType)
+ if state == "" {
+ state = normalizeChatRunState(run.state)
+ }
+ if state == "" {
+ state = ChatRunStateQueued
+ }
+ run.applyState(state)
+ if errorCode = strings.TrimSpace(errorCode); errorCode != "" {
+ run.errorCode = errorCode
+ }
+ run.updatedAt = now
+ if isTerminalChatRunState(state) {
+ run.expiresAt = now.Add(chatRunDoneRetention)
+ }
+ run.nextSeq += 1
+ seq := run.nextSeq
+ if controlType == "" {
+ controlType = chatControlTypeForState(state)
+ }
+ control := &gatewayv1.ChatControlEvent{
+ RequestId: strings.TrimSpace(run.requestID),
+ ClientRequestId: strings.TrimSpace(run.clientRequestID),
+ ConversationId: strings.TrimSpace(run.conversationID),
+ RunEpoch: run.runEpoch,
+ Type: controlType,
+ State: normalizeChatRunState(run.state),
+ ErrorCode: strings.TrimSpace(run.errorCode),
+ Message: strings.TrimSpace(message),
+ Seq: seq,
+ }
+ broadcast := &ChatBroadcastEvent{
+ RequestID: strings.TrimSpace(run.requestID),
+ Control: control,
+ Seq: seq,
+ Workdir: strings.TrimSpace(run.workdir),
+ }
+ run.events = append(run.events, cloneChatBroadcastEvent(broadcast))
+ if len(run.events) > maxBufferedChatRunEvents {
+ copy(run.events, run.events[len(run.events)-maxBufferedChatRunEvents:])
+ run.events = run.events[:maxBufferedChatRunEvents]
+ }
+ return broadcast
+}
+
func isTerminalChatEvent(event *gatewayv1.ChatEvent) bool {
if event == nil {
return false
}
return event.GetType() == gatewayv1.ChatEvent_DONE || event.GetType() == gatewayv1.ChatEvent_ERROR
}
+
+func isChatStartedControlEvent(event *gatewayv1.ChatEvent) bool {
+ return chatControlEventType(event) == "started"
+}
+
+func isChatAcceptedControlEvent(event *gatewayv1.ChatEvent) bool {
+ return chatControlEventType(event) == "accepted"
+}
+
+func chatControlEventType(event *gatewayv1.ChatEvent) string {
+ if event == nil || event.GetType() != gatewayv1.ChatEvent_TOKEN {
+ return ""
+ }
+ raw := strings.TrimSpace(event.GetData())
+ if raw == "" {
+ return ""
+ }
+ var decoded map[string]any
+ if err := json.Unmarshal([]byte(raw), &decoded); err != nil {
+ return ""
+ }
+ value, _ := decoded["type"].(string)
+ return strings.TrimSpace(value)
+}
diff --git a/crates/agent-gateway/internal/session/manager_registry.go b/crates/agent-gateway/internal/session/manager_registry.go
index 09609a19c..c31f46961 100644
--- a/crates/agent-gateway/internal/session/manager_registry.go
+++ b/crates/agent-gateway/internal/session/manager_registry.go
@@ -1,6 +1,8 @@
package session
import (
+ "context"
+ "strings"
"time"
gatewayv1 "github.com/liveagent/agent-gateway/internal/proto/v1"
@@ -40,6 +42,7 @@ func (m *Manager) SetSession(s *AgentSession) {
}
if previous != s {
m.registry.sessionEpoch += 1
+ clearRuntimeStatusLocked(m.registry)
}
sessionChanged := previous != s
m.registry.session = s
@@ -62,6 +65,7 @@ func (m *Manager) ClearSession(session *AgentSession) {
}
clearedEpoch := m.registry.sessionEpoch
m.registry.session = nil
+ clearRuntimeStatusLocked(m.registry)
m.registry.mu.Unlock()
if session == nil {
@@ -73,10 +77,54 @@ func (m *Manager) ClearSession(session *AgentSession) {
m.failOpenChatRunsForSessionEpoch(clearedEpoch, agentDisconnectedChatRunMessage)
}
+func (m *Manager) ClearSessionIfHeartbeatStale(session *AgentSession, timeout time.Duration) bool {
+ if session == nil || timeout <= 0 {
+ return false
+ }
+
+ now := time.Now()
+ m.registry.mu.Lock()
+ if m.registry.session != session {
+ m.registry.mu.Unlock()
+ return false
+ }
+ if lastPing := m.registry.session.LastPing; !lastPing.IsZero() && now.Sub(lastPing) <= timeout {
+ m.registry.mu.Unlock()
+ return false
+ }
+ clearedEpoch := m.registry.sessionEpoch
+ m.registry.session = nil
+ clearRuntimeStatusLocked(m.registry)
+ m.registry.mu.Unlock()
+
+ session.Close()
+ m.clearTerminalSessionSnapshot()
+ m.failOpenChatRunsForSessionEpoch(clearedEpoch, agentDisconnectedChatRunMessage)
+ return true
+}
+
+func (m *Manager) ClearSessionForEpoch(sessionEpoch uint64) bool {
+ m.registry.mu.Lock()
+ session := m.registry.session
+ if session == nil || m.registry.sessionEpoch != sessionEpoch {
+ m.registry.mu.Unlock()
+ return false
+ }
+ m.registry.session = nil
+ clearRuntimeStatusLocked(m.registry)
+ m.registry.mu.Unlock()
+
+ session.Close()
+ m.clearTerminalSessionSnapshot()
+ m.failOpenChatRunsForSessionEpoch(sessionEpoch, agentDisconnectedChatRunMessage)
+ return true
+}
+
func (m *Manager) Status() Status {
m.registry.mu.RLock()
defer m.registry.mu.RUnlock()
+ now := time.Now()
status := Status{}
if m.registry.authValid {
status.AgentID = m.registry.lastAuth.AgentID
@@ -87,14 +135,52 @@ func (m *Manager) Status() Status {
return status
}
status.Online = true
+ status.AgentReady = true
status.AgentID = m.registry.session.AgentID
status.AgentVersion = m.registry.session.AgentVersion
status.SessionID = m.registry.session.SessionID
status.ConnectedSince = m.registry.session.ConnectedAt.Unix()
status.LastHeartbeat = m.registry.session.LastPing.Unix()
+ status.RuntimeState = m.registry.runtimeState
+ status.RuntimeWorkerID = m.registry.runtimeWorkerID
+ status.RuntimeVisible = m.registry.runtimeVisible
+ status.RuntimeActiveRunCount = m.registry.runtimeActiveRunCount
+ if !m.registry.runtimeLastHeartbeat.IsZero() {
+ status.RuntimeLastHeartbeat = m.registry.runtimeLastHeartbeat.Unix()
+ }
+ status.ChatRuntimeReady = runtimeReadyLocked(m.registry, now)
return status
}
+func (m *Manager) ChatRuntimeReady() bool {
+ m.registry.mu.RLock()
+ defer m.registry.mu.RUnlock()
+ return runtimeReadyLocked(m.registry, time.Now())
+}
+
+func (m *Manager) UpdateRuntimeStatus(
+ session *AgentSession,
+ event *gatewayv1.RuntimeStatusEvent,
+) {
+ if event == nil {
+ return
+ }
+ workerID := strings.TrimSpace(event.GetWorkerId())
+ state := normalizeRuntimeState(event.GetState())
+ now := time.Now()
+
+ m.registry.mu.Lock()
+ defer m.registry.mu.Unlock()
+ if m.registry.session == nil || (session != nil && m.registry.session != session) {
+ return
+ }
+ m.registry.runtimeState = state
+ m.registry.runtimeWorkerID = workerID
+ m.registry.runtimeLastHeartbeat = now
+ m.registry.runtimeVisible = event.GetVisible()
+ m.registry.runtimeActiveRunCount = event.GetActiveRunCount()
+}
+
func (m *Manager) TouchHeartbeat(session *AgentSession) {
m.registry.mu.Lock()
defer m.registry.mu.Unlock()
@@ -103,6 +189,42 @@ func (m *Manager) TouchHeartbeat(session *AgentSession) {
}
}
+func clearRuntimeStatusLocked(registry *sessionRegistry) {
+ registry.runtimeState = ""
+ registry.runtimeWorkerID = ""
+ registry.runtimeLastHeartbeat = time.Time{}
+ registry.runtimeVisible = false
+ registry.runtimeActiveRunCount = 0
+}
+
+func runtimeReadyLocked(registry *sessionRegistry, now time.Time) bool {
+ if registry == nil || registry.session == nil {
+ return false
+ }
+ if registry.session.LastPing.IsZero() || now.Sub(registry.session.LastPing) > agentSessionHeartbeatTTL {
+ return false
+ }
+ if registry.runtimeLastHeartbeat.IsZero() ||
+ now.Sub(registry.runtimeLastHeartbeat) > chatRuntimeReadyTTL {
+ return false
+ }
+ switch normalizeRuntimeState(registry.runtimeState) {
+ case "ready", "draining", "busy":
+ return true
+ default:
+ return false
+ }
+}
+
+func normalizeRuntimeState(state string) string {
+ switch strings.TrimSpace(state) {
+ case "ready", "draining", "busy", "suspended":
+ return strings.TrimSpace(state)
+ default:
+ return defaultRuntimeReadyState
+ }
+}
+
func (m *Manager) SendToAgent(env *gatewayv1.GatewayEnvelope) error {
m.registry.mu.RLock()
session := m.registry.session
@@ -111,7 +233,29 @@ func (m *Manager) SendToAgent(env *gatewayv1.GatewayEnvelope) error {
return ErrAgentOffline
}
- return session.SendToAgent(env)
+ err := session.SendToAgent(env)
+ m.clearSessionAfterSendError(session, err)
+ return err
+}
+
+func (m *Manager) SendToAgentContext(ctx context.Context, env *gatewayv1.GatewayEnvelope) error {
+ m.registry.mu.RLock()
+ session := m.registry.session
+ m.registry.mu.RUnlock()
+ if session == nil {
+ return ErrAgentOffline
+ }
+
+ err := session.SendToAgentContext(ctx, env)
+ m.clearSessionAfterSendError(session, err)
+ return err
+}
+
+func (m *Manager) clearSessionAfterSendError(session *AgentSession, err error) {
+ if err == nil || session == nil {
+ return
+ }
+ m.ClearSession(session)
}
func (m *Manager) currentSessionEpoch() uint64 {
diff --git a/crates/agent-gateway/internal/session/manager_state.go b/crates/agent-gateway/internal/session/manager_state.go
index a32a4528b..55c780bbe 100644
--- a/crates/agent-gateway/internal/session/manager_state.go
+++ b/crates/agent-gateway/internal/session/manager_state.go
@@ -2,6 +2,7 @@ package session
import (
"sync"
+ "time"
gatewayv1 "github.com/liveagent/agent-gateway/internal/proto/v1"
)
@@ -12,6 +13,12 @@ type sessionRegistry struct {
sessionEpoch uint64
lastAuth AuthSnapshot
authValid bool
+
+ runtimeState string
+ runtimeWorkerID string
+ runtimeLastHeartbeat time.Time
+ runtimeVisible bool
+ runtimeActiveRunCount uint32
}
func newSessionRegistry() *sessionRegistry {
@@ -49,6 +56,7 @@ type chatRunStore struct {
nextChatSubID int
chatSubscribers map[int]chan *ChatBroadcastEvent
nextChatRunSubID int
+ nextChatRunEpoch int64
chatRuns map[string]*chatRun
chatRunByConversation map[string]string
chatRunByClientRequest map[string]string
diff --git a/crates/agent-gateway/internal/session/manager_tunnel.go b/crates/agent-gateway/internal/session/manager_tunnel.go
new file mode 100644
index 000000000..aaf2c1712
--- /dev/null
+++ b/crates/agent-gateway/internal/session/manager_tunnel.go
@@ -0,0 +1,843 @@
+package session
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "net/url"
+ "strings"
+ "sync"
+ "time"
+
+ gatewayv1 "github.com/liveagent/agent-gateway/internal/proto/v1"
+)
+
+const (
+ MaxTunnelsPerAgent = 5
+ MaxTunnelConnections = 20
+ defaultTunnelTTLSeconds = 3600
+ tunnelSlugEntropyBytes = 24
+ tunnelStreamChannelDepth = 256
+ tunnelAgentSendTimeout = 10 * time.Second
+)
+
+type tunnelStore struct {
+ mu sync.Mutex
+ tunnelsByID map[string]*tunnelRecord
+ tunnelIDBySlug map[string]string
+ streams map[string]*tunnelStream
+}
+
+type tunnelRecord struct {
+ id string
+ slug string
+ name string
+ targetURL string
+ publicURL string
+ projectPathKey string
+ createdAt time.Time
+ expiresAt time.Time
+ activeConnections int
+ closed bool
+}
+
+type tunnelStream struct {
+ streamID string
+ tunnelID string
+ ch chan *gatewayv1.TunnelFrame
+ done chan struct{}
+ once sync.Once
+}
+
+type TunnelStreamLease struct {
+ manager *Manager
+ stream *tunnelStream
+ tunnel *gatewayv1.TunnelSummary
+ once sync.Once
+}
+
+func newTunnelStore() *tunnelStore {
+ return &tunnelStore{
+ tunnelsByID: make(map[string]*tunnelRecord),
+ tunnelIDBySlug: make(map[string]string),
+ streams: make(map[string]*tunnelStream),
+ }
+}
+
+func (l *TunnelStreamLease) Tunnel() *gatewayv1.TunnelSummary {
+ if l == nil || l.tunnel == nil {
+ return nil
+ }
+ return cloneTunnelSummary(l.tunnel)
+}
+
+func (l *TunnelStreamLease) TunnelID() string {
+ if l == nil || l.stream == nil {
+ return ""
+ }
+ return l.stream.tunnelID
+}
+
+func (l *TunnelStreamLease) StreamID() string {
+ if l == nil || l.stream == nil {
+ return ""
+ }
+ return l.stream.streamID
+}
+
+func (l *TunnelStreamLease) Frames() <-chan *gatewayv1.TunnelFrame {
+ if l == nil || l.stream == nil {
+ return nil
+ }
+ return l.stream.ch
+}
+
+func (l *TunnelStreamLease) Done() <-chan struct{} {
+ if l == nil || l.stream == nil {
+ return nil
+ }
+ return l.stream.done
+}
+
+func (l *TunnelStreamLease) Release() {
+ if l == nil {
+ return
+ }
+ l.once.Do(func() {
+ l.manager.releaseTunnelStream(l.stream)
+ })
+}
+
+func (s *tunnelStream) close() {
+ if s == nil {
+ return
+ }
+ s.once.Do(func() {
+ close(s.done)
+ })
+}
+
+func (s *tunnelStream) send(frame *gatewayv1.TunnelFrame) bool {
+ select {
+ case <-s.done:
+ return false
+ case s.ch <- frame:
+ return true
+ }
+}
+
+func (m *Manager) WebTunnelsEnabled() bool {
+ m.syncHub.settingsSnapshotMu.RLock()
+ defer m.syncHub.settingsSnapshotMu.RUnlock()
+
+ remote, ok := m.syncHub.settingsSnapshot["remote"].(map[string]any)
+ if !ok {
+ return false
+ }
+ enabled, ok := remote["enableWebTunnels"].(bool)
+ return ok && enabled
+}
+
+func (m *Manager) ListTunnels() []*gatewayv1.TunnelSummary {
+ now := time.Now()
+ online := m.IsOnline()
+ m.tunnels.mu.Lock()
+ defer m.tunnels.mu.Unlock()
+
+ summaries := make([]*gatewayv1.TunnelSummary, 0, len(m.tunnels.tunnelsByID))
+ for _, record := range m.tunnels.tunnelsByID {
+ if record == nil || record.closed {
+ continue
+ }
+ summaries = append(summaries, tunnelSummaryLocked(record, now, online))
+ }
+ sortTunnelSummaries(summaries)
+ return summaries
+}
+
+func (m *Manager) PrepareTunnelCreate(
+ input *gatewayv1.TunnelControlRequest,
+ publicBaseURL string,
+) (*gatewayv1.TunnelControlRequest, error) {
+ if input == nil {
+ return nil, errors.New("tunnel create input is required")
+ }
+ ttlSeconds, err := normalizeTunnelTTL(input.GetTtlSeconds())
+ if err != nil {
+ return nil, err
+ }
+ now := time.Now()
+ var expiresAt time.Time
+ if ttlSeconds > 0 {
+ expiresAt = now.Add(time.Duration(ttlSeconds) * time.Second)
+ }
+
+ m.tunnels.mu.Lock()
+ defer m.tunnels.mu.Unlock()
+
+ activeCount := 0
+ for _, record := range m.tunnels.tunnelsByID {
+ if record == nil || record.closed || isTunnelExpired(record, now) {
+ continue
+ }
+ activeCount += 1
+ }
+ if activeCount >= MaxTunnelsPerAgent {
+ return nil, ErrTunnelLimitExceeded
+ }
+
+ id := strings.TrimSpace(input.GetTunnelId())
+ if id == "" {
+ id = generateTunnelID()
+ }
+ if _, exists := m.tunnels.tunnelsByID[id]; exists {
+ return nil, fmt.Errorf("tunnel id already exists")
+ }
+ slug := strings.TrimSpace(input.GetSlug())
+ if slug == "" {
+ for {
+ generated, err := generateTunnelSlug()
+ if err != nil {
+ return nil, err
+ }
+ if _, exists := m.tunnels.tunnelIDBySlug[generated]; !exists {
+ slug = generated
+ break
+ }
+ }
+ } else if _, exists := m.tunnels.tunnelIDBySlug[slug]; exists {
+ return nil, fmt.Errorf("tunnel slug already exists")
+ }
+
+ publicURL := normalizeTunnelPublicURL(input.GetPublicUrl())
+ if publicURL == "" {
+ publicURL = buildTunnelPublicURL(publicBaseURL, slug)
+ }
+
+ return &gatewayv1.TunnelControlRequest{
+ Action: strings.TrimSpace(input.GetAction()),
+ TunnelId: id,
+ Slug: slug,
+ TargetUrl: strings.TrimSpace(input.GetTargetUrl()),
+ Name: strings.TrimSpace(input.GetName()),
+ TtlSeconds: ttlSeconds,
+ ExpiresAt: tunnelUnix(expiresAt),
+ PublicUrl: publicURL,
+ PublicBaseUrl: strings.TrimSpace(publicBaseURL),
+ ProjectPathKey: strings.TrimSpace(input.GetProjectPathKey()),
+ }, nil
+}
+
+func (m *Manager) PrepareTunnelUpdate(
+ input *gatewayv1.TunnelControlRequest,
+) (*gatewayv1.TunnelControlRequest, error) {
+ if input == nil {
+ return nil, errors.New("tunnel update input is required")
+ }
+ ttlSeconds, err := normalizeTunnelTTL(input.GetTtlSeconds())
+ if err != nil {
+ return nil, err
+ }
+ now := time.Now()
+ var expiresAt time.Time
+ if input.GetExpiresAt() > 0 {
+ expiresAt = time.Unix(input.GetExpiresAt(), 0)
+ } else if ttlSeconds > 0 {
+ expiresAt = now.Add(time.Duration(ttlSeconds) * time.Second)
+ }
+ targetURL := strings.TrimSpace(input.GetTargetUrl())
+ if targetURL == "" {
+ return nil, errors.New("target_url is required")
+ }
+
+ m.tunnels.mu.Lock()
+ defer m.tunnels.mu.Unlock()
+
+ identifier := strings.TrimSpace(input.GetTunnelId())
+ if identifier == "" {
+ identifier = strings.TrimSpace(input.GetSlug())
+ }
+ if identifier == "" {
+ return nil, ErrTunnelNotFound
+ }
+ tunnelID := identifier
+ if bySlug := m.tunnels.tunnelIDBySlug[identifier]; bySlug != "" {
+ tunnelID = bySlug
+ }
+ record := m.tunnels.tunnelsByID[tunnelID]
+ if record == nil || record.closed {
+ return nil, ErrTunnelNotFound
+ }
+ if isTunnelExpired(record, now) {
+ return nil, ErrTunnelExpired
+ }
+ projectPathKey := strings.TrimSpace(input.GetProjectPathKey())
+ if projectPathKey == "" {
+ projectPathKey = record.projectPathKey
+ }
+
+ return &gatewayv1.TunnelControlRequest{
+ Action: strings.TrimSpace(input.GetAction()),
+ TunnelId: record.id,
+ Slug: record.slug,
+ TargetUrl: targetURL,
+ Name: strings.TrimSpace(input.GetName()),
+ TtlSeconds: ttlSeconds,
+ ExpiresAt: tunnelUnix(expiresAt),
+ PublicUrl: record.publicURL,
+ ProjectPathKey: projectPathKey,
+ }, nil
+}
+
+func (m *Manager) StorePreparedTunnel(
+ prepared *gatewayv1.TunnelControlRequest,
+ targetURLOverride string,
+) (*gatewayv1.TunnelSummary, error) {
+ if prepared == nil {
+ return nil, errors.New("prepared tunnel is required")
+ }
+ now := time.Now()
+ targetURL := strings.TrimSpace(targetURLOverride)
+ if targetURL == "" {
+ targetURL = strings.TrimSpace(prepared.GetTargetUrl())
+ }
+ if targetURL == "" {
+ return nil, errors.New("target_url is required")
+ }
+ var expiresAt time.Time
+ if prepared.GetExpiresAt() > 0 {
+ expiresAt = time.Unix(prepared.GetExpiresAt(), 0)
+ } else if prepared.GetTtlSeconds() > 0 {
+ ttlSeconds, err := normalizeTunnelTTL(prepared.GetTtlSeconds())
+ if err != nil {
+ return nil, err
+ }
+ expiresAt = now.Add(time.Duration(ttlSeconds) * time.Second)
+ }
+ record := &tunnelRecord{
+ id: strings.TrimSpace(prepared.GetTunnelId()),
+ slug: strings.TrimSpace(prepared.GetSlug()),
+ name: strings.TrimSpace(prepared.GetName()),
+ targetURL: targetURL,
+ publicURL: normalizeTunnelPublicURL(prepared.GetPublicUrl()),
+ projectPathKey: strings.TrimSpace(prepared.GetProjectPathKey()),
+ createdAt: now,
+ expiresAt: expiresAt,
+ }
+ if record.id == "" || record.slug == "" {
+ return nil, errors.New("prepared tunnel is missing id or slug")
+ }
+ if record.publicURL == "" {
+ record.publicURL = buildTunnelPublicURL(prepared.GetPublicBaseUrl(), record.slug)
+ }
+
+ online := m.IsOnline()
+ m.tunnels.mu.Lock()
+ defer m.tunnels.mu.Unlock()
+ if _, exists := m.tunnels.tunnelsByID[record.id]; exists {
+ return nil, fmt.Errorf("tunnel id already exists")
+ }
+ if _, exists := m.tunnels.tunnelIDBySlug[record.slug]; exists {
+ return nil, fmt.Errorf("tunnel slug already exists")
+ }
+ m.tunnels.tunnelsByID[record.id] = record
+ m.tunnels.tunnelIDBySlug[record.slug] = record.id
+ return tunnelSummaryLocked(record, now, online), nil
+}
+
+func (m *Manager) CreateTunnelFromAgent(
+ input *gatewayv1.TunnelControlRequest,
+) (*gatewayv1.TunnelSummary, error) {
+ prepared, err := m.PrepareTunnelCreate(input, input.GetPublicBaseUrl())
+ if err != nil {
+ return nil, err
+ }
+ return m.StorePreparedTunnel(prepared, input.GetTargetUrl())
+}
+
+func (m *Manager) UpdateTunnelFromAgent(
+ input *gatewayv1.TunnelControlRequest,
+) (*gatewayv1.TunnelSummary, error) {
+ prepared, err := m.PrepareTunnelUpdate(input)
+ if err != nil {
+ return nil, err
+ }
+ return m.ApplyTunnelUpdate(&gatewayv1.TunnelSummary{
+ Id: prepared.GetTunnelId(),
+ Slug: prepared.GetSlug(),
+ Name: prepared.GetName(),
+ TargetUrl: prepared.GetTargetUrl(),
+ PublicUrl: prepared.GetPublicUrl(),
+ ExpiresAt: prepared.GetExpiresAt(),
+ ProjectPathKey: prepared.GetProjectPathKey(),
+ })
+}
+
+func (m *Manager) ApplyTunnelUpdate(summary *gatewayv1.TunnelSummary) (*gatewayv1.TunnelSummary, error) {
+ if summary == nil {
+ return nil, errors.New("tunnel update summary is required")
+ }
+ identifier := strings.TrimSpace(summary.GetId())
+ if identifier == "" {
+ identifier = strings.TrimSpace(summary.GetSlug())
+ }
+ if identifier == "" {
+ return nil, ErrTunnelNotFound
+ }
+ targetURL := strings.TrimSpace(summary.GetTargetUrl())
+ if targetURL == "" {
+ return nil, errors.New("target_url is required")
+ }
+ now := time.Now()
+ online := m.IsOnline()
+ var expiresAt time.Time
+ if summary.GetExpiresAt() > 0 {
+ expiresAt = time.Unix(summary.GetExpiresAt(), 0)
+ }
+
+ m.tunnels.mu.Lock()
+ defer m.tunnels.mu.Unlock()
+
+ tunnelID := identifier
+ if bySlug := m.tunnels.tunnelIDBySlug[identifier]; bySlug != "" {
+ tunnelID = bySlug
+ }
+ record := m.tunnels.tunnelsByID[tunnelID]
+ if record == nil || record.closed {
+ return nil, ErrTunnelNotFound
+ }
+ if isTunnelExpired(record, now) {
+ return nil, ErrTunnelExpired
+ }
+ record.name = strings.TrimSpace(summary.GetName())
+ record.targetURL = targetURL
+ if publicURL := normalizeTunnelPublicURL(summary.GetPublicUrl()); publicURL != "" {
+ record.publicURL = publicURL
+ }
+ record.projectPathKey = strings.TrimSpace(summary.GetProjectPathKey())
+ record.expiresAt = expiresAt
+ return tunnelSummaryLocked(record, now, online), nil
+}
+
+func (m *Manager) AcquireTunnel(slug string, streamID string) (*TunnelStreamLease, error) {
+ slug = strings.TrimSpace(slug)
+ streamID = strings.TrimSpace(streamID)
+ if slug == "" || streamID == "" {
+ return nil, ErrTunnelNotFound
+ }
+ if !m.IsOnline() {
+ return nil, ErrAgentOffline
+ }
+ now := time.Now()
+ online := true
+
+ m.tunnels.mu.Lock()
+ defer m.tunnels.mu.Unlock()
+
+ tunnelID := m.tunnels.tunnelIDBySlug[slug]
+ record := m.tunnels.tunnelsByID[tunnelID]
+ if record == nil || record.closed {
+ return nil, ErrTunnelNotFound
+ }
+ if isTunnelExpired(record, now) {
+ return nil, ErrTunnelExpired
+ }
+ if record.activeConnections >= MaxTunnelConnections {
+ return nil, ErrTunnelOverLimit
+ }
+ stream := &tunnelStream{
+ streamID: streamID,
+ tunnelID: record.id,
+ ch: make(chan *gatewayv1.TunnelFrame, tunnelStreamChannelDepth),
+ done: make(chan struct{}),
+ }
+ if existing := m.tunnels.streams[streamID]; existing != nil {
+ existing.close()
+ }
+ m.tunnels.streams[streamID] = stream
+ record.activeConnections += 1
+
+ return &TunnelStreamLease{
+ manager: m,
+ stream: stream,
+ tunnel: tunnelSummaryLocked(record, now, online),
+ }, nil
+}
+
+func (m *Manager) CloseTunnel(identifier string) (*gatewayv1.TunnelSummary, error) {
+ identifier = strings.TrimSpace(identifier)
+ if identifier == "" {
+ return nil, ErrTunnelNotFound
+ }
+ now := time.Now()
+ online := m.IsOnline()
+
+ var summary *gatewayv1.TunnelSummary
+ var cancelFrames []*gatewayv1.TunnelFrame
+ m.tunnels.mu.Lock()
+ tunnelID := identifier
+ if bySlug := m.tunnels.tunnelIDBySlug[identifier]; bySlug != "" {
+ tunnelID = bySlug
+ }
+ record := m.tunnels.tunnelsByID[tunnelID]
+ if record == nil || record.closed {
+ m.tunnels.mu.Unlock()
+ return nil, ErrTunnelNotFound
+ }
+ record.closed = true
+ summary = tunnelSummaryLocked(record, now, online)
+ delete(m.tunnels.tunnelsByID, record.id)
+ delete(m.tunnels.tunnelIDBySlug, record.slug)
+ for streamID, stream := range m.tunnels.streams {
+ if stream == nil || stream.tunnelID != record.id {
+ continue
+ }
+ delete(m.tunnels.streams, streamID)
+ stream.close()
+ cancelFrames = append(cancelFrames, &gatewayv1.TunnelFrame{
+ StreamId: stream.streamID,
+ TunnelId: record.id,
+ Slug: record.slug,
+ Kind: gatewayv1.TunnelFrameKind_TUNNEL_FRAME_KIND_CANCEL,
+ })
+ }
+ m.tunnels.mu.Unlock()
+
+ for _, frame := range cancelFrames {
+ _ = m.SendTunnelFrameToAgent(frame)
+ }
+ return summary, nil
+}
+
+func (m *Manager) ResumeTunnel(input *gatewayv1.TunnelControlRequest) (*gatewayv1.TunnelSummary, error) {
+ if input == nil {
+ return nil, errors.New("resume tunnel input is required")
+ }
+ now := time.Now()
+ online := m.IsOnline()
+ id := strings.TrimSpace(input.GetTunnelId())
+ slug := strings.TrimSpace(input.GetSlug())
+ if id == "" && slug == "" {
+ return nil, ErrTunnelNotFound
+ }
+
+ m.tunnels.mu.Lock()
+ defer m.tunnels.mu.Unlock()
+ if id == "" {
+ id = m.tunnels.tunnelIDBySlug[slug]
+ }
+ record := m.tunnels.tunnelsByID[id]
+ if record == nil || record.closed {
+ return nil, ErrTunnelNotFound
+ }
+ if slug != "" && record.slug != slug {
+ return nil, ErrTunnelNotFound
+ }
+ if isTunnelExpired(record, now) {
+ return nil, ErrTunnelExpired
+ }
+ if targetURL := strings.TrimSpace(input.GetTargetUrl()); targetURL != "" {
+ record.targetURL = targetURL
+ }
+ if name := strings.TrimSpace(input.GetName()); name != "" {
+ record.name = name
+ }
+ if projectPathKey := strings.TrimSpace(input.GetProjectPathKey()); projectPathKey != "" {
+ record.projectPathKey = projectPathKey
+ }
+ return tunnelSummaryLocked(record, now, online), nil
+}
+
+func (m *Manager) SendTunnelFrameToAgent(frame *gatewayv1.TunnelFrame) error {
+ if frame == nil {
+ return errors.New("tunnel frame is required")
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), tunnelAgentSendTimeout)
+ defer cancel()
+ return m.SendToAgentContext(ctx, &gatewayv1.GatewayEnvelope{
+ RequestId: fmt.Sprintf("tunnel-frame-%s", strings.TrimSpace(frame.GetStreamId())),
+ Timestamp: time.Now().Unix(),
+ Payload: &gatewayv1.GatewayEnvelope_TunnelFrame{
+ TunnelFrame: frame,
+ },
+ })
+}
+
+func (m *Manager) dispatchTunnelFrame(frame *gatewayv1.TunnelFrame) {
+ if frame == nil {
+ return
+ }
+ streamID := strings.TrimSpace(frame.GetStreamId())
+ if streamID == "" {
+ return
+ }
+ m.tunnels.mu.Lock()
+ stream := m.tunnels.streams[streamID]
+ m.tunnels.mu.Unlock()
+ if stream == nil {
+ return
+ }
+ stream.send(frame)
+}
+
+func (m *Manager) handleAgentTunnelControl(
+ session *AgentSession,
+ requestID string,
+ request *gatewayv1.TunnelControlRequest,
+) {
+ if session == nil || request == nil {
+ return
+ }
+ response := m.handleAgentTunnelControlInner(request)
+ ctx, cancel := context.WithTimeout(context.Background(), tunnelAgentSendTimeout)
+ defer cancel()
+ _ = session.SendToAgentContext(ctx, &gatewayv1.GatewayEnvelope{
+ RequestId: requestID,
+ Timestamp: time.Now().Unix(),
+ Payload: &gatewayv1.GatewayEnvelope_TunnelControlResp{
+ TunnelControlResp: response,
+ },
+ })
+}
+
+func (m *Manager) handleAgentTunnelControlInner(
+ request *gatewayv1.TunnelControlRequest,
+) *gatewayv1.TunnelControlResponse {
+ action := strings.ToLower(strings.TrimSpace(request.GetAction()))
+ if action == "" {
+ return tunnelControlError("invalid_action", "tunnel action is required")
+ }
+ switch action {
+ case "list":
+ return &gatewayv1.TunnelControlResponse{
+ Action: action,
+ Tunnels: m.ListTunnels(),
+ }
+ case "create":
+ tunnel, err := m.CreateTunnelFromAgent(request)
+ if err != nil {
+ return tunnelControlErrorFor(action, err)
+ }
+ return &gatewayv1.TunnelControlResponse{
+ Action: action,
+ Tunnel: tunnel,
+ Tunnels: m.ListTunnels(),
+ }
+ case "update":
+ tunnel, err := m.UpdateTunnelFromAgent(request)
+ if err != nil {
+ return tunnelControlErrorFor(action, err)
+ }
+ return &gatewayv1.TunnelControlResponse{
+ Action: action,
+ Tunnel: tunnel,
+ Tunnels: m.ListTunnels(),
+ }
+ case "close":
+ identifier := request.GetTunnelId()
+ if strings.TrimSpace(identifier) == "" {
+ identifier = request.GetSlug()
+ }
+ tunnel, err := m.CloseTunnel(identifier)
+ if err != nil {
+ return tunnelControlErrorFor(action, err)
+ }
+ return &gatewayv1.TunnelControlResponse{
+ Action: action,
+ Tunnel: tunnel,
+ Tunnels: m.ListTunnels(),
+ }
+ case "resume":
+ tunnel, err := m.ResumeTunnel(request)
+ if err != nil {
+ return tunnelControlErrorFor(action, err)
+ }
+ return &gatewayv1.TunnelControlResponse{
+ Action: action,
+ Tunnel: tunnel,
+ Tunnels: m.ListTunnels(),
+ }
+ default:
+ return tunnelControlError("invalid_action", "unsupported tunnel action")
+ }
+}
+
+func (m *Manager) releaseTunnelStream(stream *tunnelStream) {
+ if stream == nil {
+ return
+ }
+ m.tunnels.mu.Lock()
+ if existing := m.tunnels.streams[stream.streamID]; existing == stream {
+ delete(m.tunnels.streams, stream.streamID)
+ }
+ if record := m.tunnels.tunnelsByID[stream.tunnelID]; record != nil && record.activeConnections > 0 {
+ record.activeConnections -= 1
+ }
+ stream.close()
+ m.tunnels.mu.Unlock()
+}
+
+func normalizeTunnelTTL(input uint32) (uint32, error) {
+ switch input {
+ case 0:
+ return 0, nil
+ case 900, 3600, 14400:
+ return input, nil
+ default:
+ return 0, errors.New("ttl_seconds must be one of 0, 900, 3600, or 14400")
+ }
+}
+
+func tunnelUnix(value time.Time) int64 {
+ if value.IsZero() {
+ return 0
+ }
+ return value.Unix()
+}
+
+func generateTunnelID() string {
+ return "tun_" + strings.ReplaceAll(time.Now().UTC().Format("20060102150405.000000000"), ".", "") + "_" + randomURLToken(8)
+}
+
+func generateTunnelSlug() (string, error) {
+ token := randomURLToken(tunnelSlugEntropyBytes)
+ if token == "" {
+ return "", errors.New("generate tunnel slug failed")
+ }
+ return token, nil
+}
+
+func randomURLToken(byteCount int) string {
+ if byteCount <= 0 {
+ return ""
+ }
+ buf := make([]byte, byteCount)
+ if _, err := rand.Read(buf); err != nil {
+ return ""
+ }
+ return base64.RawURLEncoding.EncodeToString(buf)
+}
+
+func normalizeTunnelPublicURL(input string) string {
+ trimmed := strings.TrimSpace(input)
+ if trimmed == "" {
+ return ""
+ }
+ parsed, err := url.Parse(trimmed)
+ if err != nil || parsed.Scheme == "" || parsed.Host == "" {
+ return ""
+ }
+ parsed.RawQuery = ""
+ parsed.Fragment = ""
+ if !strings.HasSuffix(parsed.Path, "/") {
+ parsed.Path += "/"
+ }
+ return parsed.String()
+}
+
+func buildTunnelPublicURL(publicBaseURL string, slug string) string {
+ base := strings.TrimSpace(publicBaseURL)
+ if base == "" || strings.TrimSpace(slug) == "" {
+ return ""
+ }
+ parsed, err := url.Parse(base)
+ if err != nil || parsed.Scheme == "" || parsed.Host == "" {
+ return ""
+ }
+ parsed.RawQuery = ""
+ parsed.Fragment = ""
+ parsed.Path = strings.TrimRight(parsed.Path, "/") + "/t/" + strings.TrimSpace(slug) + "/"
+ return parsed.String()
+}
+
+func isTunnelExpired(record *tunnelRecord, now time.Time) bool {
+ return record == nil || (!record.expiresAt.IsZero() && !record.expiresAt.After(now))
+}
+
+func tunnelSummaryLocked(record *tunnelRecord, now time.Time, online bool) *gatewayv1.TunnelSummary {
+ if record == nil {
+ return &gatewayv1.TunnelSummary{Status: "expired"}
+ }
+ status := "active"
+ if record.closed || isTunnelExpired(record, now) {
+ status = "expired"
+ } else if !online {
+ status = "offline"
+ }
+ activeConnections := uint32(0)
+ if record.activeConnections > 0 {
+ activeConnections = uint32(record.activeConnections)
+ }
+ return &gatewayv1.TunnelSummary{
+ Id: record.id,
+ Slug: record.slug,
+ Name: record.name,
+ TargetUrl: record.targetURL,
+ PublicUrl: record.publicURL,
+ CreatedAt: record.createdAt.Unix(),
+ ExpiresAt: tunnelUnix(record.expiresAt),
+ ActiveConnections: activeConnections,
+ Status: status,
+ ProjectPathKey: record.projectPathKey,
+ }
+}
+
+func cloneTunnelSummary(summary *gatewayv1.TunnelSummary) *gatewayv1.TunnelSummary {
+ if summary == nil {
+ return nil
+ }
+ return &gatewayv1.TunnelSummary{
+ Id: summary.GetId(),
+ Slug: summary.GetSlug(),
+ Name: summary.GetName(),
+ TargetUrl: summary.GetTargetUrl(),
+ PublicUrl: summary.GetPublicUrl(),
+ CreatedAt: summary.GetCreatedAt(),
+ ExpiresAt: summary.GetExpiresAt(),
+ ActiveConnections: summary.GetActiveConnections(),
+ Status: summary.GetStatus(),
+ ProjectPathKey: strings.TrimSpace(summary.GetProjectPathKey()),
+ }
+}
+
+func sortTunnelSummaries(summaries []*gatewayv1.TunnelSummary) {
+ for i := 1; i < len(summaries); i++ {
+ current := summaries[i]
+ j := i - 1
+ for j >= 0 && summaries[j].GetCreatedAt() > current.GetCreatedAt() {
+ summaries[j+1] = summaries[j]
+ j--
+ }
+ summaries[j+1] = current
+ }
+}
+
+func tunnelControlError(code string, message string) *gatewayv1.TunnelControlResponse {
+ return &gatewayv1.TunnelControlResponse{
+ ErrorCode: strings.TrimSpace(code),
+ ErrorMessage: strings.TrimSpace(message),
+ }
+}
+
+func tunnelControlErrorFor(action string, err error) *gatewayv1.TunnelControlResponse {
+ code := "failed"
+ switch {
+ case errors.Is(err, ErrTunnelNotFound):
+ code = "not_found"
+ case errors.Is(err, ErrTunnelExpired):
+ code = "expired"
+ case errors.Is(err, ErrTunnelLimitExceeded):
+ code = "limit_exceeded"
+ case errors.Is(err, ErrTunnelOverLimit):
+ code = "over_limit"
+ case errors.Is(err, ErrAgentOffline):
+ code = "agent_offline"
+ }
+ return &gatewayv1.TunnelControlResponse{
+ Action: strings.TrimSpace(action),
+ ErrorCode: code,
+ ErrorMessage: err.Error(),
+ }
+}
diff --git a/crates/agent-gateway/internal/session/manager_tunnel_test.go b/crates/agent-gateway/internal/session/manager_tunnel_test.go
new file mode 100644
index 000000000..1e147a0b3
--- /dev/null
+++ b/crates/agent-gateway/internal/session/manager_tunnel_test.go
@@ -0,0 +1,265 @@
+package session
+
+import (
+ "errors"
+ "testing"
+ "time"
+
+ gatewayv1 "github.com/liveagent/agent-gateway/internal/proto/v1"
+)
+
+func onlineTunnelTestManager() *Manager {
+ manager := NewManager()
+ manager.SetSession(NewAgentSession(AuthSnapshot{
+ AgentID: "agent-a",
+ AgentVersion: "test",
+ SessionID: "session-a",
+ }))
+ return manager
+}
+
+func createTestTunnel(t *testing.T, manager *Manager, name string) *gatewayv1.TunnelSummary {
+ t.Helper()
+ tunnel, err := manager.CreateTunnelFromAgent(&gatewayv1.TunnelControlRequest{
+ Action: "create",
+ TargetUrl: "http://localhost:3000/app",
+ Name: name,
+ TtlSeconds: 3600,
+ PublicBaseUrl: "https://gateway.example",
+ })
+ if err != nil {
+ t.Fatalf("CreateTunnelFromAgent: %v", err)
+ }
+ if tunnel.GetSlug() == "" || tunnel.GetPublicUrl() == "" {
+ t.Fatalf("created tunnel missing slug/public URL: %+v", tunnel)
+ }
+ return tunnel
+}
+
+func TestTunnelRegistryCreateLimitListAndClose(t *testing.T) {
+ manager := onlineTunnelTestManager()
+
+ var first *gatewayv1.TunnelSummary
+ for i := 0; i < MaxTunnelsPerAgent; i++ {
+ tunnel := createTestTunnel(t, manager, "app")
+ if i == 0 {
+ first = tunnel
+ }
+ }
+
+ if _, err := manager.CreateTunnelFromAgent(&gatewayv1.TunnelControlRequest{
+ Action: "create",
+ TargetUrl: "http://localhost:3001",
+ TtlSeconds: 3600,
+ PublicBaseUrl: "https://gateway.example",
+ }); !errors.Is(err, ErrTunnelLimitExceeded) {
+ t.Fatalf("expected ErrTunnelLimitExceeded, got %v", err)
+ }
+
+ if got := len(manager.ListTunnels()); got != MaxTunnelsPerAgent {
+ t.Fatalf("ListTunnels returned %d tunnels, want %d", got, MaxTunnelsPerAgent)
+ }
+
+ closed, err := manager.CloseTunnel(first.GetId())
+ if err != nil {
+ t.Fatalf("CloseTunnel: %v", err)
+ }
+ if closed.GetStatus() != "expired" {
+ t.Fatalf("closed tunnel summary status = %q, want expired", closed.GetStatus())
+ }
+ if got := len(manager.ListTunnels()); got != MaxTunnelsPerAgent-1 {
+ t.Fatalf("ListTunnels after close returned %d tunnels, want %d", got, MaxTunnelsPerAgent-1)
+ }
+}
+
+func TestTunnelAcquireConnectionLimitAndRelease(t *testing.T) {
+ manager := onlineTunnelTestManager()
+ tunnel := createTestTunnel(t, manager, "app")
+
+ leases := make([]*TunnelStreamLease, 0, MaxTunnelConnections)
+ for i := 0; i < MaxTunnelConnections; i++ {
+ lease, err := manager.AcquireTunnel(tunnel.GetSlug(), "stream-"+string(rune('a'+i)))
+ if err != nil {
+ t.Fatalf("AcquireTunnel %d: %v", i, err)
+ }
+ leases = append(leases, lease)
+ }
+ if _, err := manager.AcquireTunnel(tunnel.GetSlug(), "stream-over-limit"); !errors.Is(err, ErrTunnelOverLimit) {
+ t.Fatalf("expected ErrTunnelOverLimit, got %v", err)
+ }
+
+ leases[0].Release()
+ lease, err := manager.AcquireTunnel(tunnel.GetSlug(), "stream-after-release")
+ if err != nil {
+ t.Fatalf("AcquireTunnel after release: %v", err)
+ }
+ lease.Release()
+ for _, item := range leases[1:] {
+ item.Release()
+ }
+
+ summaries := manager.ListTunnels()
+ if len(summaries) != 1 {
+ t.Fatalf("ListTunnels returned %d tunnels, want 1", len(summaries))
+ }
+ if got := summaries[0].GetActiveConnections(); got != 0 {
+ t.Fatalf("active connections after release = %d, want 0", got)
+ }
+}
+
+func TestTunnelExpiredCannotBeAcquired(t *testing.T) {
+ manager := onlineTunnelTestManager()
+ tunnel := createTestTunnel(t, manager, "app")
+
+ manager.tunnels.mu.Lock()
+ manager.tunnels.tunnelsByID[tunnel.GetId()].expiresAt = time.Now().Add(-time.Second)
+ manager.tunnels.mu.Unlock()
+
+ if _, err := manager.AcquireTunnel(tunnel.GetSlug(), "stream-expired"); !errors.Is(err, ErrTunnelExpired) {
+ t.Fatalf("expected ErrTunnelExpired, got %v", err)
+ }
+ summaries := manager.ListTunnels()
+ if len(summaries) != 1 {
+ t.Fatalf("ListTunnels returned %d tunnels, want 1", len(summaries))
+ }
+ if summaries[0].GetStatus() != "expired" {
+ t.Fatalf("expired tunnel status = %q, want expired", summaries[0].GetStatus())
+ }
+}
+
+func TestTunnelInfiniteTTLCreatesNonExpiringTunnel(t *testing.T) {
+ manager := onlineTunnelTestManager()
+ tunnel, err := manager.CreateTunnelFromAgent(&gatewayv1.TunnelControlRequest{
+ Action: "create",
+ TargetUrl: "http://localhost:3000/app",
+ Name: "app",
+ TtlSeconds: 0,
+ PublicBaseUrl: "https://gateway.example",
+ })
+ if err != nil {
+ t.Fatalf("CreateTunnelFromAgent with infinite TTL: %v", err)
+ }
+ if tunnel.GetExpiresAt() != 0 {
+ t.Fatalf("infinite tunnel expiresAt = %d, want 0", tunnel.GetExpiresAt())
+ }
+ if tunnel.GetStatus() != "active" {
+ t.Fatalf("infinite tunnel status = %q, want active", tunnel.GetStatus())
+ }
+
+ manager.tunnels.mu.Lock()
+ manager.tunnels.tunnelsByID[tunnel.GetId()].expiresAt = time.Time{}
+ manager.tunnels.mu.Unlock()
+
+ lease, err := manager.AcquireTunnel(tunnel.GetSlug(), "stream-infinite")
+ if err != nil {
+ t.Fatalf("AcquireTunnel for infinite tunnel: %v", err)
+ }
+ lease.Release()
+}
+
+func TestTunnelUpdateChangesTargetNameScopeAndTTL(t *testing.T) {
+ manager := onlineTunnelTestManager()
+ tunnel := createTestTunnel(t, manager, "app")
+
+ updated, err := manager.UpdateTunnelFromAgent(&gatewayv1.TunnelControlRequest{
+ Action: "update",
+ TunnelId: tunnel.GetId(),
+ TargetUrl: "http://127.0.0.1:4000/dashboard",
+ Name: "dashboard",
+ TtlSeconds: 0,
+ ProjectPathKey: "project:/tmp/liveagent",
+ })
+ if err != nil {
+ t.Fatalf("UpdateTunnelFromAgent: %v", err)
+ }
+ if updated.GetName() != "dashboard" {
+ t.Fatalf("updated name = %q, want dashboard", updated.GetName())
+ }
+ if updated.GetTargetUrl() != "http://127.0.0.1:4000/dashboard" {
+ t.Fatalf("updated target = %q", updated.GetTargetUrl())
+ }
+ if updated.GetExpiresAt() != 0 {
+ t.Fatalf("updated expiresAt = %d, want 0", updated.GetExpiresAt())
+ }
+ if updated.GetProjectPathKey() != "project:/tmp/liveagent" {
+ t.Fatalf("updated projectPathKey = %q", updated.GetProjectPathKey())
+ }
+
+ listed := manager.ListTunnels()
+ if len(listed) != 1 {
+ t.Fatalf("ListTunnels returned %d tunnels, want 1", len(listed))
+ }
+ if listed[0].GetId() != tunnel.GetId() || listed[0].GetTargetUrl() != updated.GetTargetUrl() {
+ t.Fatalf("ListTunnels did not include updated tunnel: %+v", listed[0])
+ }
+}
+
+func TestTunnelInfiniteTTLStaysActiveAndVisible(t *testing.T) {
+ manager := onlineTunnelTestManager()
+
+ tunnel, err := manager.CreateTunnelFromAgent(&gatewayv1.TunnelControlRequest{
+ Action: "create",
+ TargetUrl: "http://localhost:3000/app",
+ Name: "app",
+ TtlSeconds: 0,
+ PublicBaseUrl: "https://gateway.example",
+ ProjectPathKey: "/workspace/app",
+ })
+ if err != nil {
+ t.Fatalf("CreateTunnelFromAgent: %v", err)
+ }
+ if tunnel.GetExpiresAt() != 0 {
+ t.Fatalf("infinite tunnel expires_at = %d, want 0", tunnel.GetExpiresAt())
+ }
+ if tunnel.GetProjectPathKey() != "/workspace/app" {
+ t.Fatalf("project_path_key = %q, want /workspace/app", tunnel.GetProjectPathKey())
+ }
+
+ summaries := manager.ListTunnels()
+ if len(summaries) != 1 {
+ t.Fatalf("ListTunnels returned %d tunnels, want 1", len(summaries))
+ }
+ if summaries[0].GetStatus() != "active" {
+ t.Fatalf("infinite tunnel status = %q, want active", summaries[0].GetStatus())
+ }
+ if summaries[0].GetExpiresAt() != 0 {
+ t.Fatalf("listed infinite tunnel expires_at = %d, want 0", summaries[0].GetExpiresAt())
+ }
+}
+
+func TestTunnelUpdateChangesTargetNameTTLAndKeepsProjectScope(t *testing.T) {
+ manager := onlineTunnelTestManager()
+ tunnel := createTestTunnel(t, manager, "app")
+
+ updated, err := manager.UpdateTunnelFromAgent(&gatewayv1.TunnelControlRequest{
+ Action: "update",
+ TunnelId: tunnel.GetId(),
+ TargetUrl: "http://localhost:3000/next",
+ Name: "next",
+ TtlSeconds: 0,
+ ProjectPathKey: "/workspace/app",
+ })
+ if err != nil {
+ t.Fatalf("UpdateTunnelFromAgent: %v", err)
+ }
+ if updated.GetTargetUrl() != "http://localhost:3000/next" {
+ t.Fatalf("target_url = %q, want http://localhost:3000/next", updated.GetTargetUrl())
+ }
+ if updated.GetName() != "next" {
+ t.Fatalf("name = %q, want next", updated.GetName())
+ }
+ if updated.GetExpiresAt() != 0 {
+ t.Fatalf("updated expires_at = %d, want 0", updated.GetExpiresAt())
+ }
+ if updated.GetProjectPathKey() != "/workspace/app" {
+ t.Fatalf("project_path_key = %q, want /workspace/app", updated.GetProjectPathKey())
+ }
+
+ listed := manager.ListTunnels()
+ if len(listed) != 1 {
+ t.Fatalf("ListTunnels returned %d tunnels, want 1", len(listed))
+ }
+ if listed[0].GetTargetUrl() != "http://localhost:3000/next" {
+ t.Fatalf("listed target_url = %q, want http://localhost:3000/next", listed[0].GetTargetUrl())
+ }
+}
diff --git a/crates/agent-gateway/proto/v1/gateway.proto b/crates/agent-gateway/proto/v1/gateway.proto
index 94a0df3c6..d15952d75 100644
--- a/crates/agent-gateway/proto/v1/gateway.proto
+++ b/crates/agent-gateway/proto/v1/gateway.proto
@@ -63,6 +63,9 @@ message GatewayEnvelope {
GitRequest git_request = 61;
FsReadEditableTextRequest fs_read_editable_text = 62;
FsReadWorkspaceImageRequest fs_read_workspace_image = 63;
+ TunnelControlRequest tunnel_control = 67;
+ TunnelControlResponse tunnel_control_resp = 68;
+ TunnelFrame tunnel_frame = 69;
}
}
@@ -110,6 +113,11 @@ message AgentEnvelope {
GitResponse git_response = 64;
FsReadEditableTextResponse fs_read_editable_text_resp = 65;
FsReadWorkspaceImageResponse fs_read_workspace_image_resp = 66;
+ TunnelControlRequest tunnel_control = 67;
+ TunnelControlResponse tunnel_control_resp = 68;
+ TunnelFrame tunnel_frame = 69;
+ ChatControlEvent chat_control = 70;
+ RuntimeStatusEvent runtime_status = 71;
ErrorResponse error = 99;
}
}
@@ -160,6 +168,75 @@ message UploadedImagePreviewResponse {
string data = 2;
}
+message TunnelControlRequest {
+ string action = 1;
+ string tunnel_id = 2;
+ string slug = 3;
+ string target_url = 4;
+ string name = 5;
+ uint32 ttl_seconds = 6;
+ int64 expires_at = 7;
+ string public_url = 8;
+ string public_base_url = 9;
+ string project_path_key = 10;
+}
+
+message TunnelControlResponse {
+ string action = 1;
+ repeated TunnelSummary tunnels = 2;
+ TunnelSummary tunnel = 3;
+ string error_code = 4;
+ string error_message = 5;
+}
+
+message TunnelSummary {
+ string id = 1;
+ string slug = 2;
+ string name = 3;
+ string target_url = 4;
+ string public_url = 5;
+ int64 created_at = 6;
+ int64 expires_at = 7;
+ uint32 active_connections = 8;
+ string status = 9;
+ string project_path_key = 10;
+}
+
+message TunnelHeader {
+ string name = 1;
+ string value = 2;
+}
+
+enum TunnelFrameKind {
+ TUNNEL_FRAME_KIND_UNSPECIFIED = 0;
+ TUNNEL_FRAME_KIND_HTTP_REQUEST_START = 1;
+ TUNNEL_FRAME_KIND_HTTP_REQUEST_BODY = 2;
+ TUNNEL_FRAME_KIND_HTTP_REQUEST_END = 3;
+ TUNNEL_FRAME_KIND_HTTP_RESPONSE_START = 4;
+ TUNNEL_FRAME_KIND_HTTP_RESPONSE_BODY = 5;
+ TUNNEL_FRAME_KIND_HTTP_RESPONSE_END = 6;
+ TUNNEL_FRAME_KIND_WS_OPEN = 7;
+ TUNNEL_FRAME_KIND_WS_FRAME = 8;
+ TUNNEL_FRAME_KIND_WS_CLOSE = 9;
+ TUNNEL_FRAME_KIND_ERROR = 10;
+ TUNNEL_FRAME_KIND_CANCEL = 11;
+}
+
+message TunnelFrame {
+ string stream_id = 1;
+ string tunnel_id = 2;
+ string slug = 3;
+ TunnelFrameKind kind = 4;
+ string method = 5;
+ string path = 6;
+ repeated TunnelHeader headers = 7;
+ uint32 status_code = 8;
+ bytes body = 9;
+ bool end_stream = 10;
+ string error = 11;
+ string ws_message_type = 12;
+}
+
message MemoryManageRequest {
string command = 1;
string args_json = 2;
@@ -270,6 +347,26 @@ message ChatEvent {
}
}
+message ChatControlEvent {
+ string request_id = 1;
+ string client_request_id = 2;
+ string conversation_id = 3;
+ int64 run_epoch = 4;
+ string type = 5;
+ string state = 6;
+ string error_code = 7;
+ string message = 8;
+ int64 seq = 9;
+}
+
+message RuntimeStatusEvent {
+ string worker_id = 1;
+ string state = 2;
+ bool visible = 3;
+ uint32 active_run_count = 4;
+ int64 timestamp = 5;
+}
+
message CronManageRequest {
string action = 1;
string task_id = 2;
diff --git a/crates/agent-gateway/test/session/manager_test.go b/crates/agent-gateway/test/session/manager_test.go
index 53d5e645c..41d177d95 100644
--- a/crates/agent-gateway/test/session/manager_test.go
+++ b/crates/agent-gateway/test/session/manager_test.go
@@ -1,6 +1,7 @@
package session_test
import (
+ "context"
"errors"
"fmt"
"strings"
@@ -76,6 +77,122 @@ func TestClearSessionDoesNotCloseReplacement(t *testing.T) {
}
}
+func TestClearSessionIfHeartbeatStaleClosesOnlyCurrentSession(t *testing.T) {
+ t.Parallel()
+
+ sm := newTestSessionManager()
+ first := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(first)
+ second := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(second)
+
+ time.Sleep(time.Millisecond)
+ if sm.ClearSessionIfHeartbeatStale(first, time.Nanosecond) {
+ t.Fatalf("stale first session should not close replacement session")
+ }
+ assertDoneOpen(t, second.Done())
+ if status := sm.Status(); !status.Online {
+ t.Fatalf("status online = false after stale old-session heartbeat timeout")
+ }
+
+ time.Sleep(time.Millisecond)
+ if !sm.ClearSessionIfHeartbeatStale(second, time.Nanosecond) {
+ t.Fatalf("current stale session was not cleared")
+ }
+ assertDoneClosed(t, second.Done())
+ if status := sm.Status(); status.Online {
+ t.Fatalf("status online = true after current session heartbeat timeout")
+ }
+ if err := sm.SendToAgent(&gatewayv1.GatewayEnvelope{RequestId: "after-timeout"}); !errors.Is(err, session.ErrAgentOffline) {
+ t.Fatalf("SendToAgent after heartbeat timeout = %v, want ErrAgentOffline", err)
+ }
+}
+
+func TestChatRuntimeReadyRequiresFreshRuntimeHeartbeat(t *testing.T) {
+ t.Parallel()
+
+ sm := newTestSessionManager()
+ sess := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(sess)
+
+ if status := sm.Status(); !status.Online || status.ChatRuntimeReady {
+ t.Fatalf("initial status = %#v, want online without chat runtime readiness", status)
+ }
+
+ sm.UpdateRuntimeStatus(sess, &gatewayv1.RuntimeStatusEvent{
+ WorkerId: "runtime-1",
+ State: "ready",
+ Visible: true,
+ ActiveRunCount: 0,
+ Timestamp: time.Now().Unix(),
+ })
+ if status := sm.Status(); !status.ChatRuntimeReady ||
+ status.RuntimeState != "ready" ||
+ status.RuntimeWorkerID != "runtime-1" ||
+ status.RuntimeLastHeartbeat == 0 {
+ t.Fatalf("ready runtime status = %#v", status)
+ }
+
+ sm.UpdateRuntimeStatus(sess, &gatewayv1.RuntimeStatusEvent{
+ WorkerId: "runtime-1",
+ State: "suspended",
+ Timestamp: time.Now().Unix(),
+ })
+ if status := sm.Status(); status.ChatRuntimeReady || status.RuntimeState != "suspended" {
+ t.Fatalf("suspended runtime status = %#v, want not ready", status)
+ }
+
+ sm.UpdateRuntimeStatus(sess, &gatewayv1.RuntimeStatusEvent{
+ WorkerId: "runtime-1",
+ State: "busy",
+ Timestamp: time.Now().Unix(),
+ })
+ if !sm.ChatRuntimeReady() {
+ t.Fatalf("busy runtime should be ready to manage chat runs")
+ }
+
+ sm.ClearSession(sess)
+ if status := sm.Status(); status.ChatRuntimeReady || status.RuntimeState != "" {
+ t.Fatalf("cleared session status = %#v, want runtime readiness reset", status)
+ }
+}
+
+func TestClearSessionIfHeartbeatStaleFailsOpenChatRuns(t *testing.T) {
+ t.Parallel()
+
+ sm := newTestSessionManager()
+ sess := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(sess)
+ if _, _, err := sm.StartPendingChatRunWithClientRequest(
+ "request-1",
+ "conversation-1",
+ "client-submit-1",
+ ); err != nil {
+ t.Fatalf("StartPendingChatRunWithClientRequest: %v", err)
+ }
+ ch, _, cleanup, _, err := sm.SubscribeChatRun("request-1", "conversation-1", 0)
+ if err != nil {
+ t.Fatalf("SubscribeChatRun: %v", err)
+ }
+ defer cleanup()
+
+ time.Sleep(time.Millisecond)
+ if !sm.ClearSessionIfHeartbeatStale(sess, time.Nanosecond) {
+ t.Fatalf("current stale session was not cleared")
+ }
+ select {
+ case event := <-ch:
+ if event.Event.GetType() != gatewayv1.ChatEvent_ERROR {
+ t.Fatalf("event type = %v, want ERROR", event.Event.GetType())
+ }
+ if !strings.Contains(event.Event.GetData(), "Desktop agent disconnected") {
+ t.Fatalf("event data = %q", event.Event.GetData())
+ }
+ case <-time.After(time.Second):
+ t.Fatalf("timed out waiting for heartbeat timeout chat error")
+ }
+}
+
func TestDispatchFromStaleSessionIsIgnored(t *testing.T) {
t.Parallel()
@@ -158,6 +275,31 @@ func TestSendToAgentUnblocksWhenSessionCloses(t *testing.T) {
}
}
+func TestSendToAgentContextReturnsWhenOutboundQueueIsFull(t *testing.T) {
+ t.Parallel()
+
+ sm := newTestSessionManager()
+ sess := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(sess)
+
+ for i := 0; i < 64; i += 1 {
+ if err := sm.SendToAgent(&gatewayv1.GatewayEnvelope{RequestId: fmt.Sprintf("queued-%d", i)}); err != nil {
+ t.Fatalf("prime outbound queue: %v", err)
+ }
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
+ defer cancel()
+
+ err := sm.SendToAgentContext(ctx, &gatewayv1.GatewayEnvelope{RequestId: "blocked"})
+ if !errors.Is(err, context.DeadlineExceeded) {
+ t.Fatalf("SendToAgentContext with full queue = %v, want context deadline exceeded", err)
+ }
+ if status := sm.Status(); status.Online {
+ t.Fatalf("status online = true after SendToAgentContext timeout")
+ }
+}
+
func TestRemoveChatRunByConversationReleasesBufferedRun(t *testing.T) {
t.Parallel()
@@ -252,6 +394,251 @@ func TestStartChatRunWithClientRequestReusesExistingRun(t *testing.T) {
}
}
+func TestPendingChatRunBecomesActiveOnlyAfterStartedEvent(t *testing.T) {
+ t.Parallel()
+
+ sm := newTestSessionManager()
+ sm.SetSession(session.NewAgentSession(sm.LatestAuthSnapshot()))
+ snapshot, created, err := sm.StartPendingChatRunWithClientRequest(
+ "request-1",
+ "conversation-1",
+ "client-submit-1",
+ "/workspace",
+ )
+ if err != nil {
+ t.Fatalf("StartPendingChatRunWithClientRequest: %v", err)
+ }
+ if !created || snapshot.RequestID != "request-1" {
+ t.Fatalf("pending run = %#v created=%v", snapshot, created)
+ }
+ if got := sm.ActiveChatRunConversationIDs(); len(got) != 0 {
+ t.Fatalf("pending active chat runs = %#v, want empty", got)
+ }
+
+ ch, _, cleanup, _, err := sm.SubscribeChatRun("request-1", "conversation-1", 0)
+ if err != nil {
+ t.Fatalf("SubscribeChatRun: %v", err)
+ }
+ defer cleanup()
+
+ sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
+ RequestId: "request-1",
+ Payload: &gatewayv1.AgentEnvelope_ChatEvent{
+ ChatEvent: &gatewayv1.ChatEvent{
+ Type: gatewayv1.ChatEvent_TOKEN,
+ ConversationId: "conversation-1",
+ Data: `{"type":"accepted"}`,
+ },
+ },
+ })
+ if got := sm.ActiveChatRunConversationIDs(); len(got) != 0 {
+ t.Fatalf("accepted active chat runs = %#v, want empty", got)
+ }
+ if sm.FailStartingChatRun("request-1", "desktop did not accept") {
+ t.Fatalf("accepted pending run should not fail the accept watchdog")
+ }
+
+ sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
+ RequestId: "request-1",
+ Payload: &gatewayv1.AgentEnvelope_ChatEvent{
+ ChatEvent: &gatewayv1.ChatEvent{
+ Type: gatewayv1.ChatEvent_TOKEN,
+ ConversationId: "conversation-1",
+ Data: `{"type":"started"}`,
+ },
+ },
+ })
+
+ got := sm.ActiveChatRunConversationIDs()
+ want := []string{"conversation-1"}
+ if fmt.Sprint(got) != fmt.Sprint(want) {
+ t.Fatalf("active chat runs after started = %#v, want %#v", got, want)
+ }
+ select {
+ case event := <-ch:
+ t.Fatalf("started control event leaked to subscriber: %#v", event)
+ case <-time.After(50 * time.Millisecond):
+ }
+}
+
+func TestFailStartingChatRunBroadcastsErrorAndClearsActiveSummary(t *testing.T) {
+ t.Parallel()
+
+ sm := newTestSessionManager()
+ sm.SetSession(session.NewAgentSession(sm.LatestAuthSnapshot()))
+ if _, _, err := sm.StartPendingChatRunWithClientRequest(
+ "request-1",
+ "conversation-1",
+ "client-submit-1",
+ ); err != nil {
+ t.Fatalf("StartPendingChatRunWithClientRequest: %v", err)
+ }
+ ch, _, cleanup, _, err := sm.SubscribeChatRun("request-1", "conversation-1", 0)
+ if err != nil {
+ t.Fatalf("SubscribeChatRun: %v", err)
+ }
+ defer cleanup()
+
+ if !sm.FailStartingChatRun("request-1", "desktop did not accept") {
+ t.Fatalf("FailStartingChatRun returned false")
+ }
+
+ select {
+ case event := <-ch:
+ if event.Event.GetType() != gatewayv1.ChatEvent_ERROR {
+ t.Fatalf("event type = %v, want ERROR", event.Event.GetType())
+ }
+ if !strings.Contains(event.Event.GetData(), "desktop did not accept") {
+ t.Fatalf("event data = %q", event.Event.GetData())
+ }
+ case <-time.After(time.Second):
+ t.Fatalf("timed out waiting for starting run failure event")
+ }
+ if got := sm.ActiveChatRunConversationIDs(); len(got) != 0 {
+ t.Fatalf("active chat runs after failed start = %#v, want empty", got)
+ }
+ if status := sm.Status(); status.Online {
+ t.Fatalf("status online = true after chat run failed before desktop accept")
+ }
+}
+
+func TestFailUnstartedChatRunBroadcastsErrorUnlessStarted(t *testing.T) {
+ t.Parallel()
+
+ sm := newTestSessionManager()
+ sm.SetSession(session.NewAgentSession(sm.LatestAuthSnapshot()))
+ if _, _, err := sm.StartPendingChatRunWithClientRequest(
+ "request-1",
+ "conversation-1",
+ "client-submit-1",
+ ); err != nil {
+ t.Fatalf("StartPendingChatRunWithClientRequest request-1: %v", err)
+ }
+ ch, _, cleanup, _, err := sm.SubscribeChatRun("request-1", "conversation-1", 0)
+ if err != nil {
+ t.Fatalf("SubscribeChatRun: %v", err)
+ }
+ defer cleanup()
+ if sm.FailUnstartedChatRun("request-1", "desktop app did not start") {
+ t.Fatalf("unaccepted pending run should not fail the render-start watchdog")
+ }
+ sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
+ RequestId: "request-1",
+ Payload: &gatewayv1.AgentEnvelope_ChatEvent{
+ ChatEvent: &gatewayv1.ChatEvent{
+ Type: gatewayv1.ChatEvent_TOKEN,
+ ConversationId: "conversation-1",
+ Data: `{"type":"accepted"}`,
+ },
+ },
+ })
+
+ if !sm.FailUnstartedChatRun("request-1", "desktop app did not start") {
+ t.Fatalf("FailUnstartedChatRun returned false for accepted pending run")
+ }
+ select {
+ case event := <-ch:
+ if event.Event.GetType() != gatewayv1.ChatEvent_ERROR {
+ t.Fatalf("event type = %v, want ERROR", event.Event.GetType())
+ }
+ if !strings.Contains(event.Event.GetData(), "desktop app did not start") {
+ t.Fatalf("event data = %q", event.Event.GetData())
+ }
+ case <-time.After(time.Second):
+ t.Fatalf("timed out waiting for unstarted run failure event")
+ }
+
+ if _, _, err := sm.StartPendingChatRunWithClientRequest(
+ "request-2",
+ "conversation-2",
+ "client-submit-2",
+ ); err != nil {
+ t.Fatalf("StartPendingChatRunWithClientRequest request-2: %v", err)
+ }
+ sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
+ RequestId: "request-2",
+ Payload: &gatewayv1.AgentEnvelope_ChatEvent{
+ ChatEvent: &gatewayv1.ChatEvent{
+ Type: gatewayv1.ChatEvent_TOKEN,
+ ConversationId: "conversation-2",
+ Data: `{"type":"started"}`,
+ },
+ },
+ })
+ if sm.FailUnstartedChatRun("request-2", "desktop app did not start") {
+ t.Fatalf("started run should not fail the render-start watchdog")
+ }
+}
+
+func TestTerminalChatRunStateIsImmutable(t *testing.T) {
+ t.Parallel()
+
+ sm := newTestSessionManager()
+ sm.SetSession(session.NewAgentSession(sm.LatestAuthSnapshot()))
+ if _, _, err := sm.StartPendingChatRunWithClientRequest(
+ "request-1",
+ "conversation-1",
+ "client-submit-1",
+ ); err != nil {
+ t.Fatalf("StartPendingChatRunWithClientRequest: %v", err)
+ }
+ sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
+ RequestId: "request-1",
+ Payload: &gatewayv1.AgentEnvelope_ChatEvent{
+ ChatEvent: &gatewayv1.ChatEvent{
+ Type: gatewayv1.ChatEvent_ERROR,
+ ConversationId: "conversation-1",
+ Data: `{"message":"startup failed"}`,
+ },
+ },
+ })
+ sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
+ RequestId: "request-1",
+ Payload: &gatewayv1.AgentEnvelope_ChatControl{
+ ChatControl: &gatewayv1.ChatControlEvent{
+ Type: "completed",
+ State: session.ChatRunStateCompleted,
+ RequestId: "request-1",
+ ConversationId: "conversation-1",
+ },
+ },
+ })
+ sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
+ RequestId: "request-1",
+ Payload: &gatewayv1.AgentEnvelope_ChatEvent{
+ ChatEvent: &gatewayv1.ChatEvent{
+ Type: gatewayv1.ChatEvent_TOKEN,
+ ConversationId: "conversation-1",
+ Data: `{"text":"late token"}`,
+ },
+ },
+ })
+
+ ch, done, cleanup, snapshot, err := sm.SubscribeChatRun("request-1", "conversation-1", 0)
+ if err != nil {
+ t.Fatalf("SubscribeChatRun: %v", err)
+ }
+ defer cleanup()
+ assertDoneOpen(t, done)
+ if snapshot.State != session.ChatRunStateFailed {
+ t.Fatalf("terminal state = %q, want %q", snapshot.State, session.ChatRunStateFailed)
+ }
+
+ select {
+ case event := <-ch:
+ if event.Event == nil || event.Event.GetType() != gatewayv1.ChatEvent_ERROR {
+ t.Fatalf("replayed event = %#v, want ERROR", event)
+ }
+ case <-time.After(time.Second):
+ t.Fatalf("timed out waiting for replayed error event")
+ }
+ select {
+ case event := <-ch:
+ t.Fatalf("terminal completion control should be ignored after failure: %#v", event)
+ default:
+ }
+}
+
func TestDesktopBroadcastChatEventCreatesAttachableRun(t *testing.T) {
t.Parallel()
diff --git a/crates/agent-gateway/test/upload/import_readable_files_test.go b/crates/agent-gateway/test/upload/import_readable_files_test.go
index 7b26d803a..d25c08480 100644
--- a/crates/agent-gateway/test/upload/import_readable_files_test.go
+++ b/crates/agent-gateway/test/upload/import_readable_files_test.go
@@ -65,7 +65,8 @@ func TestImportReadableFilesForwardsMultipartToAgent(t *testing.T) {
var outbound *gatewayv1.GatewayEnvelope
select {
- case outbound = <-agentSession.Outbound():
+ case delivered := <-agentSession.Outbound():
+ outbound = delivered.GatewayEnvelope
case <-time.After(time.Second):
t.Fatalf("timed out waiting for upload request to reach agent")
}
diff --git a/crates/agent-gateway/test/websocket/chat_bridge_test.go b/crates/agent-gateway/test/websocket/chat_bridge_test.go
index 194c9c0bc..f644ad2c3 100644
--- a/crates/agent-gateway/test/websocket/chat_bridge_test.go
+++ b/crates/agent-gateway/test/websocket/chat_bridge_test.go
@@ -77,6 +77,17 @@ func receiveEnvelopeWithID(t *testing.T, conn *websocket.Conn, id string) wsEnve
return wsEnvelope{}
}
+func assertNoEnvelopeWithin(t *testing.T, conn *websocket.Conn, timeout time.Duration) {
+ t.Helper()
+ if err := conn.SetDeadline(time.Now().Add(timeout)); err != nil {
+ t.Fatalf("set websocket deadline: %v", err)
+ }
+ var env wsEnvelope
+ if err := websocket.JSON.Receive(conn, &env); err == nil {
+ t.Fatalf("unexpected websocket envelope: %#v", env)
+ }
+}
+
func authWebSocket(t *testing.T, conn *websocket.Conn, token string) {
t.Helper()
sendEnvelope(t, conn, "auth-1", "auth", map[string]any{"token": token})
@@ -90,13 +101,39 @@ func readOutboundEnvelope(t *testing.T, agentSession *session.AgentSession) *gat
t.Helper()
select {
case outbound := <-agentSession.Outbound():
- return outbound
+ outbound.Ack(nil)
+ return outbound.GatewayEnvelope
case <-time.After(time.Second):
t.Fatalf("timed out waiting for gateway request to reach agent")
return nil
}
}
+func dispatchChatStarted(t *testing.T, sm *session.Manager, requestID string, conversationID string) {
+ t.Helper()
+ sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
+ RequestId: requestID,
+ Timestamp: time.Now().Unix(),
+ Payload: &gatewayv1.AgentEnvelope_ChatEvent{
+ ChatEvent: &gatewayv1.ChatEvent{
+ Type: gatewayv1.ChatEvent_TOKEN,
+ ConversationId: conversationID,
+ Data: `{"type":"started"}`,
+ },
+ },
+ })
+}
+
+func markRuntimeReady(sm *session.Manager, agentSession *session.AgentSession) {
+ sm.UpdateRuntimeStatus(agentSession, &gatewayv1.RuntimeStatusEvent{
+ WorkerId: "test-runtime",
+ State: "ready",
+ Visible: true,
+ ActiveRunCount: 0,
+ Timestamp: time.Now().Unix(),
+ })
+}
+
func TestWebSocketRejectsRequestsBeforeAuth(t *testing.T) {
t.Parallel()
@@ -121,6 +158,7 @@ func TestWebSocketChatStartForwardsNormalizedRequestAndStreamsEvents(t *testing.
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
handler := server.NewWebSocketServer(&config.Config{
Token: "ws-token",
@@ -184,7 +222,9 @@ func TestWebSocketChatStartForwardsNormalizedRequestAndStreamsEvents(t *testing.
var outbound *gatewayv1.GatewayEnvelope
select {
- case outbound = <-agentSession.Outbound():
+ case delivered := <-agentSession.Outbound():
+ delivered.Ack(nil)
+ outbound = delivered.GatewayEnvelope
case <-time.After(time.Second):
t.Fatalf("timed out waiting for chat request to reach agent")
}
@@ -278,6 +318,351 @@ func TestWebSocketChatStartForwardsNormalizedRequestAndStreamsEvents(t *testing.
}
}
+func TestWebSocketChatStartRequiresRuntimeReady(t *testing.T) {
+ t.Parallel()
+
+ sm := session.NewManager()
+ sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
+ agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(agentSession)
+
+ handler := server.NewWebSocketServer(&config.Config{
+ Token: "ws-token",
+ RequestTimeout: time.Second,
+ }, sm)
+ conn, cleanup := dialGatewayWebSocket(t, handler)
+ defer cleanup()
+
+ authWebSocket(t, conn, "ws-token")
+ sendEnvelope(t, conn, "chat-not-ready", "chat.start", map[string]any{
+ "conversation_id": "conversation-not-ready",
+ "message": "hello gateway",
+ })
+
+ env := receiveEnvelope(t, conn)
+ if env.ID != "chat-not-ready" ||
+ env.Type != "error" ||
+ !strings.Contains(env.Error, "Desktop chat runtime is not ready") {
+ t.Fatalf("not-ready response = %#v, want chat runtime readiness error", env)
+ }
+ select {
+ case outbound := <-agentSession.Outbound():
+ t.Fatalf("unexpected outbound request while runtime not ready: %#v", outbound)
+ case <-time.After(100 * time.Millisecond):
+ }
+ if got := sm.ActiveChatRunConversationIDs(); len(got) != 0 {
+ t.Fatalf("active chat runs after not-ready request = %#v, want empty", got)
+ }
+}
+
+func TestWebSocketChatStartClearsRunWhenAgentDeliveryStalls(t *testing.T) {
+ t.Parallel()
+
+ sm := session.NewManager()
+ sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
+ agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
+
+ handler := server.NewWebSocketServer(&config.Config{
+ Token: "ws-token",
+ RequestTimeout: time.Second,
+ WebSocketWriteTimeout: 50 * time.Millisecond,
+ }, sm)
+ conn, cleanup := dialGatewayWebSocket(t, handler)
+ defer cleanup()
+
+ authWebSocket(t, conn, "ws-token")
+ sendEnvelope(t, conn, "chat-stalled", "chat.start", map[string]any{
+ "conversation_id": "conversation-stalled",
+ "message": "hello gateway",
+ })
+
+ select {
+ case outbound := <-agentSession.Outbound():
+ if outbound.GetChatRequest() == nil {
+ t.Fatalf("outbound payload = %T, want ChatRequest", outbound.GetPayload())
+ }
+ case <-time.After(time.Second):
+ t.Fatalf("timed out waiting for chat request to be enqueued")
+ }
+
+ env := receiveEnvelope(t, conn)
+ if env.ID != "chat-stalled" || env.Type != "error" || env.Error != "request timed out" {
+ t.Fatalf("stalled delivery response = %#v, want timeout error", env)
+ }
+ if got := sm.ActiveChatRunConversationIDs(); len(got) != 0 {
+ t.Fatalf("active chat runs after stalled delivery = %#v, want empty", got)
+ }
+}
+
+func TestWebSocketChatStartClearsRunWhenDesktopDoesNotAccept(t *testing.T) {
+ t.Parallel()
+
+ sm := session.NewManager()
+ sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
+ agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
+
+ handler := server.NewWebSocketServer(&config.Config{
+ Token: "ws-token",
+ RequestTimeout: time.Second,
+ WebSocketWriteTimeout: time.Second,
+ ChatStartTimeout: 50 * time.Millisecond,
+ }, sm)
+ conn, cleanup := dialGatewayWebSocket(t, handler)
+ defer cleanup()
+
+ authWebSocket(t, conn, "ws-token")
+ sendEnvelope(t, conn, "chat-unaccepted", "chat.start", map[string]any{
+ "conversation_id": "conversation-unaccepted",
+ "message": "hello gateway",
+ })
+
+ outbound := readOutboundEnvelope(t, agentSession)
+ if outbound.GetChatRequest() == nil {
+ t.Fatalf("outbound payload = %T, want ChatRequest", outbound.GetPayload())
+ }
+ if got := sm.ActiveChatRunConversationIDs(); len(got) != 0 {
+ t.Fatalf("active chat runs before desktop accept = %#v, want empty", got)
+ }
+
+ env := receiveEnvelope(t, conn)
+ if env.ID != "chat-unaccepted" || env.Type != "chat.event" {
+ t.Fatalf("unaccepted desktop response = %#v, want chat.event", env)
+ }
+ var payload map[string]any
+ if err := json.Unmarshal(env.Payload, &payload); err != nil {
+ t.Fatalf("decode unaccepted desktop payload: %v", err)
+ }
+ if payload["type"] != "error" ||
+ payload["conversation_id"] != "conversation-unaccepted" ||
+ !strings.Contains(fmt.Sprint(payload["message"]), "Desktop backend did not accept") {
+ t.Fatalf("unaccepted desktop payload = %#v", payload)
+ }
+ if got := sm.ActiveChatRunConversationIDs(); len(got) != 0 {
+ t.Fatalf("active chat runs after unaccepted desktop request = %#v, want empty", got)
+ }
+ if status := sm.Status(); status.Online {
+ t.Fatalf("status online = true after desktop failed to accept chat request")
+ }
+}
+
+func TestWebSocketChatStartAcceptedByDesktopDoesNotTripStartTimeout(t *testing.T) {
+ t.Parallel()
+
+ sm := session.NewManager()
+ sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
+ agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
+
+ handler := server.NewWebSocketServer(&config.Config{
+ Token: "ws-token",
+ RequestTimeout: time.Second,
+ WebSocketWriteTimeout: time.Second,
+ ChatStartTimeout: 50 * time.Millisecond,
+ }, sm)
+ conn, cleanup := dialGatewayWebSocket(t, handler)
+ defer cleanup()
+
+ authWebSocket(t, conn, "ws-token")
+ sendEnvelope(t, conn, "chat-accepted", "chat.start", map[string]any{
+ "conversation_id": "conversation-accepted",
+ "message": "hello gateway",
+ })
+
+ outbound := readOutboundEnvelope(t, agentSession)
+ if outbound.GetChatRequest() == nil {
+ t.Fatalf("outbound payload = %T, want ChatRequest", outbound.GetPayload())
+ }
+ sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
+ RequestId: outbound.GetRequestId(),
+ Timestamp: time.Now().Unix(),
+ Payload: &gatewayv1.AgentEnvelope_ChatEvent{
+ ChatEvent: &gatewayv1.ChatEvent{
+ Type: gatewayv1.ChatEvent_TOKEN,
+ ConversationId: "conversation-accepted",
+ Data: `{"type":"accepted"}`,
+ },
+ },
+ })
+
+ if got := sm.ActiveChatRunConversationIDs(); len(got) != 0 {
+ t.Fatalf("active chat runs after desktop accept before start = %#v, want empty", got)
+ }
+ assertNoEnvelopeWithin(t, conn, 120*time.Millisecond)
+}
+
+func TestWebSocketChatStartFailsWhenDesktopAcceptsButDoesNotStart(t *testing.T) {
+ t.Parallel()
+
+ sm := session.NewManager()
+ sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
+ agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
+
+ handler := server.NewWebSocketServer(&config.Config{
+ Token: "ws-token",
+ RequestTimeout: time.Second,
+ WebSocketWriteTimeout: time.Second,
+ ChatStartTimeout: 25 * time.Millisecond,
+ ChatRenderStartTimeout: 75 * time.Millisecond,
+ }, sm)
+ conn, cleanup := dialGatewayWebSocket(t, handler)
+ defer cleanup()
+
+ authWebSocket(t, conn, "ws-token")
+ sendEnvelope(t, conn, "chat-render-stalled", "chat.start", map[string]any{
+ "conversation_id": "conversation-render-stalled",
+ "message": "hello gateway",
+ })
+
+ outbound := readOutboundEnvelope(t, agentSession)
+ if outbound.GetChatRequest() == nil {
+ t.Fatalf("outbound payload = %T, want ChatRequest", outbound.GetPayload())
+ }
+ sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
+ RequestId: outbound.GetRequestId(),
+ Timestamp: time.Now().Unix(),
+ Payload: &gatewayv1.AgentEnvelope_ChatEvent{
+ ChatEvent: &gatewayv1.ChatEvent{
+ Type: gatewayv1.ChatEvent_TOKEN,
+ ConversationId: "conversation-render-stalled",
+ Data: `{"type":"accepted"}`,
+ },
+ },
+ })
+
+ env := receiveEnvelope(t, conn)
+ if env.ID != "chat-render-stalled" || env.Type != "chat.event" {
+ t.Fatalf("render-stalled response = %#v, want chat.event", env)
+ }
+ var payload map[string]any
+ if err := json.Unmarshal(env.Payload, &payload); err != nil {
+ t.Fatalf("decode render-stalled payload: %v", err)
+ }
+ if payload["type"] != "error" ||
+ payload["conversation_id"] != "conversation-render-stalled" ||
+ !strings.Contains(fmt.Sprint(payload["message"]), "Desktop app accepted") {
+ t.Fatalf("render-stalled payload = %#v", payload)
+ }
+}
+
+func TestWebSocketChatResumeFailsPendingRunWhenDesktopStillDoesNotAccept(t *testing.T) {
+ t.Parallel()
+
+ sm := session.NewManager()
+ sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
+ agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
+
+ handler := server.NewWebSocketServer(&config.Config{
+ Token: "ws-token",
+ RequestTimeout: time.Second,
+ WebSocketWriteTimeout: time.Second,
+ ChatStartTimeout: 50 * time.Millisecond,
+ }, sm)
+ conn1, cleanup1 := dialGatewayWebSocket(t, handler)
+ defer cleanup1()
+
+ authWebSocket(t, conn1, "ws-token")
+ sendEnvelope(t, conn1, "chat-pending", "chat.start", map[string]any{
+ "conversation_id": "conversation-pending",
+ "message": "hello gateway",
+ })
+ outbound := readOutboundEnvelope(t, agentSession)
+ if outbound.GetChatRequest() == nil {
+ t.Fatalf("outbound payload = %T, want ChatRequest", outbound.GetPayload())
+ }
+ _ = conn1.Close()
+
+ conn2, cleanup2 := dialGatewayWebSocket(t, handler)
+ defer cleanup2()
+ authWebSocket(t, conn2, "ws-token")
+ sendEnvelope(t, conn2, "resume-pending", "chat.resume", map[string]any{
+ "request_id": outbound.GetRequestId(),
+ "conversation_id": "conversation-pending",
+ })
+
+ env := receiveEnvelope(t, conn2)
+ if env.ID != outbound.GetRequestId() || env.Type != "chat.event" {
+ t.Fatalf("resume pending response = %#v, want chat.event", env)
+ }
+ var payload map[string]any
+ if err := json.Unmarshal(env.Payload, &payload); err != nil {
+ t.Fatalf("decode resume pending payload: %v", err)
+ }
+ if payload["type"] != "error" || !strings.Contains(fmt.Sprint(payload["message"]), "Desktop backend did not accept") {
+ t.Fatalf("resume pending payload = %#v", payload)
+ }
+ if status := sm.Status(); status.Online {
+ t.Fatalf("status online = true after resumed pending chat was not accepted")
+ }
+}
+
+func TestWebSocketChatResumeFailsAcceptedRunWhenDesktopDoesNotStart(t *testing.T) {
+ t.Parallel()
+
+ sm := session.NewManager()
+ sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
+ agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
+ sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
+
+ handler := server.NewWebSocketServer(&config.Config{
+ Token: "ws-token",
+ RequestTimeout: time.Second,
+ WebSocketWriteTimeout: time.Second,
+ ChatStartTimeout: 25 * time.Millisecond,
+ ChatRenderStartTimeout: 75 * time.Millisecond,
+ }, sm)
+ conn1, cleanup1 := dialGatewayWebSocket(t, handler)
+ defer cleanup1()
+
+ authWebSocket(t, conn1, "ws-token")
+ sendEnvelope(t, conn1, "chat-accepted-pending", "chat.start", map[string]any{
+ "conversation_id": "conversation-accepted-pending",
+ "message": "hello gateway",
+ })
+ outbound := readOutboundEnvelope(t, agentSession)
+ sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
+ RequestId: outbound.GetRequestId(),
+ Timestamp: time.Now().Unix(),
+ Payload: &gatewayv1.AgentEnvelope_ChatEvent{
+ ChatEvent: &gatewayv1.ChatEvent{
+ Type: gatewayv1.ChatEvent_TOKEN,
+ ConversationId: "conversation-accepted-pending",
+ Data: `{"type":"accepted"}`,
+ },
+ },
+ })
+ _ = conn1.Close()
+
+ conn2, cleanup2 := dialGatewayWebSocket(t, handler)
+ defer cleanup2()
+ authWebSocket(t, conn2, "ws-token")
+ sendEnvelope(t, conn2, "resume-accepted-pending", "chat.resume", map[string]any{
+ "request_id": outbound.GetRequestId(),
+ "conversation_id": "conversation-accepted-pending",
+ })
+
+ env := receiveEnvelope(t, conn2)
+ if env.ID != outbound.GetRequestId() || env.Type != "chat.event" {
+ t.Fatalf("resume accepted pending response = %#v, want chat.event", env)
+ }
+ var payload map[string]any
+ if err := json.Unmarshal(env.Payload, &payload); err != nil {
+ t.Fatalf("decode resume accepted pending payload: %v", err)
+ }
+ if payload["type"] != "error" || !strings.Contains(fmt.Sprint(payload["message"]), "Desktop app accepted") {
+ t.Fatalf("resume accepted pending payload = %#v", payload)
+ }
+}
+
func TestWebSocketChatStartDedupesClientRequestID(t *testing.T) {
t.Parallel()
@@ -285,6 +670,7 @@ func TestWebSocketChatStartDedupesClientRequestID(t *testing.T) {
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
handler := server.NewWebSocketServer(&config.Config{
Token: "ws-token",
@@ -366,6 +752,7 @@ func TestWebSocketChatStartFailsWhenAgentSessionDisconnects(t *testing.T) {
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
handler := server.NewWebSocketServer(&config.Config{
Token: "ws-token",
@@ -406,6 +793,7 @@ func TestWebSocketChatStartFailsWhenAgentSessionDisconnects(t *testing.T) {
replacementSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(replacementSession)
+ markRuntimeReady(sm, replacementSession)
sendEnvelope(t, conn, "chat-2", "chat.start", map[string]any{
"conversation_id": "conversation-1",
"client_request_id": "client-submit-1",
@@ -424,6 +812,7 @@ func TestWebSocketMemoryManageForwardsJSONArgs(t *testing.T) {
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
handler := server.NewWebSocketServer(&config.Config{
Token: "ws-token",
@@ -487,6 +876,7 @@ func TestWebSocketChatResumeReplaysEventsAfterReconnect(t *testing.T) {
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
handler := server.NewWebSocketServer(&config.Config{
Token: "ws-token",
@@ -596,6 +986,7 @@ func TestWebSocketChatAttachReplaysBufferedEventsByConversationID(t *testing.T)
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
handler := server.NewWebSocketServer(&config.Config{
Token: "ws-token",
@@ -686,6 +1077,7 @@ func TestWebSocketChatAttachExpiresAfterDoneHistoryUpsert(t *testing.T) {
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
handler := server.NewWebSocketServer(&config.Config{
Token: "ws-token",
@@ -751,6 +1143,7 @@ func TestWebSocketChatCancelReleasesBufferedAttachRun(t *testing.T) {
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
handler := server.NewWebSocketServer(&config.Config{
Token: "ws-token",
@@ -810,6 +1203,7 @@ func TestWebSocketForwardsHistorySettingsAndFsRPCs(t *testing.T) {
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
handler := server.NewWebSocketServer(&config.Config{
Token: "ws-token",
@@ -828,6 +1222,7 @@ func TestWebSocketForwardsHistorySettingsAndFsRPCs(t *testing.T) {
if chatOutbound.GetChatRequest() == nil {
t.Fatalf("chat outbound payload = %T, want ChatRequest", chatOutbound.GetPayload())
}
+ dispatchChatStarted(t, sm, chatOutbound.GetRequestId(), "conversation-1")
sendEnvelope(t, conn, "history-1", "history.list", map[string]any{
"page": 2,
@@ -1275,6 +1670,7 @@ func TestWebSocketDefaultsInvalidHistoryListPagination(t *testing.T) {
sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1")
agentSession := session.NewAgentSession(sm.LatestAuthSnapshot())
sm.SetSession(agentSession)
+ markRuntimeReady(sm, agentSession)
handler := server.NewWebSocketServer(&config.Config{
Token: "ws-token",
diff --git a/crates/agent-gateway/test/webui/gateway-socket-client.test.mjs b/crates/agent-gateway/test/webui/gateway-socket-client.test.mjs
index 43e511971..7905c433e 100644
--- a/crates/agent-gateway/test/webui/gateway-socket-client.test.mjs
+++ b/crates/agent-gateway/test/webui/gateway-socket-client.test.mjs
@@ -70,12 +70,44 @@ function installBrowser(options = {}) {
FakeWebSocket.instances = [];
globalThis.WebSocket = FakeWebSocket;
delete globalThis.SharedWorker;
+ const windowListeners = new Map();
+ const documentListeners = new Map();
+ const addListener = (listeners, type, listener) => {
+ const items = listeners.get(type) ?? new Set();
+ items.add(listener);
+ listeners.set(type, items);
+ };
+ const removeListener = (listeners, type, listener) => {
+ listeners.get(type)?.delete(listener);
+ };
+ const dispatch = (listeners, event) => {
+ const type = event?.type;
+ if (typeof type !== "string") return;
+ for (const listener of listeners.get(type) ?? []) {
+ listener(event);
+ }
+ };
globalThis.window = {
location: { origin: "https://gateway.example" },
setTimeout: options.setTimeout ?? setTimeout,
clearTimeout: options.clearTimeout ?? clearTimeout,
setInterval: options.setInterval ?? setInterval,
clearInterval: options.clearInterval ?? clearInterval,
+ addEventListener: (type, listener) => addListener(windowListeners, type, listener),
+ removeEventListener: (type, listener) => removeListener(windowListeners, type, listener),
+ dispatchEvent: (event) => {
+ dispatch(windowListeners, event);
+ return true;
+ },
+ };
+ globalThis.document = {
+ visibilityState: options.visibilityState ?? "visible",
+ addEventListener: (type, listener) => addListener(documentListeners, type, listener),
+ removeEventListener: (type, listener) => removeListener(documentListeners, type, listener),
+ dispatchEvent: (event) => {
+ dispatch(documentListeners, event);
+ return true;
+ },
};
}
@@ -216,6 +248,35 @@ test("SharedWorker gateway client sends conversation cancel even without a local
resetGatewayWebSocketClient();
});
+test("SharedWorker gateway client forwards foreground wakeups to the worker", async () => {
+ installBrowser();
+ FakeSharedWorker.instances = [];
+ globalThis.SharedWorker = FakeSharedWorker;
+ const loader = createWebModuleLoader();
+ const { getGatewayWebSocketClient, resetGatewayWebSocketClient } = loader.loadModule("src/lib/gatewaySocket.ts");
+ resetGatewayWebSocketClient();
+
+ getGatewayWebSocketClient(" token ");
+ assert.equal(FakeSharedWorker.instances.length, 1);
+ const port = FakeSharedWorker.instances[0].port;
+ const connect = port.messages.find((message) => message.type === "connect");
+ assert.ok(connect);
+ port.emit({
+ type: "ready",
+ connection_id: connect.connection_id,
+ payload: { status: { online: true }, error: null },
+ });
+
+ window.dispatchEvent({ type: "pageshow" });
+
+ assert.deepEqual(port.messages.at(-1), {
+ type: "wakeup",
+ connection_id: connect.connection_id,
+ });
+
+ resetGatewayWebSocketClient();
+});
+
test("SharedWorker gateway client accepts terminal list sessions from worker payload", async () => {
installBrowser();
FakeSharedWorker.instances = [];
@@ -468,6 +529,69 @@ test("Gateway SharedWorker broadcasts events with each port connection id", asyn
globalThis.onconnect = previousOnConnect;
});
+test("Gateway SharedWorker applies foreground wakeups to the managed socket client", async () => {
+ installBrowser();
+ const loader = createWebModuleLoader();
+ const gatewaySocketPath = loader.resolveLocal("src/lib/gatewaySocket.ts");
+ const clientInstances = [];
+
+ class MockGatewayWebSocketClient {
+ wakeups = 0;
+
+ constructor(token) {
+ this.token = token;
+ clientInstances.push(this);
+ }
+
+ subscribeStatus() {
+ return () => {};
+ }
+
+ subscribeHistory() {
+ return () => {};
+ }
+
+ subscribeConversation() {
+ return () => {};
+ }
+
+ subscribeSettings() {
+ return () => {};
+ }
+
+ subscribeTerminal() {
+ return () => {};
+ }
+
+ noteForegroundWakeup() {
+ this.wakeups += 1;
+ }
+
+ dispose() {}
+ }
+
+ const workerLoader = createWebModuleLoader({
+ mocks: {
+ [gatewaySocketPath]: {
+ GatewayWebSocketClient: MockGatewayWebSocketClient,
+ },
+ },
+ });
+
+ const previousOnConnect = globalThis.onconnect;
+ workerLoader.loadModule("src/lib/gatewaySocket.worker.ts");
+
+ const port = new FakeMessagePort();
+ globalThis.onconnect({ ports: [port] });
+ port.emit({ type: "connect", connection_id: "connection-1", token: "token" });
+ port.emit({ type: "wakeup", connection_id: "connection-1" });
+
+ assert.equal(clientInstances.length, 1);
+ assert.equal(clientInstances[0].wakeups, 1);
+
+ globalThis.onconnect = previousOnConnect;
+});
+
test("Gateway SharedWorker terminal metadata reaches every page while output stays scoped", async () => {
installBrowser();
const loader = createWebModuleLoader();
@@ -1618,6 +1742,208 @@ test("Gateway SharedWorker forwards history share requests", async () => {
globalThis.onconnect = previousOnConnect;
});
+test("Gateway SharedWorker forwards tunnel requests", async () => {
+ installBrowser();
+ const loader = createWebModuleLoader();
+ const gatewaySocketPath = loader.resolveLocal("src/lib/gatewaySocket.ts");
+ const clientInstances = [];
+
+ class MockGatewayWebSocketClient {
+ calls = [];
+
+ constructor(token) {
+ this.token = token;
+ clientInstances.push(this);
+ }
+
+ subscribeStatus() {
+ return () => {};
+ }
+
+ subscribeHistory() {
+ return () => {};
+ }
+
+ subscribeConversation() {
+ return () => {};
+ }
+
+ subscribeSettings() {
+ return () => {};
+ }
+
+ subscribeTerminal() {
+ return () => {};
+ }
+
+ listTunnels() {
+ this.calls.push(["listTunnels"]);
+ return [
+ {
+ id: "tun-1",
+ slug: "slug-1",
+ name: "App",
+ targetUrl: "http://localhost:3000",
+ publicUrl: "https://gateway.example/t/slug-1/",
+ createdAt: 10,
+ expiresAt: 3700,
+ activeConnections: 0,
+ status: "active",
+ },
+ ];
+ }
+
+ createTunnel(input) {
+ this.calls.push(["createTunnel", input]);
+ return {
+ id: "tun-2",
+ slug: "slug-2",
+ name: input.name ?? "",
+ targetUrl: input.targetUrl,
+ publicUrl: "https://gateway.example/t/slug-2/",
+ createdAt: 20,
+ expiresAt: 920,
+ activeConnections: 0,
+ status: "active",
+ };
+ }
+
+ updateTunnel(input) {
+ this.calls.push(["updateTunnel", input]);
+ return {
+ id: input.id,
+ slug: "slug-2",
+ name: input.name ?? "",
+ targetUrl: input.targetUrl,
+ publicUrl: "https://gateway.example/t/slug-2/",
+ createdAt: 20,
+ expiresAt: input.ttlSeconds === 0 ? 0 : 920,
+ activeConnections: 0,
+ status: "active",
+ projectPathKey: input.projectPathKey ?? "",
+ };
+ }
+
+ closeTunnel(id) {
+ this.calls.push(["closeTunnel", id]);
+ return {
+ id,
+ slug: "slug-2",
+ name: "Closed",
+ targetUrl: "http://localhost:3000",
+ publicUrl: "https://gateway.example/t/slug-2/",
+ createdAt: 20,
+ expiresAt: 920,
+ activeConnections: 0,
+ status: "expired",
+ };
+ }
+
+ dispose() {}
+ }
+
+ const workerLoader = createWebModuleLoader({
+ mocks: {
+ [gatewaySocketPath]: {
+ GatewayWebSocketClient: MockGatewayWebSocketClient,
+ },
+ },
+ });
+
+ const previousOnConnect = globalThis.onconnect;
+ workerLoader.loadModule("src/lib/gatewaySocket.worker.ts");
+
+ const port = new FakeMessagePort();
+ globalThis.onconnect({ ports: [port] });
+ port.emit({ type: "connect", connection_id: "connection-1", token: " token " });
+ assert.equal(clientInstances.length, 1);
+
+ port.emit({
+ type: "request",
+ connection_id: "connection-1",
+ request_id: "tunnel-list",
+ method: "tunnel.list",
+ payload: {},
+ });
+ await waitFor(
+ () => port.messages.some((message) => message.request_id === "tunnel-list"),
+ "shared worker tunnel list response",
+ );
+ assert.deepEqual(clientInstances[0].calls.at(-1), ["listTunnels"]);
+ assert.equal(port.messages.at(-1).payload.tunnels[0].id, "tun-1");
+
+ port.emit({
+ type: "request",
+ connection_id: "connection-1",
+ request_id: "tunnel-create",
+ method: "tunnel.create",
+ payload: {
+ targetUrl: "http://localhost:3000/app",
+ ttlSeconds: 900,
+ name: "App",
+ },
+ });
+ await waitFor(
+ () => port.messages.some((message) => message.request_id === "tunnel-create"),
+ "shared worker tunnel create response",
+ );
+ assert.deepEqual(clientInstances[0].calls.at(-1), [
+ "createTunnel",
+ {
+ targetUrl: "http://localhost:3000/app",
+ ttlSeconds: 900,
+ name: "App",
+ },
+ ]);
+ assert.equal(port.messages.at(-1).payload.tunnel.id, "tun-2");
+
+ port.emit({
+ type: "request",
+ connection_id: "connection-1",
+ request_id: "tunnel-update-infinite",
+ method: "tunnel.update",
+ payload: {
+ id: "tun-2",
+ targetUrl: "http://localhost:4000/dashboard",
+ ttlSeconds: 0,
+ name: "Dashboard",
+ projectPathKey: "project:/tmp/liveagent",
+ },
+ });
+ await waitFor(
+ () => port.messages.some((message) => message.request_id === "tunnel-update-infinite"),
+ "shared worker tunnel update response",
+ );
+ assert.deepEqual(clientInstances[0].calls.at(-1), [
+ "updateTunnel",
+ {
+ id: "tun-2",
+ targetUrl: "http://localhost:4000/dashboard",
+ ttlSeconds: 0,
+ name: "Dashboard",
+ projectPathKey: "project:/tmp/liveagent",
+ },
+ ]);
+ assert.equal(port.messages.at(-1).payload.tunnel.expiresAt, 0);
+ assert.equal(port.messages.at(-1).payload.tunnel.projectPathKey, "project:/tmp/liveagent");
+
+ port.emit({
+ type: "request",
+ connection_id: "connection-1",
+ request_id: "tunnel-close",
+ method: "tunnel.close",
+ payload: { id: "tun-2" },
+ });
+ await waitFor(
+ () => port.messages.some((message) => message.request_id === "tunnel-close"),
+ "shared worker tunnel close response",
+ );
+ assert.deepEqual(clientInstances[0].calls.at(-1), ["closeTunnel", "tun-2"]);
+ assert.equal(port.messages.at(-1).payload.tunnel.status, "expired");
+
+ globalThis.onconnect = previousOnConnect;
+});
+
test("Gateway SharedWorker forwards chat.attach streams to the requesting port", async () => {
installBrowser();
const loader = createWebModuleLoader();
@@ -2039,6 +2365,78 @@ test("GatewayWebSocketClient reconnects before read requests when an authenticat
}
});
+test("GatewayWebSocketClient reconnects before chat.start after a foreground restore", async () => {
+ installBrowser();
+ const loader = createWebModuleLoader();
+ const { getGatewayWebSocketClient, resetGatewayWebSocketClient } = loader.loadModule("src/lib/gatewaySocket.ts");
+ resetGatewayWebSocketClient();
+
+ const realDateNow = Date.now;
+ try {
+ const client = getGatewayWebSocketClient("token");
+ const statusPromise = client.getStatus();
+ const firstSocket = await connectAndAuth();
+ await waitFor(() => firstSocket.sent.some((item) => item.type === "status.get"), "initial status.get");
+ const statusRequest = firstSocket.sent.find((item) => item.type === "status.get");
+ firstSocket.receive({
+ id: statusRequest.id,
+ type: "response",
+ payload: { online: true, agent_id: "desktop-agent" },
+ });
+ await statusPromise;
+
+ let mockNow = realDateNow();
+ Date.now = () => mockNow;
+ mockNow += 12_000;
+ window.dispatchEvent({ type: "pageshow" });
+
+ const stream = client.chat("hello", "conversation-1");
+ const firstEventPromise = stream.next();
+ assert.equal(firstSocket.readyState, FakeWebSocket.CLOSED);
+ assert.equal(FakeWebSocket.instances.length, 2);
+
+ Date.now = realDateNow;
+
+ const reconnectSocket = FakeWebSocket.instances[1];
+ reconnectSocket.open();
+ await waitFor(() => reconnectSocket.sent.length >= 1, "foreground reconnect auth envelope");
+ reconnectSocket.receive({
+ id: reconnectSocket.sent[0].id,
+ type: "response",
+ payload: { ok: true },
+ });
+ await waitFor(
+ () => reconnectSocket.sent.some((item) => item.type === "chat.start"),
+ "chat.start after foreground reconnect",
+ );
+ const chatStart = reconnectSocket.sent.find((item) => item.type === "chat.start");
+ assert.deepEqual(chatStart.payload.conversation_id, "conversation-1");
+
+ reconnectSocket.receive({
+ id: chatStart.id,
+ type: "chat.control",
+ payload: {
+ type: "started",
+ state: "running",
+ conversation_id: "conversation-1",
+ seq: 1,
+ },
+ });
+ assert.deepEqual(await firstEventPromise, {
+ value: {
+ type: "started",
+ state: "running",
+ conversation_id: "conversation-1",
+ seq: 1,
+ },
+ done: false,
+ });
+ } finally {
+ Date.now = realDateNow;
+ resetGatewayWebSocketClient();
+ }
+});
+
test("GatewayWebSocketClient retries history.get after a recoverable transport stall timeout", async () => {
const realSetTimeout = setTimeout;
installBrowser({
@@ -2089,6 +2487,61 @@ test("GatewayWebSocketClient retries history.get after a recoverable transport s
resetGatewayWebSocketClient();
});
+test("GatewayWebSocketClient recovers chat.start when the socket stops receiving inbound traffic", async () => {
+ const realSetTimeout = setTimeout;
+ installBrowser({
+ setTimeout: (fn, delay, ...args) =>
+ realSetTimeout(fn, delay >= 8_000 ? 0 : delay, ...args),
+ });
+ const loader = createWebModuleLoader();
+ const { getGatewayWebSocketClient, resetGatewayWebSocketClient } = loader.loadModule("src/lib/gatewaySocket.ts");
+ resetGatewayWebSocketClient();
+
+ const client = getGatewayWebSocketClient("token");
+ const stream = client.chat("hello", "conversation-1");
+ const firstEventPromise = stream.next();
+ const firstSocket = await connectAndAuth(0);
+ await waitFor(() => firstSocket.sent.some((item) => item.type === "chat.start"), "chat.start envelope");
+ const chatStart = firstSocket.sent.find((item) => item.type === "chat.start");
+
+ await waitFor(() => FakeWebSocket.instances.length === 2, "chat.start transport recovery websocket");
+ const reconnectSocket = FakeWebSocket.instances[1];
+ reconnectSocket.open();
+ await waitFor(() => reconnectSocket.sent.length >= 1, "chat.start recovery auth envelope");
+ reconnectSocket.receive({
+ id: reconnectSocket.sent[0].id,
+ type: "response",
+ payload: { ok: true },
+ });
+
+ await waitFor(
+ () => reconnectSocket.sent.some((item) => item.type === "chat.resume"),
+ "chat.start recovery resume envelope",
+ );
+ const resume = reconnectSocket.sent.find((item) => item.type === "chat.resume");
+ assert.deepEqual(resume.payload, {
+ request_id: chatStart.id,
+ conversation_id: "conversation-1",
+ after_seq: 0,
+ });
+
+ reconnectSocket.receive({
+ id: chatStart.id,
+ type: "error",
+ error: "chat run not found",
+ });
+ assert.deepEqual(await firstEventPromise, {
+ value: {
+ type: "error",
+ message: "chat run not found",
+ conversation_id: "conversation-1",
+ },
+ done: false,
+ });
+ assert.deepEqual(await stream.next(), { value: undefined, done: true });
+ resetGatewayWebSocketClient();
+});
+
test("GatewayWebSocketClient does not open chat streams for pre-aborted signals", async () => {
installBrowser();
const loader = createWebModuleLoader();
diff --git a/crates/agent-gateway/test/webui/web-settings.test.mjs b/crates/agent-gateway/test/webui/web-settings.test.mjs
index 5f28d7086..60708b2e0 100644
--- a/crates/agent-gateway/test/webui/web-settings.test.mjs
+++ b/crates/agent-gateway/test/webui/web-settings.test.mjs
@@ -198,6 +198,10 @@ test("loadWebSettings forces current gateway URL/token over stale persisted remo
openProjectPathKeys: ["/stale/project"],
openVersion: 1,
};
+ stale.customSettings.projectToolsTunnel = {
+ openProjectPathKeys: ["/stale/project"],
+ openVersion: 1,
+ };
store.set("liveagent.gateway.webui.settings.v1", JSON.stringify(stale));
const loaded = webSettings.loadWebSettings(" new-token ");
@@ -207,6 +211,7 @@ test("loadWebSettings forces current gateway URL/token over stale persisted remo
assert.equal(loaded.remote.enabled, true);
assert.deepEqual(loaded.customSettings.projectToolsFileTree.openProjectPathKeys, []);
assert.deepEqual(loaded.customSettings.projectToolsGitReview.openProjectPathKeys, []);
+ assert.deepEqual(loaded.customSettings.projectToolsTunnel.openProjectPathKeys, []);
});
test("gateway settings sync keeps remote connection local and syncs web terminal setting", () => {
@@ -249,6 +254,7 @@ test("gateway settings sync keeps remote connection local and syncs web terminal
assert.deepEqual(payload.remote, {
enableWebTerminal: synced.remote.enableWebTerminal,
enableWebGit: synced.remote.enableWebGit,
+ enableWebTunnels: synced.remote.enableWebTunnels,
});
assert.deepEqual(payload.chatRuntimeControls, synced.chatRuntimeControls);
});
@@ -338,48 +344,116 @@ test("gateway settings sync preserves active workspace project by path when ids
assert.equal(synced.system.activeWorkspaceProjectId, "desktop-project-a");
});
-test("gateway settings sync keeps newer git review tab open state", () => {
+test("gateway settings sync keeps newer project tool tab open state", () => {
installWindow();
const current = settings.normalizeSettings({
customSettings: {
+ projectToolsPanel: {
+ width: 612,
+ activeTab: "gitReview",
+ activeTabs: {
+ "/web/project": "gitReview",
+ },
+ tabOrders: {
+ "/web/project": ["__git_review__", "__file_tree__"],
+ },
+ },
projectToolsGitReview: {
openProjectPathKeys: ["/web/project"],
openVersion: 2,
},
+ projectToolsTunnel: {
+ openProjectPathKeys: ["/web/project"],
+ openVersion: 2,
+ },
},
});
const staleSynced = settingsSync.applyGatewaySettingsSyncPayload(current, {
customSettings: {
+ projectToolsPanel: {
+ width: 360,
+ activeTab: "terminal",
+ tabOrders: {
+ "/desktop/project": ["terminal-1", "__file_tree__"],
+ },
+ },
projectToolsGitReview: {
openProjectPathKeys: [],
openVersion: 1,
},
+ projectToolsTunnel: {
+ openProjectPathKeys: [],
+ openVersion: 1,
+ },
},
});
assert.deepEqual(staleSynced.customSettings.projectToolsGitReview.openProjectPathKeys, [
"/web/project",
]);
assert.equal(staleSynced.customSettings.projectToolsGitReview.openVersion, 2);
+ assert.deepEqual(staleSynced.customSettings.projectToolsTunnel.openProjectPathKeys, [
+ "/web/project",
+ ]);
+ assert.equal(staleSynced.customSettings.projectToolsTunnel.openVersion, 2);
+ assert.equal(staleSynced.customSettings.projectToolsPanel.width, 612);
+ assert.equal(staleSynced.customSettings.projectToolsPanel.activeTab, "gitReview");
+ assert.deepEqual(staleSynced.customSettings.projectToolsPanel.activeTabs, {
+ "/web/project": "gitReview",
+ });
+ assert.deepEqual(staleSynced.customSettings.projectToolsPanel.tabOrders, {
+ "/web/project": ["__git_review__", "__file_tree__"],
+ });
const newerSynced = settingsSync.applyGatewaySettingsSyncPayload(staleSynced, {
customSettings: {
+ projectToolsPanel: {
+ width: 360,
+ activeTab: "tunnel",
+ activeTabs: {
+ "/desktop/project": "tunnel",
+ },
+ tabOrders: {
+ "/desktop/project": ["terminal-1", "__tunnel__"],
+ },
+ },
projectToolsGitReview: {
openProjectPathKeys: ["/desktop/project"],
openVersion: 3,
},
+ projectToolsTunnel: {
+ openProjectPathKeys: ["/desktop/project"],
+ openVersion: 3,
+ },
},
});
assert.deepEqual(newerSynced.customSettings.projectToolsGitReview.openProjectPathKeys, [
"/desktop/project",
]);
assert.equal(newerSynced.customSettings.projectToolsGitReview.openVersion, 3);
+ assert.deepEqual(newerSynced.customSettings.projectToolsTunnel.openProjectPathKeys, [
+ "/desktop/project",
+ ]);
+ assert.equal(newerSynced.customSettings.projectToolsTunnel.openVersion, 3);
+ assert.equal(newerSynced.customSettings.projectToolsPanel.width, 612);
+ assert.equal(newerSynced.customSettings.projectToolsPanel.activeTab, "gitReview");
+ assert.deepEqual(newerSynced.customSettings.projectToolsPanel.activeTabs, {
+ "/web/project": "gitReview",
+ });
+ assert.deepEqual(newerSynced.customSettings.projectToolsPanel.tabOrders, {
+ "/web/project": ["__git_review__", "__file_tree__"],
+ });
const payload = settingsSync.buildGatewaySettingsSyncPayload(newerSynced);
+ assert.equal(Object.hasOwn(payload.customSettings, "projectToolsPanel"), false);
assert.deepEqual(payload.customSettings.projectToolsGitReview, {
openProjectPathKeys: ["/desktop/project"],
openVersion: 3,
});
+ assert.deepEqual(payload.customSettings.projectToolsTunnel, {
+ openProjectPathKeys: ["/desktop/project"],
+ openVersion: 3,
+ });
});
test("gateway settings sync keeps newer project conversation activity", () => {
diff --git a/crates/agent-gateway/web/src/App.tsx b/crates/agent-gateway/web/src/App.tsx
index ba683e931..973d607b5 100644
--- a/crates/agent-gateway/web/src/App.tsx
+++ b/crates/agent-gateway/web/src/App.tsx
@@ -59,16 +59,18 @@ import { McpHubPage } from "@/pages/mcp-hub/McpHubPage";
import type { SectionId } from "@/pages/settings/types";
import { useChatSkills } from "@/pages/chat/useChatSkills";
import { mergeAlwaysEnabledSkillNames } from "@/lib/skills";
-import { buildModelOptions, sortHistoryItems } from "@/lib/chat/chatPageHelpers";
+import { buildModelOptions, sortHistoryItems, VIBING_STATUS } from "@/lib/chat/chatPageHelpers";
import { SettingsPage } from "@/pages/SettingsPage";
import {
findProviderModelConfig,
getChatRuntimeReasoningLevelsForProvider,
getProjectToolsFileTreeProjectState,
+ getProjectToolsPanelActiveTab,
getProjectToolsPanelTabOrder,
isAgentDevMode,
isProjectToolsFileTreeOpen,
isProjectToolsGitReviewOpen,
+ isProjectToolsTunnelOpen,
normalizeChatRuntimeControlsForProvider,
normalizeSettings,
removeProjectToolsProjectState,
@@ -79,6 +81,8 @@ import {
updateProjectToolsFileTreeProjectState,
updateProjectToolsFileTreeOpen,
updateProjectToolsGitReviewOpen,
+ updateProjectToolsTunnelOpen,
+ updateProjectToolsPanelActiveTab,
updateProjectToolsPanelTabOrder,
type AppSettings,
type ChatRuntimeControls,
@@ -107,6 +111,7 @@ import {
import type { TerminalSession } from "./lib/terminal/types";
import type {
AgentStatus,
+ ChatControlEvent,
ChatEvent,
ConversationSummary,
GatewayHistoryEvent,
@@ -379,6 +384,8 @@ const SECONDS_TIMESTAMP_MAX = 10_000_000_000;
const DRAFT_HISTORY_ADOPTION_WINDOW_MS = 30_000;
const LIVE_STREAM_HISTORY_REFRESH_SUPPRESS_MS = 30_000;
const PAGE_RESTORE_HISTORY_REFRESH_THROTTLE_MS = 900;
+const CHAT_RUNTIME_PREPARE_TIMEOUT_MS = 2_500;
+const CHAT_RUNTIME_FOREGROUND_PREPARE_TIMEOUT_MS = 1_500;
const DEFAULT_BROWSER_TITLE = "LiveAgent Gateway";
const NEW_CONVERSATION_BROWSER_TITLE = "LiveAgent";
const SHARED_HISTORY_BROWSER_TITLE = "分享会话";
@@ -477,6 +484,66 @@ function isTerminalChatEvent(event: ChatEvent) {
return event.type === "done" || event.type === "error";
}
+function isChatControlEvent(event: ChatEvent): event is ChatControlEvent {
+ switch (event.type) {
+ case "accepted":
+ case "delivered":
+ case "claimed":
+ case "starting":
+ case "started":
+ case "progress":
+ case "completed":
+ case "failed":
+ case "cancelled":
+ return true;
+ default:
+ return false;
+ }
+}
+
+function isTerminalChatControlEvent(event: ChatEvent) {
+ return (
+ isChatControlEvent(event) &&
+ (event.state === "completed" || event.state === "failed" || event.state === "cancelled")
+ );
+}
+
+function isRunningChatControlEvent(event: ChatEvent) {
+ return isChatControlEvent(event) && (event.state === "running" || event.type === "started");
+}
+
+type TunnelManagerToolChange = {
+ action: "create" | "close";
+ projectPathKey: string;
+};
+
+function asRecord(value: unknown): Record {
+ return value && typeof value === "object" && !Array.isArray(value)
+ ? (value as Record)
+ : {};
+}
+
+function readTunnelManagerToolChange(event: ChatEvent): TunnelManagerToolChange | null {
+ if (event.type !== "tool_result" || event.isError === true) {
+ return null;
+ }
+ const details = asRecord(event.details);
+ if (details.kind !== "tunnel_manager") {
+ return null;
+ }
+ const action = typeof details.action === "string" ? details.action.trim() : "";
+ if (action !== "create" && action !== "close") {
+ return null;
+ }
+ const tunnel = asRecord(details.tunnel);
+ const projectPathKey =
+ (typeof tunnel.projectPathKey === "string" ? tunnel.projectPathKey.trim() : "") ||
+ (typeof tunnel.project_path_key === "string" ? tunnel.project_path_key.trim() : "") ||
+ event.workdir?.trim() ||
+ "";
+ return { action, projectPathKey };
+}
+
function buildGatewaySelectedModel(
selectedModel: SelectedModel | undefined,
providers: ModelProviderSource[],
@@ -829,6 +896,7 @@ export default function App() {
const [isFileDropActive, setIsFileDropActive] = useState(false);
const [activeView, setActiveView] = useState<"chat" | "skills-hub" | "mcp-hub">("chat");
const [projectToolsPanelOpen, setProjectToolsPanelOpen] = useState(false);
+ const [tunnelRefreshToken, setTunnelRefreshToken] = useState(0);
const previousProjectToolsFileTreeOpenRef = useRef(false);
const [workspaceEditorMounted, setWorkspaceEditorMounted] = useState(false);
const [workspaceEditorOpen, setWorkspaceEditorOpen] = useState(false);
@@ -861,6 +929,7 @@ export default function App() {
const fileInputRef = useRef(null);
const conversationIdRef = useRef(conversationId);
const selectedHistoryIdRef = useRef(selectedHistoryId);
+ const statusRef = useRef(status);
const chatBusyRef = useRef(chatBusy);
const chatMessagesRef = useRef(chatMessages);
const chatErrorRef = useRef(chatError);
@@ -878,6 +947,7 @@ export default function App() {
const sharedHistoryItemsRef = useRef([]);
const sharedHistoryListRequestRef = useRef | null>(null);
const pendingUploadedFilesRef = useRef(pendingUploadedFiles);
+ const pendingUploadsByConversationRef = useRef