diff --git a/crates/agent-gateway/internal/kbrain/permission_rules.go b/crates/agent-gateway/internal/kbrain/permission_rules.go new file mode 100644 index 000000000..aff0b9296 --- /dev/null +++ b/crates/agent-gateway/internal/kbrain/permission_rules.go @@ -0,0 +1,138 @@ +package kbrain + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" +) + +// permissionRulesInput is everything the settings page may send; other fields +// (session_id, unknown keys) are dropped before K-brain sees the request. +type permissionRulesInput struct { + Workspace string `json:"workspace"` + Rule string `json:"rule"` + Effect string `json:"effect"` + Scope string `json:"scope"` + ID string `json:"id"` +} + +// PermissionRules serves Settings → Permissions for a direct K-brain target: list, +// add and delete remembered rules through /v1/permissions/rules. Session rules stay +// with their session and are not reachable here. +func (r *Relay) PermissionRules(ctx context.Context, req *gatewayv2.PermissionRulesRequest) (*gatewayv2.PermissionRulesResponse, error) { + if req == nil { + return nil, errors.New("invalid permission rules request") + } + var in permissionRulesInput + if raw := strings.TrimSpace(req.GetInputJson()); raw != "" { + if err := json.Unmarshal([]byte(raw), &in); err != nil { + return nil, errors.New("invalid permission rules request: input must be a JSON object") + } + } + workspace := strings.TrimSpace(in.Workspace) + in.Effect, in.Scope = strings.TrimSpace(in.Effect), strings.TrimSpace(in.Scope) + const path = "/v1/permissions/rules" + var ( + method string + target string + body any + ) + switch strings.TrimSpace(req.GetAction()) { + case "list": + method, target = http.MethodGet, path+permissionRulesQuery("workspace", workspace) + case "add": + rule := strings.TrimSpace(in.Rule) + if rule == "" { + return nil, errors.New("invalid permission rules request: rule is required") + } + if in.Effect != "allow" && in.Effect != "deny" { + return nil, errors.New("invalid permission rules request: effect must be allow or deny") + } + if err := checkPermissionRuleScope(in.Scope, workspace); err != nil { + return nil, err + } + create := map[string]string{"rule": rule, "effect": in.Effect, "scope": in.Scope} + if in.Scope == "project" { + create["workspace"] = workspace + } + method, target, body = http.MethodPost, path, create + case "delete": + id := strings.TrimSpace(in.ID) + if id == "" { + return nil, errors.New("invalid permission rules request: id is required") + } + if err := checkPermissionRuleScope(in.Scope, workspace); err != nil { + return nil, err + } + query := permissionRulesQuery("scope", in.Scope) + if in.Scope == "project" { + query += "&workspace=" + url.QueryEscape(workspace) + } + method, target = http.MethodDelete, path+"/"+url.PathEscape(id)+query + default: + return nil, errors.New("invalid permission rules request: action must be list, add or delete") + } + raw, err := r.permissionRulesRequest(ctx, method, target, body) + if err != nil { + return nil, err + } + return &gatewayv2.PermissionRulesResponse{ResultJson: string(raw)}, nil +} + +func checkPermissionRuleScope(scope, workspace string) error { + switch scope { + case "user": + return nil + case "project": + if workspace == "" { + return errors.New("invalid permission rules request: project scope needs a workspace") + } + return nil + } + return errors.New("invalid permission rules request: scope must be project or user") +} + +func permissionRulesQuery(key, value string) string { + if value == "" { + return "" + } + return "?" + key + "=" + url.QueryEscape(value) +} + +// permissionRulesRequest returns K-brain's JSON answer; a refusal comes back as +// K-brain's own explanation, which the settings page shows as is. +func (r *Relay) permissionRulesRequest(ctx context.Context, method, path string, input any) (json.RawMessage, error) { + resp, err := r.client.request(ctx, method, path, input, nil) + if err != nil { + return nil, err + } + defer func() { _ = resp.Body.Close() }() + const maxAnswer = 4 << 20 + data, err := io.ReadAll(io.LimitReader(resp.Body, maxAnswer+1)) + if err != nil { + return nil, err + } + if len(data) > maxAnswer { + return nil, errors.New("kbrain permission rules response is too large") + } + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + var failure struct { + Error string `json:"error"` + } + if json.Unmarshal(data, &failure) == nil && strings.TrimSpace(failure.Error) != "" { + return nil, errors.New(strings.TrimSpace(failure.Error)) + } + return nil, fmt.Errorf("kbrain permission rules request: %s", resp.Status) + } + if !json.Valid(data) { + return nil, errors.New("kbrain permission rules response is not JSON") + } + return data, nil +} diff --git a/crates/agent-gateway/internal/kbrain/permission_rules_test.go b/crates/agent-gateway/internal/kbrain/permission_rules_test.go new file mode 100644 index 000000000..998647512 --- /dev/null +++ b/crates/agent-gateway/internal/kbrain/permission_rules_test.go @@ -0,0 +1,116 @@ +package kbrain + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" +) + +type recordedRequest struct { + method string + uri string + body map[string]any +} + +func permissionRulesRelay(t *testing.T, status int, answer string) (*Relay, *[]recordedRequest) { + t.Helper() + var seen []recordedRequest + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + raw, _ := io.ReadAll(req.Body) + record := recordedRequest{method: req.Method, uri: req.URL.RequestURI()} + if len(raw) > 0 { + _ = json.Unmarshal(raw, &record.body) + } + seen = append(seen, record) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _, _ = io.WriteString(w, answer) + })) + t.Cleanup(server.Close) + client, err := New(server.URL, "secret-token", server.Client()) + if err != nil { + t.Fatal(err) + } + relay, err := NewRelay(client, "target", ModelRef{}, "") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { relay.Close() }) + return relay, &seen +} + +// The WebUI reaches K-brain's rules API through the relay with only the fields the +// settings page needs; the answer is K-brain's JSON as is. +func TestPermissionRulesRelayMapsActionsToTheRulesAPI(t *testing.T) { + relay, seen := permissionRulesRelay(t, http.StatusOK, `{"project_root":"D:\\w","rules":[{"id":"r_1","rule":"Bash(ls:*)","effect":"allow","scope":"project"}]}`) + response, err := relay.PermissionRules(context.Background(), &gatewayv2.PermissionRulesRequest{Action: "list", InputJson: `{"workspace":"D:\\w","session_id":"s1"}`}) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(response.GetResultJson(), `"r_1"`) { + t.Fatalf("result = %s", response.GetResultJson()) + } + if got := (*seen)[0]; got.method != http.MethodGet || got.uri != "/v1/permissions/rules?workspace=D%3A%5Cw" { + t.Fatalf("list request = %+v", got) + } + + if _, err := relay.PermissionRules(context.Background(), &gatewayv2.PermissionRulesRequest{Action: "add", InputJson: `{"rule":"Bash(rm:*)","effect":"deny","scope":"project","workspace":"/w","session_id":"s1","extra":1}`}); err != nil { + t.Fatal(err) + } + add := (*seen)[1] + if add.method != http.MethodPost || add.uri != "/v1/permissions/rules" { + t.Fatalf("add request = %+v", add) + } + want := map[string]any{"rule": "Bash(rm:*)", "effect": "deny", "scope": "project", "workspace": "/w"} + if len(add.body) != len(want) { + t.Fatalf("add body = %v, want only %v", add.body, want) + } + for key, value := range want { + if add.body[key] != value { + t.Fatalf("add body = %v, want %v", add.body, want) + } + } + + if _, err := relay.PermissionRules(context.Background(), &gatewayv2.PermissionRulesRequest{Action: "delete", InputJson: `{"id":"r 1/x","scope":"user"}`}); err != nil { + t.Fatal(err) + } + if got := (*seen)[2]; got.method != http.MethodDelete || got.uri != "/v1/permissions/rules/r%201%2Fx?scope=user" { + t.Fatalf("delete request = %+v", got) + } +} + +// Session rules and malformed requests never reach K-brain. +func TestPermissionRulesRelayRejectsBadInputLocally(t *testing.T) { + relay, seen := permissionRulesRelay(t, http.StatusOK, `{}`) + for _, req := range []*gatewayv2.PermissionRulesRequest{ + {Action: "add", InputJson: `{"rule":"Bash(rm:*)","effect":"deny","scope":"session"}`}, + {Action: "add", InputJson: `{"rule":"Bash(rm:*)","effect":"ask","scope":"user"}`}, + {Action: "add", InputJson: `{"rule":" ","effect":"deny","scope":"user"}`}, + {Action: "add", InputJson: `{"rule":"Bash(rm:*)","effect":"deny","scope":"project"}`}, + {Action: "delete", InputJson: `{"scope":"user"}`}, + {Action: "delete", InputJson: `{"id":"r_1","scope":"session"}`}, + {Action: "rename", InputJson: `{}`}, + } { + if _, err := relay.PermissionRules(context.Background(), req); err == nil { + t.Fatalf("%s %s: expected rejection", req.GetAction(), req.GetInputJson()) + } + } + if len(*seen) != 0 { + t.Fatalf("rejected requests reached K-brain: %+v", *seen) + } +} + +// K-brain explains a refused rule in {"error"}; the WebUI shows that text, not the JSON. +func TestPermissionRulesRelayKeepsKBrainErrorText(t *testing.T) { + relay, _ := permissionRulesRelay(t, http.StatusBadRequest, `{"error":"permission rule \"Bash(\": missing closing parenthesis","version":"kbrain.agent.v1"}`) + _, err := relay.PermissionRules(context.Background(), &gatewayv2.PermissionRulesRequest{Action: "add", InputJson: `{"rule":"Bash(","effect":"deny","scope":"user"}`}) + if err == nil || err.Error() != `permission rule "Bash(": missing closing parenthesis` { + t.Fatalf("error = %v", err) + } +} diff --git a/crates/agent-gateway/internal/kbrain/review_la3b_permission_rules_r2_test.go b/crates/agent-gateway/internal/kbrain/review_la3b_permission_rules_r2_test.go new file mode 100644 index 000000000..88e3afe26 --- /dev/null +++ b/crates/agent-gateway/internal/kbrain/review_la3b_permission_rules_r2_test.go @@ -0,0 +1,67 @@ +package kbrain + +import ( + "context" + "net/http" + "strings" + "testing" + + gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" +) + +// LA-3b review, round 2: effect/scope are trimmed like the desktop bridge does, so the +// same input gets the same answer on both arms. +func TestReviewLA3bR2PermissionRulesRelayTrimsEffectAndScope(t *testing.T) { + relay, seen := permissionRulesRelay(t, http.StatusCreated, `{"id":"r_1"}`) + if _, err := relay.PermissionRules(context.Background(), &gatewayv2.PermissionRulesRequest{ + Action: "add", + InputJson: `{"rule":" Bash(rm:*) ","effect":" deny ","scope":" project ","workspace":" /w "}`, + }); err != nil { + t.Fatal(err) + } + if _, err := relay.PermissionRules(context.Background(), &gatewayv2.PermissionRulesRequest{ + Action: "delete", + InputJson: `{"id":" r_1 ","scope":" user "}`, + }); err != nil { + t.Fatal(err) + } + got := *seen + if len(got) != 2 { + t.Fatalf("seen %v", got) + } + want := map[string]any{"rule": "Bash(rm:*)", "effect": "deny", "scope": "project", "workspace": "/w"} + for key, value := range want { + if got[0].body[key] != value { + t.Fatalf("add body %v, want %v", got[0].body, want) + } + } + if len(got[0].body) != len(want) { + t.Fatalf("add body %v has extra fields", got[0].body) + } + if got[1].method != http.MethodDelete || got[1].uri != "/v1/permissions/rules/r_1?scope=user" { + t.Fatalf("delete = %s %s", got[1].method, got[1].uri) + } +} + +// Exactly 4 MiB of JSON still passes; one byte more is reported as too large, whether +// K-brain answered 200 or an error. +func TestReviewLA3bR2PermissionRulesRelayAnswerLimit(t *testing.T) { + const limit = 4 << 20 + fill := func(size int) string { + prefix, suffix := `{"rules":[],"pad":"`, `"}` + return prefix + strings.Repeat("x", size-len(prefix)-len(suffix)) + suffix + } + list := &gatewayv2.PermissionRulesRequest{Action: "list"} + + relay, _ := permissionRulesRelay(t, http.StatusOK, fill(limit)) + response, err := relay.PermissionRules(context.Background(), list) + if err != nil || len(response.GetResultJson()) != limit { + t.Fatalf("4 MiB answer: len=%d err=%v", len(response.GetResultJson()), err) + } + for _, status := range []int{http.StatusOK, http.StatusBadRequest} { + relay, _ := permissionRulesRelay(t, status, fill(limit+1)) + if _, err := relay.PermissionRules(context.Background(), list); err == nil || !strings.Contains(err.Error(), "too large") { + t.Fatalf("status %d, 4 MiB + 1: err=%v", status, err) + } + } +} diff --git a/crates/agent-gateway/internal/kbrain/review_la3b_permission_rules_test.go b/crates/agent-gateway/internal/kbrain/review_la3b_permission_rules_test.go new file mode 100644 index 000000000..67e732fe7 --- /dev/null +++ b/crates/agent-gateway/internal/kbrain/review_la3b_permission_rules_test.go @@ -0,0 +1,68 @@ +package kbrain + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" +) + +// LA-3b review: workspace and id reach K-brain exactly as typed, read the way K-brain's +// permissionRules handler reads them (r.URL.Query(), TrimPrefix + PathUnescape on the +// already-decoded r.URL.Path). Covers space (QueryEscape writes "+"), literal "+", "%", +// "#", "?", "&" and CJK. +func TestReviewLA3bPermissionRulesRelayEncodingMatchesKBrainDecoding(t *testing.T) { + type got struct{ workspace, scope, id string } + var seen []got + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + entry := got{workspace: strings.TrimSpace(query.Get("workspace")), scope: query.Get("scope")} + if rest := strings.TrimPrefix(r.URL.Path, "/v1/permissions/rules"); strings.HasPrefix(rest, "/") { + id, err := url.PathUnescape(strings.TrimPrefix(rest, "/")) + if err != nil { + id = "" + } + entry.id = id + } + seen = append(seen, entry) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"ok":true,"rules":[]}`)) + })) + defer server.Close() + client, err := New(server.URL, "t", server.Client()) + if err != nil { + t.Fatal(err) + } + relay, err := NewRelay(client, "target", ModelRef{}, "") + if err != nil { + t.Fatal(err) + } + defer relay.Close() + + workspace := `D:\项目\a b+c%20#?&x=1` + for _, req := range []*gatewayv2.PermissionRulesRequest{ + {Action: "list", InputJson: `{"workspace":` + quote(workspace) + `}`}, + {Action: "delete", InputJson: `{"id":"r_Ab-9","scope":"project","workspace":` + quote(workspace) + `}`}, + } { + if _, err := relay.PermissionRules(context.Background(), req); err != nil { + t.Fatalf("%s: %v", req.GetAction(), err) + } + } + want := []got{{workspace: workspace}, {workspace: workspace, scope: "project", id: "r_Ab-9"}} + if len(seen) != len(want) { + t.Fatalf("seen %v", seen) + } + for i := range want { + if seen[i] != want[i] { + t.Fatalf("request %d: K-brain read %+v, want %+v", i, seen[i], want[i]) + } + } +} + +func quote(value string) string { + return `"` + strings.NewReplacer(`\`, `\\`, `"`, `\"`).Replace(value) + `"` +} diff --git a/crates/agent-gateway/internal/proto/v2/capabilities.go b/crates/agent-gateway/internal/proto/v2/capabilities.go index b9756f844..ae7610f5c 100644 --- a/crates/agent-gateway/internal/proto/v2/capabilities.go +++ b/crates/agent-gateway/internal/proto/v2/capabilities.go @@ -20,3 +20,8 @@ const ComposerCommandsV1Capability = "COMPOSER_COMMANDS_V1" // proxy_detect agent request (app proxy settings: detect running local proxy // services and test them). const ProxyDetectV1Capability = "PROXY_DETECT_V1" + +// PermissionRulesV1Capability identifies desktop runtimes that handle the +// permission_rules agent request (Settings → Permissions: list, add and delete +// K-brain's remembered permission rules). +const PermissionRulesV1Capability = "PERMISSION_RULES_V1" diff --git a/crates/agent-gateway/internal/proto/v2/gateway.pb.go b/crates/agent-gateway/internal/proto/v2/gateway.pb.go index 937b6b56a..6c65e1055 100644 --- a/crates/agent-gateway/internal/proto/v2/gateway.pb.go +++ b/crates/agent-gateway/internal/proto/v2/gateway.pb.go @@ -337,7 +337,7 @@ func (x ChatIngressAck_Action) Number() protoreflect.EnumNumber { // Deprecated: Use ChatIngressAck_Action.Descriptor instead. func (ChatIngressAck_Action) EnumDescriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{184, 0} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{186, 0} } type GatewayEnvelope struct { @@ -411,6 +411,7 @@ type GatewayEnvelope struct { // *GatewayEnvelope_LoopManage // *GatewayEnvelope_ProxyDetect // *GatewayEnvelope_HistoryResync + // *GatewayEnvelope_PermissionRules Payload isGatewayEnvelope_Payload `protobuf_oneof:"payload"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1052,6 +1053,15 @@ func (x *GatewayEnvelope) GetHistoryResync() *HistoryResyncRequest { return nil } +func (x *GatewayEnvelope) GetPermissionRules() *PermissionRulesRequest { + if x != nil { + if x, ok := x.Payload.(*GatewayEnvelope_PermissionRules); ok { + return x.PermissionRules + } + } + return nil +} + type isGatewayEnvelope_Payload interface { isGatewayEnvelope_Payload() } @@ -1316,6 +1326,10 @@ type GatewayEnvelope_HistoryResync struct { HistoryResync *HistoryResyncRequest `protobuf:"bytes,110,opt,name=history_resync,json=historyResync,proto3,oneof"` } +type GatewayEnvelope_PermissionRules struct { + PermissionRules *PermissionRulesRequest `protobuf:"bytes,111,opt,name=permission_rules,json=permissionRules,proto3,oneof"` +} + func (*GatewayEnvelope_ChatCommand) isGatewayEnvelope_Payload() {} func (*GatewayEnvelope_CronManage) isGatewayEnvelope_Payload() {} @@ -1446,6 +1460,8 @@ func (*GatewayEnvelope_ProxyDetect) isGatewayEnvelope_Payload() {} func (*GatewayEnvelope_HistoryResync) isGatewayEnvelope_Payload() {} +func (*GatewayEnvelope_PermissionRules) 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"` @@ -1533,6 +1549,7 @@ type AgentEnvelope struct { // *AgentEnvelope_ProxyDetectResp // *AgentEnvelope_HistorySyncStatus // *AgentEnvelope_HistorySyncSnapshot + // *AgentEnvelope_PermissionRulesResp Payload isAgentEnvelope_Payload `protobuf_oneof:"payload"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -2318,6 +2335,15 @@ func (x *AgentEnvelope) GetHistorySyncSnapshot() *HistorySyncSnapshot { return nil } +func (x *AgentEnvelope) GetPermissionRulesResp() *PermissionRulesResponse { + if x != nil { + if x, ok := x.Payload.(*AgentEnvelope_PermissionRulesResp); ok { + return x.PermissionRulesResp + } + } + return nil +} + type isAgentEnvelope_Payload interface { isAgentEnvelope_Payload() } @@ -2646,6 +2672,10 @@ type AgentEnvelope_HistorySyncSnapshot struct { HistorySyncSnapshot *HistorySyncSnapshot `protobuf:"bytes,118,opt,name=history_sync_snapshot,json=historySyncSnapshot,proto3,oneof"` } +type AgentEnvelope_PermissionRulesResp struct { + PermissionRulesResp *PermissionRulesResponse `protobuf:"bytes,119,opt,name=permission_rules_resp,json=permissionRulesResp,proto3,oneof"` +} + func (*AgentEnvelope_ChatEvent) isAgentEnvelope_Payload() {} func (*AgentEnvelope_CronManageResp) isAgentEnvelope_Payload() {} @@ -2808,6 +2838,8 @@ func (*AgentEnvelope_HistorySyncStatus) isAgentEnvelope_Payload() {} func (*AgentEnvelope_HistorySyncSnapshot) isAgentEnvelope_Payload() {} +func (*AgentEnvelope_PermissionRulesResp) isAgentEnvelope_Payload() {} + type ChatSelectedModel struct { state protoimpl.MessageState `protogen:"open.v1"` CustomProviderId string `protobuf:"bytes,1,opt,name=custom_provider_id,json=customProviderId,proto3" json:"custom_provider_id,omitempty"` @@ -12653,6 +12685,108 @@ func (x *ProxyDetectResponse) GetResultJson() string { return "" } +// 设置 → 权限:查看、添加、删除 K-brain 记住的权限规则(/v1/permissions/rules)。 +// 桌面端或直连 K-brain 的网关只转发设置页需要的字段,会话规则不经这里。 +type PermissionRulesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // "list" | "add" | "delete"。 + Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"` + // list: {"workspace"?};add: {"rule","effect":"allow"|"deny","scope":"project"|"user","workspace"?}; + // delete: {"id","scope":"project"|"user","workspace"?}。project 作用域必须带 workspace。 + InputJson string `protobuf:"bytes,2,opt,name=input_json,json=inputJson,proto3" json:"input_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PermissionRulesRequest) Reset() { + *x = PermissionRulesRequest{} + mi := &file_proto_v2_gateway_proto_msgTypes[141] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PermissionRulesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PermissionRulesRequest) ProtoMessage() {} + +func (x *PermissionRulesRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_v2_gateway_proto_msgTypes[141] + 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 PermissionRulesRequest.ProtoReflect.Descriptor instead. +func (*PermissionRulesRequest) Descriptor() ([]byte, []int) { + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{141} +} + +func (x *PermissionRulesRequest) GetAction() string { + if x != nil { + return x.Action + } + return "" +} + +func (x *PermissionRulesRequest) GetInputJson() string { + if x != nil { + return x.InputJson + } + return "" +} + +type PermissionRulesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // K-brain 的应答 JSON 原样返回:list 为 {project_root?, rules[]},add 为新规则,delete 为 {ok}。 + ResultJson string `protobuf:"bytes,1,opt,name=result_json,json=resultJson,proto3" json:"result_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PermissionRulesResponse) Reset() { + *x = PermissionRulesResponse{} + mi := &file_proto_v2_gateway_proto_msgTypes[142] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PermissionRulesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PermissionRulesResponse) ProtoMessage() {} + +func (x *PermissionRulesResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_v2_gateway_proto_msgTypes[142] + 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 PermissionRulesResponse.ProtoReflect.Descriptor instead. +func (*PermissionRulesResponse) Descriptor() ([]byte, []int) { + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{142} +} + +func (x *PermissionRulesResponse) GetResultJson() string { + if x != nil { + return x.ResultJson + } + return "" +} + type FsRoot struct { state protoimpl.MessageState `protogen:"open.v1"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` @@ -12665,7 +12799,7 @@ type FsRoot struct { func (x *FsRoot) Reset() { *x = FsRoot{} - mi := &file_proto_v2_gateway_proto_msgTypes[141] + mi := &file_proto_v2_gateway_proto_msgTypes[143] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12677,7 +12811,7 @@ func (x *FsRoot) String() string { func (*FsRoot) ProtoMessage() {} func (x *FsRoot) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[141] + mi := &file_proto_v2_gateway_proto_msgTypes[143] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12690,7 +12824,7 @@ func (x *FsRoot) ProtoReflect() protoreflect.Message { // Deprecated: Use FsRoot.ProtoReflect.Descriptor instead. func (*FsRoot) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{141} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{143} } func (x *FsRoot) GetId() string { @@ -12729,7 +12863,7 @@ type FsRootsRequest struct { func (x *FsRootsRequest) Reset() { *x = FsRootsRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[142] + mi := &file_proto_v2_gateway_proto_msgTypes[144] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12741,7 +12875,7 @@ func (x *FsRootsRequest) String() string { func (*FsRootsRequest) ProtoMessage() {} func (x *FsRootsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[142] + mi := &file_proto_v2_gateway_proto_msgTypes[144] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12754,7 +12888,7 @@ func (x *FsRootsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FsRootsRequest.ProtoReflect.Descriptor instead. func (*FsRootsRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{142} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{144} } type FsRootsResponse struct { @@ -12766,7 +12900,7 @@ type FsRootsResponse struct { func (x *FsRootsResponse) Reset() { *x = FsRootsResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[143] + mi := &file_proto_v2_gateway_proto_msgTypes[145] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12778,7 +12912,7 @@ func (x *FsRootsResponse) String() string { func (*FsRootsResponse) ProtoMessage() {} func (x *FsRootsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[143] + mi := &file_proto_v2_gateway_proto_msgTypes[145] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12791,7 +12925,7 @@ func (x *FsRootsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FsRootsResponse.ProtoReflect.Descriptor instead. func (*FsRootsResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{143} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{145} } func (x *FsRootsResponse) GetRoots() []*FsRoot { @@ -12811,7 +12945,7 @@ type FsListDirsRequest struct { func (x *FsListDirsRequest) Reset() { *x = FsListDirsRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[144] + mi := &file_proto_v2_gateway_proto_msgTypes[146] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12823,7 +12957,7 @@ func (x *FsListDirsRequest) String() string { func (*FsListDirsRequest) ProtoMessage() {} func (x *FsListDirsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[144] + mi := &file_proto_v2_gateway_proto_msgTypes[146] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12836,7 +12970,7 @@ func (x *FsListDirsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FsListDirsRequest.ProtoReflect.Descriptor instead. func (*FsListDirsRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{144} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{146} } func (x *FsListDirsRequest) GetPath() string { @@ -12863,7 +12997,7 @@ type FsDirEntry struct { func (x *FsDirEntry) Reset() { *x = FsDirEntry{} - mi := &file_proto_v2_gateway_proto_msgTypes[145] + mi := &file_proto_v2_gateway_proto_msgTypes[147] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12875,7 +13009,7 @@ func (x *FsDirEntry) String() string { func (*FsDirEntry) ProtoMessage() {} func (x *FsDirEntry) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[145] + mi := &file_proto_v2_gateway_proto_msgTypes[147] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12888,7 +13022,7 @@ func (x *FsDirEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use FsDirEntry.ProtoReflect.Descriptor instead. func (*FsDirEntry) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{145} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{147} } func (x *FsDirEntry) GetPath() string { @@ -12916,7 +13050,7 @@ type FsListDirsResponse struct { func (x *FsListDirsResponse) Reset() { *x = FsListDirsResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[146] + mi := &file_proto_v2_gateway_proto_msgTypes[148] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12928,7 +13062,7 @@ func (x *FsListDirsResponse) String() string { func (*FsListDirsResponse) ProtoMessage() {} func (x *FsListDirsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[146] + mi := &file_proto_v2_gateway_proto_msgTypes[148] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12941,7 +13075,7 @@ func (x *FsListDirsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FsListDirsResponse.ProtoReflect.Descriptor instead. func (*FsListDirsResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{146} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{148} } func (x *FsListDirsResponse) GetPath() string { @@ -12975,7 +13109,7 @@ type FsCreateProjectFolderRequest struct { func (x *FsCreateProjectFolderRequest) Reset() { *x = FsCreateProjectFolderRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[147] + mi := &file_proto_v2_gateway_proto_msgTypes[149] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12987,7 +13121,7 @@ func (x *FsCreateProjectFolderRequest) String() string { func (*FsCreateProjectFolderRequest) ProtoMessage() {} func (x *FsCreateProjectFolderRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[147] + mi := &file_proto_v2_gateway_proto_msgTypes[149] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13000,7 +13134,7 @@ func (x *FsCreateProjectFolderRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FsCreateProjectFolderRequest.ProtoReflect.Descriptor instead. func (*FsCreateProjectFolderRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{147} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{149} } func (x *FsCreateProjectFolderRequest) GetParent() string { @@ -13026,7 +13160,7 @@ type FsCreateProjectFolderResponse struct { func (x *FsCreateProjectFolderResponse) Reset() { *x = FsCreateProjectFolderResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[148] + mi := &file_proto_v2_gateway_proto_msgTypes[150] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13038,7 +13172,7 @@ func (x *FsCreateProjectFolderResponse) String() string { func (*FsCreateProjectFolderResponse) ProtoMessage() {} func (x *FsCreateProjectFolderResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[148] + mi := &file_proto_v2_gateway_proto_msgTypes[150] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13051,7 +13185,7 @@ func (x *FsCreateProjectFolderResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FsCreateProjectFolderResponse.ProtoReflect.Descriptor instead. func (*FsCreateProjectFolderResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{148} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{150} } func (x *FsCreateProjectFolderResponse) GetPath() string { @@ -13075,7 +13209,7 @@ type FsListRequest struct { func (x *FsListRequest) Reset() { *x = FsListRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[149] + mi := &file_proto_v2_gateway_proto_msgTypes[151] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13087,7 +13221,7 @@ func (x *FsListRequest) String() string { func (*FsListRequest) ProtoMessage() {} func (x *FsListRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[149] + mi := &file_proto_v2_gateway_proto_msgTypes[151] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13100,7 +13234,7 @@ func (x *FsListRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FsListRequest.ProtoReflect.Descriptor instead. func (*FsListRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{149} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{151} } func (x *FsListRequest) GetWorkdir() string { @@ -13156,7 +13290,7 @@ type FsListEntry struct { func (x *FsListEntry) Reset() { *x = FsListEntry{} - mi := &file_proto_v2_gateway_proto_msgTypes[150] + mi := &file_proto_v2_gateway_proto_msgTypes[152] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13168,7 +13302,7 @@ func (x *FsListEntry) String() string { func (*FsListEntry) ProtoMessage() {} func (x *FsListEntry) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[150] + mi := &file_proto_v2_gateway_proto_msgTypes[152] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13181,7 +13315,7 @@ func (x *FsListEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use FsListEntry.ProtoReflect.Descriptor instead. func (*FsListEntry) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{150} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{152} } func (x *FsListEntry) GetPath() string { @@ -13221,7 +13355,7 @@ type FsListResponse struct { func (x *FsListResponse) Reset() { *x = FsListResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[151] + mi := &file_proto_v2_gateway_proto_msgTypes[153] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13233,7 +13367,7 @@ func (x *FsListResponse) String() string { func (*FsListResponse) ProtoMessage() {} func (x *FsListResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[151] + mi := &file_proto_v2_gateway_proto_msgTypes[153] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13246,7 +13380,7 @@ func (x *FsListResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FsListResponse.ProtoReflect.Descriptor instead. func (*FsListResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{151} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{153} } func (x *FsListResponse) GetPath() string { @@ -13315,7 +13449,7 @@ type FsReadEditableTextRequest struct { func (x *FsReadEditableTextRequest) Reset() { *x = FsReadEditableTextRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[152] + mi := &file_proto_v2_gateway_proto_msgTypes[154] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13327,7 +13461,7 @@ func (x *FsReadEditableTextRequest) String() string { func (*FsReadEditableTextRequest) ProtoMessage() {} func (x *FsReadEditableTextRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[152] + mi := &file_proto_v2_gateway_proto_msgTypes[154] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13340,7 +13474,7 @@ func (x *FsReadEditableTextRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FsReadEditableTextRequest.ProtoReflect.Descriptor instead. func (*FsReadEditableTextRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{152} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{154} } func (x *FsReadEditableTextRequest) GetWorkdir() string { @@ -13371,7 +13505,7 @@ type FsReadEditableTextResponse struct { func (x *FsReadEditableTextResponse) Reset() { *x = FsReadEditableTextResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[153] + mi := &file_proto_v2_gateway_proto_msgTypes[155] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13383,7 +13517,7 @@ func (x *FsReadEditableTextResponse) String() string { func (*FsReadEditableTextResponse) ProtoMessage() {} func (x *FsReadEditableTextResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[153] + mi := &file_proto_v2_gateway_proto_msgTypes[155] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13396,7 +13530,7 @@ func (x *FsReadEditableTextResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FsReadEditableTextResponse.ProtoReflect.Descriptor instead. func (*FsReadEditableTextResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{153} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{155} } func (x *FsReadEditableTextResponse) GetPath() string { @@ -13451,7 +13585,7 @@ type FsReadWorkspaceImageRequest struct { func (x *FsReadWorkspaceImageRequest) Reset() { *x = FsReadWorkspaceImageRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[154] + mi := &file_proto_v2_gateway_proto_msgTypes[156] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13463,7 +13597,7 @@ func (x *FsReadWorkspaceImageRequest) String() string { func (*FsReadWorkspaceImageRequest) ProtoMessage() {} func (x *FsReadWorkspaceImageRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[154] + mi := &file_proto_v2_gateway_proto_msgTypes[156] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13476,7 +13610,7 @@ func (x *FsReadWorkspaceImageRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FsReadWorkspaceImageRequest.ProtoReflect.Descriptor instead. func (*FsReadWorkspaceImageRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{154} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{156} } func (x *FsReadWorkspaceImageRequest) GetWorkdir() string { @@ -13507,7 +13641,7 @@ type FsReadWorkspaceImageResponse struct { func (x *FsReadWorkspaceImageResponse) Reset() { *x = FsReadWorkspaceImageResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[155] + mi := &file_proto_v2_gateway_proto_msgTypes[157] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13519,7 +13653,7 @@ func (x *FsReadWorkspaceImageResponse) String() string { func (*FsReadWorkspaceImageResponse) ProtoMessage() {} func (x *FsReadWorkspaceImageResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[155] + mi := &file_proto_v2_gateway_proto_msgTypes[157] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13532,7 +13666,7 @@ func (x *FsReadWorkspaceImageResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FsReadWorkspaceImageResponse.ProtoReflect.Descriptor instead. func (*FsReadWorkspaceImageResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{155} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{157} } func (x *FsReadWorkspaceImageResponse) GetPath() string { @@ -13593,7 +13727,7 @@ type ChatFileOpenRequest struct { func (x *ChatFileOpenRequest) Reset() { *x = ChatFileOpenRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[156] + mi := &file_proto_v2_gateway_proto_msgTypes[158] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13605,7 +13739,7 @@ func (x *ChatFileOpenRequest) String() string { func (*ChatFileOpenRequest) ProtoMessage() {} func (x *ChatFileOpenRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[156] + mi := &file_proto_v2_gateway_proto_msgTypes[158] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13618,7 +13752,7 @@ func (x *ChatFileOpenRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatFileOpenRequest.ProtoReflect.Descriptor instead. func (*ChatFileOpenRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{156} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{158} } func (x *ChatFileOpenRequest) GetConversationId() string { @@ -13693,7 +13827,7 @@ type ChatFileOpenResponse struct { func (x *ChatFileOpenResponse) Reset() { *x = ChatFileOpenResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[157] + mi := &file_proto_v2_gateway_proto_msgTypes[159] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13705,7 +13839,7 @@ func (x *ChatFileOpenResponse) String() string { func (*ChatFileOpenResponse) ProtoMessage() {} func (x *ChatFileOpenResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[157] + mi := &file_proto_v2_gateway_proto_msgTypes[159] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13718,7 +13852,7 @@ func (x *ChatFileOpenResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatFileOpenResponse.ProtoReflect.Descriptor instead. func (*ChatFileOpenResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{157} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{159} } func (x *ChatFileOpenResponse) GetAction() string { @@ -13793,7 +13927,7 @@ type FsWriteTextRequest struct { func (x *FsWriteTextRequest) Reset() { *x = FsWriteTextRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[158] + mi := &file_proto_v2_gateway_proto_msgTypes[160] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13805,7 +13939,7 @@ func (x *FsWriteTextRequest) String() string { func (*FsWriteTextRequest) ProtoMessage() {} func (x *FsWriteTextRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[158] + mi := &file_proto_v2_gateway_proto_msgTypes[160] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13818,7 +13952,7 @@ func (x *FsWriteTextRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FsWriteTextRequest.ProtoReflect.Descriptor instead. func (*FsWriteTextRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{158} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{160} } func (x *FsWriteTextRequest) GetWorkdir() string { @@ -13892,7 +14026,7 @@ type FsWriteTextResponse struct { func (x *FsWriteTextResponse) Reset() { *x = FsWriteTextResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[159] + mi := &file_proto_v2_gateway_proto_msgTypes[161] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13904,7 +14038,7 @@ func (x *FsWriteTextResponse) String() string { func (*FsWriteTextResponse) ProtoMessage() {} func (x *FsWriteTextResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[159] + mi := &file_proto_v2_gateway_proto_msgTypes[161] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13917,7 +14051,7 @@ func (x *FsWriteTextResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FsWriteTextResponse.ProtoReflect.Descriptor instead. func (*FsWriteTextResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{159} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{161} } func (x *FsWriteTextResponse) GetPath() string { @@ -13979,7 +14113,7 @@ type FsCreateDirRequest struct { func (x *FsCreateDirRequest) Reset() { *x = FsCreateDirRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[160] + mi := &file_proto_v2_gateway_proto_msgTypes[162] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13991,7 +14125,7 @@ func (x *FsCreateDirRequest) String() string { func (*FsCreateDirRequest) ProtoMessage() {} func (x *FsCreateDirRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[160] + mi := &file_proto_v2_gateway_proto_msgTypes[162] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14004,7 +14138,7 @@ func (x *FsCreateDirRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FsCreateDirRequest.ProtoReflect.Descriptor instead. func (*FsCreateDirRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{160} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{162} } func (x *FsCreateDirRequest) GetWorkdir() string { @@ -14031,7 +14165,7 @@ type FsCreateDirResponse struct { func (x *FsCreateDirResponse) Reset() { *x = FsCreateDirResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[161] + mi := &file_proto_v2_gateway_proto_msgTypes[163] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14043,7 +14177,7 @@ func (x *FsCreateDirResponse) String() string { func (*FsCreateDirResponse) ProtoMessage() {} func (x *FsCreateDirResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[161] + mi := &file_proto_v2_gateway_proto_msgTypes[163] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14056,7 +14190,7 @@ func (x *FsCreateDirResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FsCreateDirResponse.ProtoReflect.Descriptor instead. func (*FsCreateDirResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{161} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{163} } func (x *FsCreateDirResponse) GetPath() string { @@ -14084,7 +14218,7 @@ type FsRenameRequest struct { func (x *FsRenameRequest) Reset() { *x = FsRenameRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[162] + mi := &file_proto_v2_gateway_proto_msgTypes[164] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14096,7 +14230,7 @@ func (x *FsRenameRequest) String() string { func (*FsRenameRequest) ProtoMessage() {} func (x *FsRenameRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[162] + mi := &file_proto_v2_gateway_proto_msgTypes[164] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14109,7 +14243,7 @@ func (x *FsRenameRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FsRenameRequest.ProtoReflect.Descriptor instead. func (*FsRenameRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{162} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{164} } func (x *FsRenameRequest) GetWorkdir() string { @@ -14144,7 +14278,7 @@ type FsRenameResponse struct { func (x *FsRenameResponse) Reset() { *x = FsRenameResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[163] + mi := &file_proto_v2_gateway_proto_msgTypes[165] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14156,7 +14290,7 @@ func (x *FsRenameResponse) String() string { func (*FsRenameResponse) ProtoMessage() {} func (x *FsRenameResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[163] + mi := &file_proto_v2_gateway_proto_msgTypes[165] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14169,7 +14303,7 @@ func (x *FsRenameResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FsRenameResponse.ProtoReflect.Descriptor instead. func (*FsRenameResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{163} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{165} } func (x *FsRenameResponse) GetFromPath() string { @@ -14203,7 +14337,7 @@ type FsDeleteRequest struct { func (x *FsDeleteRequest) Reset() { *x = FsDeleteRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[164] + mi := &file_proto_v2_gateway_proto_msgTypes[166] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14215,7 +14349,7 @@ func (x *FsDeleteRequest) String() string { func (*FsDeleteRequest) ProtoMessage() {} func (x *FsDeleteRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[164] + mi := &file_proto_v2_gateway_proto_msgTypes[166] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14228,7 +14362,7 @@ func (x *FsDeleteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FsDeleteRequest.ProtoReflect.Descriptor instead. func (*FsDeleteRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{164} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{166} } func (x *FsDeleteRequest) GetWorkdir() string { @@ -14255,7 +14389,7 @@ type FsDeleteResponse struct { func (x *FsDeleteResponse) Reset() { *x = FsDeleteResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[165] + mi := &file_proto_v2_gateway_proto_msgTypes[167] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14267,7 +14401,7 @@ func (x *FsDeleteResponse) String() string { func (*FsDeleteResponse) ProtoMessage() {} func (x *FsDeleteResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[165] + mi := &file_proto_v2_gateway_proto_msgTypes[167] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14280,7 +14414,7 @@ func (x *FsDeleteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FsDeleteResponse.ProtoReflect.Descriptor instead. func (*FsDeleteResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{165} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{167} } func (x *FsDeleteResponse) GetPath() string { @@ -14306,7 +14440,7 @@ type PingRequest struct { func (x *PingRequest) Reset() { *x = PingRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[166] + mi := &file_proto_v2_gateway_proto_msgTypes[168] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14318,7 +14452,7 @@ func (x *PingRequest) String() string { func (*PingRequest) ProtoMessage() {} func (x *PingRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[166] + mi := &file_proto_v2_gateway_proto_msgTypes[168] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14331,7 +14465,7 @@ func (x *PingRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PingRequest.ProtoReflect.Descriptor instead. func (*PingRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{166} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{168} } func (x *PingRequest) GetTimestamp() int64 { @@ -14350,7 +14484,7 @@ type PongResponse struct { func (x *PongResponse) Reset() { *x = PongResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[167] + mi := &file_proto_v2_gateway_proto_msgTypes[169] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14362,7 +14496,7 @@ func (x *PongResponse) String() string { func (*PongResponse) ProtoMessage() {} func (x *PongResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[167] + mi := &file_proto_v2_gateway_proto_msgTypes[169] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14375,7 +14509,7 @@ func (x *PongResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PongResponse.ProtoReflect.Descriptor instead. func (*PongResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{167} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{169} } func (x *PongResponse) GetTimestamp() int64 { @@ -14395,7 +14529,7 @@ type ErrorResponse struct { func (x *ErrorResponse) Reset() { *x = ErrorResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[168] + mi := &file_proto_v2_gateway_proto_msgTypes[170] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14407,7 +14541,7 @@ func (x *ErrorResponse) String() string { func (*ErrorResponse) ProtoMessage() {} func (x *ErrorResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[168] + mi := &file_proto_v2_gateway_proto_msgTypes[170] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14420,7 +14554,7 @@ func (x *ErrorResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ErrorResponse.ProtoReflect.Descriptor instead. func (*ErrorResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{168} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{170} } func (x *ErrorResponse) GetCode() int32 { @@ -14447,7 +14581,7 @@ type ProviderCustomHeader struct { func (x *ProviderCustomHeader) Reset() { *x = ProviderCustomHeader{} - mi := &file_proto_v2_gateway_proto_msgTypes[169] + mi := &file_proto_v2_gateway_proto_msgTypes[171] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14459,7 +14593,7 @@ func (x *ProviderCustomHeader) String() string { func (*ProviderCustomHeader) ProtoMessage() {} func (x *ProviderCustomHeader) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[169] + mi := &file_proto_v2_gateway_proto_msgTypes[171] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14472,7 +14606,7 @@ func (x *ProviderCustomHeader) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCustomHeader.ProtoReflect.Descriptor instead. func (*ProviderCustomHeader) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{169} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{171} } func (x *ProviderCustomHeader) GetName() string { @@ -14501,7 +14635,7 @@ type ProviderCustomHeaders struct { func (x *ProviderCustomHeaders) Reset() { *x = ProviderCustomHeaders{} - mi := &file_proto_v2_gateway_proto_msgTypes[170] + mi := &file_proto_v2_gateway_proto_msgTypes[172] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14513,7 +14647,7 @@ func (x *ProviderCustomHeaders) String() string { func (*ProviderCustomHeaders) ProtoMessage() {} func (x *ProviderCustomHeaders) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[170] + mi := &file_proto_v2_gateway_proto_msgTypes[172] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14526,7 +14660,7 @@ func (x *ProviderCustomHeaders) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCustomHeaders.ProtoReflect.Descriptor instead. func (*ProviderCustomHeaders) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{170} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{172} } func (x *ProviderCustomHeaders) GetHeaders() []*ProviderCustomHeader { @@ -14559,7 +14693,7 @@ type ProviderModelsRequest struct { func (x *ProviderModelsRequest) Reset() { *x = ProviderModelsRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[171] + mi := &file_proto_v2_gateway_proto_msgTypes[173] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14571,7 +14705,7 @@ func (x *ProviderModelsRequest) String() string { func (*ProviderModelsRequest) ProtoMessage() {} func (x *ProviderModelsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[171] + mi := &file_proto_v2_gateway_proto_msgTypes[173] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14584,7 +14718,7 @@ func (x *ProviderModelsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderModelsRequest.ProtoReflect.Descriptor instead. func (*ProviderModelsRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{171} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{173} } func (x *ProviderModelsRequest) GetProviderType() string { @@ -14659,7 +14793,7 @@ type ProviderModelsResponse struct { func (x *ProviderModelsResponse) Reset() { *x = ProviderModelsResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[172] + mi := &file_proto_v2_gateway_proto_msgTypes[174] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14671,7 +14805,7 @@ func (x *ProviderModelsResponse) String() string { func (*ProviderModelsResponse) ProtoMessage() {} func (x *ProviderModelsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[172] + mi := &file_proto_v2_gateway_proto_msgTypes[174] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14684,7 +14818,7 @@ func (x *ProviderModelsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderModelsResponse.ProtoReflect.Descriptor instead. func (*ProviderModelsResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{172} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{174} } func (x *ProviderModelsResponse) GetModelsJson() string { @@ -14707,7 +14841,7 @@ type ProviderUsageRequest struct { func (x *ProviderUsageRequest) Reset() { *x = ProviderUsageRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[173] + mi := &file_proto_v2_gateway_proto_msgTypes[175] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14719,7 +14853,7 @@ func (x *ProviderUsageRequest) String() string { func (*ProviderUsageRequest) ProtoMessage() {} func (x *ProviderUsageRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[173] + mi := &file_proto_v2_gateway_proto_msgTypes[175] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14732,7 +14866,7 @@ func (x *ProviderUsageRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderUsageRequest.ProtoReflect.Descriptor instead. func (*ProviderUsageRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{173} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{175} } func (x *ProviderUsageRequest) GetProviderId() string { @@ -14765,7 +14899,7 @@ type ProviderUsageResponse struct { func (x *ProviderUsageResponse) Reset() { *x = ProviderUsageResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[174] + mi := &file_proto_v2_gateway_proto_msgTypes[176] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14777,7 +14911,7 @@ func (x *ProviderUsageResponse) String() string { func (*ProviderUsageResponse) ProtoMessage() {} func (x *ProviderUsageResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[174] + mi := &file_proto_v2_gateway_proto_msgTypes[176] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14790,7 +14924,7 @@ func (x *ProviderUsageResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderUsageResponse.ProtoReflect.Descriptor instead. func (*ProviderUsageResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{174} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{176} } func (x *ProviderUsageResponse) GetResultJson() string { @@ -14814,7 +14948,7 @@ type ChatIngressBatch struct { func (x *ChatIngressBatch) Reset() { *x = ChatIngressBatch{} - mi := &file_proto_v2_gateway_proto_msgTypes[175] + mi := &file_proto_v2_gateway_proto_msgTypes[177] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14826,7 +14960,7 @@ func (x *ChatIngressBatch) String() string { func (*ChatIngressBatch) ProtoMessage() {} func (x *ChatIngressBatch) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[175] + mi := &file_proto_v2_gateway_proto_msgTypes[177] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14839,7 +14973,7 @@ func (x *ChatIngressBatch) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatIngressBatch.ProtoReflect.Descriptor instead. func (*ChatIngressBatch) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{175} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{177} } func (x *ChatIngressBatch) GetRunId() string { @@ -14885,7 +15019,7 @@ type ChatIngressRecord struct { func (x *ChatIngressRecord) Reset() { *x = ChatIngressRecord{} - mi := &file_proto_v2_gateway_proto_msgTypes[176] + mi := &file_proto_v2_gateway_proto_msgTypes[178] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -14897,7 +15031,7 @@ func (x *ChatIngressRecord) String() string { func (*ChatIngressRecord) ProtoMessage() {} func (x *ChatIngressRecord) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[176] + mi := &file_proto_v2_gateway_proto_msgTypes[178] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -14910,7 +15044,7 @@ func (x *ChatIngressRecord) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatIngressRecord.ProtoReflect.Descriptor instead. func (*ChatIngressRecord) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{176} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{178} } func (x *ChatIngressRecord) GetPayload() isChatIngressRecord_Payload { @@ -14994,7 +15128,7 @@ type ChatIngressDelta struct { func (x *ChatIngressDelta) Reset() { *x = ChatIngressDelta{} - mi := &file_proto_v2_gateway_proto_msgTypes[177] + mi := &file_proto_v2_gateway_proto_msgTypes[179] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15006,7 +15140,7 @@ func (x *ChatIngressDelta) String() string { func (*ChatIngressDelta) ProtoMessage() {} func (x *ChatIngressDelta) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[177] + mi := &file_proto_v2_gateway_proto_msgTypes[179] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15019,7 +15153,7 @@ func (x *ChatIngressDelta) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatIngressDelta.ProtoReflect.Descriptor instead. func (*ChatIngressDelta) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{177} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{179} } func (x *ChatIngressDelta) GetEventJson() string { @@ -15046,7 +15180,7 @@ type ChatIngressHeartbeat struct { func (x *ChatIngressHeartbeat) Reset() { *x = ChatIngressHeartbeat{} - mi := &file_proto_v2_gateway_proto_msgTypes[178] + mi := &file_proto_v2_gateway_proto_msgTypes[180] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15058,7 +15192,7 @@ func (x *ChatIngressHeartbeat) String() string { func (*ChatIngressHeartbeat) ProtoMessage() {} func (x *ChatIngressHeartbeat) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[178] + mi := &file_proto_v2_gateway_proto_msgTypes[180] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15071,7 +15205,7 @@ func (x *ChatIngressHeartbeat) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatIngressHeartbeat.ProtoReflect.Descriptor instead. func (*ChatIngressHeartbeat) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{178} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{180} } func (x *ChatIngressHeartbeat) GetUpdatedAt() int64 { @@ -15096,7 +15230,7 @@ type ChatIngressCheckpoint struct { func (x *ChatIngressCheckpoint) Reset() { *x = ChatIngressCheckpoint{} - mi := &file_proto_v2_gateway_proto_msgTypes[179] + mi := &file_proto_v2_gateway_proto_msgTypes[181] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15108,7 +15242,7 @@ func (x *ChatIngressCheckpoint) String() string { func (*ChatIngressCheckpoint) ProtoMessage() {} func (x *ChatIngressCheckpoint) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[179] + mi := &file_proto_v2_gateway_proto_msgTypes[181] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15121,7 +15255,7 @@ func (x *ChatIngressCheckpoint) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatIngressCheckpoint.ProtoReflect.Descriptor instead. func (*ChatIngressCheckpoint) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{179} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{181} } func (x *ChatIngressCheckpoint) GetCoversThroughSeq() uint64 { @@ -15191,7 +15325,7 @@ type ChatIngressTerminal struct { func (x *ChatIngressTerminal) Reset() { *x = ChatIngressTerminal{} - mi := &file_proto_v2_gateway_proto_msgTypes[180] + mi := &file_proto_v2_gateway_proto_msgTypes[182] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15203,7 +15337,7 @@ func (x *ChatIngressTerminal) String() string { func (*ChatIngressTerminal) ProtoMessage() {} func (x *ChatIngressTerminal) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[180] + mi := &file_proto_v2_gateway_proto_msgTypes[182] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15216,7 +15350,7 @@ func (x *ChatIngressTerminal) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatIngressTerminal.ProtoReflect.Descriptor instead. func (*ChatIngressTerminal) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{180} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{182} } func (x *ChatIngressTerminal) GetCoversThroughSeq() uint64 { @@ -15300,7 +15434,7 @@ type ChatIngressResume struct { func (x *ChatIngressResume) Reset() { *x = ChatIngressResume{} - mi := &file_proto_v2_gateway_proto_msgTypes[181] + mi := &file_proto_v2_gateway_proto_msgTypes[183] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15312,7 +15446,7 @@ func (x *ChatIngressResume) String() string { func (*ChatIngressResume) ProtoMessage() {} func (x *ChatIngressResume) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[181] + mi := &file_proto_v2_gateway_proto_msgTypes[183] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15325,7 +15459,7 @@ func (x *ChatIngressResume) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatIngressResume.ProtoReflect.Descriptor instead. func (*ChatIngressResume) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{181} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{183} } func (x *ChatIngressResume) GetRuns() []*ChatIngressRunResume { @@ -15351,7 +15485,7 @@ type ChatIngressRunResume struct { func (x *ChatIngressRunResume) Reset() { *x = ChatIngressRunResume{} - mi := &file_proto_v2_gateway_proto_msgTypes[182] + mi := &file_proto_v2_gateway_proto_msgTypes[184] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15363,7 +15497,7 @@ func (x *ChatIngressRunResume) String() string { func (*ChatIngressRunResume) ProtoMessage() {} func (x *ChatIngressRunResume) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[182] + mi := &file_proto_v2_gateway_proto_msgTypes[184] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15376,7 +15510,7 @@ func (x *ChatIngressRunResume) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatIngressRunResume.ProtoReflect.Descriptor instead. func (*ChatIngressRunResume) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{182} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{184} } func (x *ChatIngressRunResume) GetRunId() string { @@ -15454,7 +15588,7 @@ type ChatIngressFragment struct { func (x *ChatIngressFragment) Reset() { *x = ChatIngressFragment{} - mi := &file_proto_v2_gateway_proto_msgTypes[183] + mi := &file_proto_v2_gateway_proto_msgTypes[185] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15466,7 +15600,7 @@ func (x *ChatIngressFragment) String() string { func (*ChatIngressFragment) ProtoMessage() {} func (x *ChatIngressFragment) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[183] + mi := &file_proto_v2_gateway_proto_msgTypes[185] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15479,7 +15613,7 @@ func (x *ChatIngressFragment) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatIngressFragment.ProtoReflect.Descriptor instead. func (*ChatIngressFragment) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{183} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{185} } func (x *ChatIngressFragment) GetRunId() string { @@ -15554,7 +15688,7 @@ type ChatIngressAck struct { func (x *ChatIngressAck) Reset() { *x = ChatIngressAck{} - mi := &file_proto_v2_gateway_proto_msgTypes[184] + mi := &file_proto_v2_gateway_proto_msgTypes[186] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15566,7 +15700,7 @@ func (x *ChatIngressAck) String() string { func (*ChatIngressAck) ProtoMessage() {} func (x *ChatIngressAck) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[184] + mi := &file_proto_v2_gateway_proto_msgTypes[186] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15579,7 +15713,7 @@ func (x *ChatIngressAck) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatIngressAck.ProtoReflect.Descriptor instead. func (*ChatIngressAck) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{184} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{186} } func (x *ChatIngressAck) GetRunId() string { @@ -15648,7 +15782,7 @@ type HistorySetCwdRequest struct { func (x *HistorySetCwdRequest) Reset() { *x = HistorySetCwdRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[185] + mi := &file_proto_v2_gateway_proto_msgTypes[187] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15660,7 +15794,7 @@ func (x *HistorySetCwdRequest) String() string { func (*HistorySetCwdRequest) ProtoMessage() {} func (x *HistorySetCwdRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[185] + mi := &file_proto_v2_gateway_proto_msgTypes[187] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15673,7 +15807,7 @@ func (x *HistorySetCwdRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use HistorySetCwdRequest.ProtoReflect.Descriptor instead. func (*HistorySetCwdRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{185} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{187} } func (x *HistorySetCwdRequest) GetConversationId() string { @@ -15699,7 +15833,7 @@ type HistorySetCwdResponse struct { func (x *HistorySetCwdResponse) Reset() { *x = HistorySetCwdResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[186] + mi := &file_proto_v2_gateway_proto_msgTypes[188] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15711,7 +15845,7 @@ func (x *HistorySetCwdResponse) String() string { func (*HistorySetCwdResponse) ProtoMessage() {} func (x *HistorySetCwdResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[186] + mi := &file_proto_v2_gateway_proto_msgTypes[188] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15724,7 +15858,7 @@ func (x *HistorySetCwdResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use HistorySetCwdResponse.ProtoReflect.Descriptor instead. func (*HistorySetCwdResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{186} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{188} } func (x *HistorySetCwdResponse) GetConversation() *ConversationSummary { @@ -15746,7 +15880,7 @@ type WorkspaceRootGrantDraft struct { func (x *WorkspaceRootGrantDraft) Reset() { *x = WorkspaceRootGrantDraft{} - mi := &file_proto_v2_gateway_proto_msgTypes[187] + mi := &file_proto_v2_gateway_proto_msgTypes[189] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15758,7 +15892,7 @@ func (x *WorkspaceRootGrantDraft) String() string { func (*WorkspaceRootGrantDraft) ProtoMessage() {} func (x *WorkspaceRootGrantDraft) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[187] + mi := &file_proto_v2_gateway_proto_msgTypes[189] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15771,7 +15905,7 @@ func (x *WorkspaceRootGrantDraft) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkspaceRootGrantDraft.ProtoReflect.Descriptor instead. func (*WorkspaceRootGrantDraft) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{187} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{189} } func (x *WorkspaceRootGrantDraft) GetId() string { @@ -15820,7 +15954,7 @@ type WorkspaceRootGrant struct { func (x *WorkspaceRootGrant) Reset() { *x = WorkspaceRootGrant{} - mi := &file_proto_v2_gateway_proto_msgTypes[188] + mi := &file_proto_v2_gateway_proto_msgTypes[190] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15832,7 +15966,7 @@ func (x *WorkspaceRootGrant) String() string { func (*WorkspaceRootGrant) ProtoMessage() {} func (x *WorkspaceRootGrant) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[188] + mi := &file_proto_v2_gateway_proto_msgTypes[190] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15845,7 +15979,7 @@ func (x *WorkspaceRootGrant) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkspaceRootGrant.ProtoReflect.Descriptor instead. func (*WorkspaceRootGrant) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{188} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{190} } func (x *WorkspaceRootGrant) GetId() string { @@ -15930,7 +16064,7 @@ type WorkspaceRootGrantsRequest struct { func (x *WorkspaceRootGrantsRequest) Reset() { *x = WorkspaceRootGrantsRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[189] + mi := &file_proto_v2_gateway_proto_msgTypes[191] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -15942,7 +16076,7 @@ func (x *WorkspaceRootGrantsRequest) String() string { func (*WorkspaceRootGrantsRequest) ProtoMessage() {} func (x *WorkspaceRootGrantsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[189] + mi := &file_proto_v2_gateway_proto_msgTypes[191] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -15955,7 +16089,7 @@ func (x *WorkspaceRootGrantsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkspaceRootGrantsRequest.ProtoReflect.Descriptor instead. func (*WorkspaceRootGrantsRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{189} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{191} } func (x *WorkspaceRootGrantsRequest) GetAction() string { @@ -15995,7 +16129,7 @@ type WorkspaceRootGrantsResponse struct { func (x *WorkspaceRootGrantsResponse) Reset() { *x = WorkspaceRootGrantsResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[190] + mi := &file_proto_v2_gateway_proto_msgTypes[192] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16007,7 +16141,7 @@ func (x *WorkspaceRootGrantsResponse) String() string { func (*WorkspaceRootGrantsResponse) ProtoMessage() {} func (x *WorkspaceRootGrantsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[190] + mi := &file_proto_v2_gateway_proto_msgTypes[192] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16020,7 +16154,7 @@ func (x *WorkspaceRootGrantsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkspaceRootGrantsResponse.ProtoReflect.Descriptor instead. func (*WorkspaceRootGrantsResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{190} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{192} } func (x *WorkspaceRootGrantsResponse) GetGrants() []*WorkspaceRootGrant { @@ -16040,7 +16174,7 @@ type CheckpointExpectedEntry struct { func (x *CheckpointExpectedEntry) Reset() { *x = CheckpointExpectedEntry{} - mi := &file_proto_v2_gateway_proto_msgTypes[191] + mi := &file_proto_v2_gateway_proto_msgTypes[193] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16052,7 +16186,7 @@ func (x *CheckpointExpectedEntry) String() string { func (*CheckpointExpectedEntry) ProtoMessage() {} func (x *CheckpointExpectedEntry) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[191] + mi := &file_proto_v2_gateway_proto_msgTypes[193] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16065,7 +16199,7 @@ func (x *CheckpointExpectedEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointExpectedEntry.ProtoReflect.Descriptor instead. func (*CheckpointExpectedEntry) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{191} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{193} } func (x *CheckpointExpectedEntry) GetKey() string { @@ -16095,7 +16229,7 @@ type CheckpointRequest struct { func (x *CheckpointRequest) Reset() { *x = CheckpointRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[192] + mi := &file_proto_v2_gateway_proto_msgTypes[194] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16107,7 +16241,7 @@ func (x *CheckpointRequest) String() string { func (*CheckpointRequest) ProtoMessage() {} func (x *CheckpointRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[192] + mi := &file_proto_v2_gateway_proto_msgTypes[194] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16120,7 +16254,7 @@ func (x *CheckpointRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointRequest.ProtoReflect.Descriptor instead. func (*CheckpointRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{192} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{194} } func (x *CheckpointRequest) GetAction() string { @@ -16168,7 +16302,7 @@ type CheckpointResponse struct { func (x *CheckpointResponse) Reset() { *x = CheckpointResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[193] + mi := &file_proto_v2_gateway_proto_msgTypes[195] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16180,7 +16314,7 @@ func (x *CheckpointResponse) String() string { func (*CheckpointResponse) ProtoMessage() {} func (x *CheckpointResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[193] + mi := &file_proto_v2_gateway_proto_msgTypes[195] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16193,7 +16327,7 @@ func (x *CheckpointResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckpointResponse.ProtoReflect.Descriptor instead. func (*CheckpointResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{193} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{195} } func (x *CheckpointResponse) GetAction() string { @@ -16222,7 +16356,7 @@ type ChatConversationReference struct { func (x *ChatConversationReference) Reset() { *x = ChatConversationReference{} - mi := &file_proto_v2_gateway_proto_msgTypes[194] + mi := &file_proto_v2_gateway_proto_msgTypes[196] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16234,7 +16368,7 @@ func (x *ChatConversationReference) String() string { func (*ChatConversationReference) ProtoMessage() {} func (x *ChatConversationReference) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[194] + mi := &file_proto_v2_gateway_proto_msgTypes[196] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16247,7 +16381,7 @@ func (x *ChatConversationReference) ProtoReflect() protoreflect.Message { // Deprecated: Use ChatConversationReference.ProtoReflect.Descriptor instead. func (*ChatConversationReference) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{194} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{196} } func (x *ChatConversationReference) GetId() string { @@ -16289,7 +16423,7 @@ type PlanningRequest struct { func (x *PlanningRequest) Reset() { *x = PlanningRequest{} - mi := &file_proto_v2_gateway_proto_msgTypes[195] + mi := &file_proto_v2_gateway_proto_msgTypes[197] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16301,7 +16435,7 @@ func (x *PlanningRequest) String() string { func (*PlanningRequest) ProtoMessage() {} func (x *PlanningRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[195] + mi := &file_proto_v2_gateway_proto_msgTypes[197] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16314,7 +16448,7 @@ func (x *PlanningRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PlanningRequest.ProtoReflect.Descriptor instead. func (*PlanningRequest) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{195} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{197} } func (x *PlanningRequest) GetAction() string { @@ -16340,7 +16474,7 @@ type PlanningResponse struct { func (x *PlanningResponse) Reset() { *x = PlanningResponse{} - mi := &file_proto_v2_gateway_proto_msgTypes[196] + mi := &file_proto_v2_gateway_proto_msgTypes[198] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16352,7 +16486,7 @@ func (x *PlanningResponse) String() string { func (*PlanningResponse) ProtoMessage() {} func (x *PlanningResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[196] + mi := &file_proto_v2_gateway_proto_msgTypes[198] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16365,7 +16499,7 @@ func (x *PlanningResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PlanningResponse.ProtoReflect.Descriptor instead. func (*PlanningResponse) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{196} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{198} } func (x *PlanningResponse) GetResultJson() string { @@ -16384,7 +16518,7 @@ type PlanningChanged struct { func (x *PlanningChanged) Reset() { *x = PlanningChanged{} - mi := &file_proto_v2_gateway_proto_msgTypes[197] + mi := &file_proto_v2_gateway_proto_msgTypes[199] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -16396,7 +16530,7 @@ func (x *PlanningChanged) String() string { func (*PlanningChanged) ProtoMessage() {} func (x *PlanningChanged) ProtoReflect() protoreflect.Message { - mi := &file_proto_v2_gateway_proto_msgTypes[197] + mi := &file_proto_v2_gateway_proto_msgTypes[199] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -16409,7 +16543,7 @@ func (x *PlanningChanged) ProtoReflect() protoreflect.Message { // Deprecated: Use PlanningChanged.ProtoReflect.Descriptor instead. func (*PlanningChanged) Descriptor() ([]byte, []int) { - return file_proto_v2_gateway_proto_rawDescGZIP(), []int{197} + return file_proto_v2_gateway_proto_rawDescGZIP(), []int{199} } func (x *PlanningChanged) GetSeq() uint64 { @@ -16423,7 +16557,7 @@ var File_proto_v2_gateway_proto protoreflect.FileDescriptor const file_proto_v2_gateway_proto_rawDesc = "" + "\n" + - "\x16proto/v2/gateway.proto\x12\x14liveagent.gateway.v2\"\x9b,\n" + + "\x16proto/v2/gateway.proto\x12\x14liveagent.gateway.v2\"\xf6,\n" + "\x0fGatewayEnvelope\x12\x1d\n" + "\n" + "request_id\x18\x01 \x01(\tR\trequestId\x12\x1c\n" + @@ -16504,8 +16638,9 @@ const file_proto_v2_gateway_proto_rawDesc = "" + "\vloop_manage\x18l \x01(\v2'.liveagent.gateway.v2.LoopManageRequestH\x00R\n" + "loopManage\x12M\n" + "\fproxy_detect\x18m \x01(\v2(.liveagent.gateway.v2.ProxyDetectRequestH\x00R\vproxyDetect\x12S\n" + - "\x0ehistory_resync\x18n \x01(\v2*.liveagent.gateway.v2.HistoryResyncRequestH\x00R\rhistoryResyncB\t\n" + - "\apayloadJ\x04\bC\x10DJ\x04\bD\x10EJ\x04\bE\x10FJ\x04\bJ\x10K\"\xa0;\n" + + "\x0ehistory_resync\x18n \x01(\v2*.liveagent.gateway.v2.HistoryResyncRequestH\x00R\rhistoryResync\x12Y\n" + + "\x10permission_rules\x18o \x01(\v2,.liveagent.gateway.v2.PermissionRulesRequestH\x00R\x0fpermissionRulesB\t\n" + + "\apayloadJ\x04\bC\x10DJ\x04\bD\x10EJ\x04\bE\x10FJ\x04\bJ\x10K\"\x85<\n" + "\rAgentEnvelope\x12\x1d\n" + "\n" + "request_id\x18\x01 \x01(\tR\trequestId\x12\x1c\n" + @@ -16594,7 +16729,8 @@ const file_proto_v2_gateway_proto_rawDesc = "" + "\x10loop_manage_resp\x18s \x01(\v2(.liveagent.gateway.v2.LoopManageResponseH\x00R\x0eloopManageResp\x12W\n" + "\x11proxy_detect_resp\x18t \x01(\v2).liveagent.gateway.v2.ProxyDetectResponseH\x00R\x0fproxyDetectResp\x12Y\n" + "\x13history_sync_status\x18u \x01(\v2'.liveagent.gateway.v2.HistorySyncStatusH\x00R\x11historySyncStatus\x12_\n" + - "\x15history_sync_snapshot\x18v \x01(\v2).liveagent.gateway.v2.HistorySyncSnapshotH\x00R\x13historySyncSnapshotB\t\n" + + "\x15history_sync_snapshot\x18v \x01(\v2).liveagent.gateway.v2.HistorySyncSnapshotH\x00R\x13historySyncSnapshot\x12c\n" + + "\x15permission_rules_resp\x18w \x01(\v2-.liveagent.gateway.v2.PermissionRulesResponseH\x00R\x13permissionRulesRespB\t\n" + "\apayloadJ\x04\bC\x10DJ\x04\bD\x10EJ\x04\bE\x10FJ\x04\bN\x10O\"|\n" + "\x11ChatSelectedModel\x12,\n" + "\x12custom_provider_id\x18\x01 \x01(\tR\x10customProviderId\x12\x14\n" + @@ -17472,6 +17608,13 @@ const file_proto_v2_gateway_proto_rawDesc = "" + "\x13ProxyDetectResponse\x12\x16\n" + "\x06action\x18\x01 \x01(\tR\x06action\x12\x1f\n" + "\vresult_json\x18\x02 \x01(\tR\n" + + "resultJson\"O\n" + + "\x16PermissionRulesRequest\x12\x16\n" + + "\x06action\x18\x01 \x01(\tR\x06action\x12\x1d\n" + + "\n" + + "input_json\x18\x02 \x01(\tR\tinputJson\":\n" + + "\x17PermissionRulesResponse\x12\x1f\n" + + "\vresult_json\x18\x01 \x01(\tR\n" + "resultJson\"V\n" + "\x06FsRoot\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + @@ -17826,7 +17969,7 @@ func file_proto_v2_gateway_proto_rawDescGZIP() []byte { } var file_proto_v2_gateway_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_proto_v2_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 198) +var file_proto_v2_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 200) var file_proto_v2_gateway_proto_goTypes = []any{ (ImportDirectoryOperation)(0), // 0: liveagent.gateway.v2.ImportDirectoryOperation (TunnelFrameKind)(0), // 1: liveagent.gateway.v2.TunnelFrameKind @@ -17974,63 +18117,65 @@ var file_proto_v2_gateway_proto_goTypes = []any{ (*CuaDriverResponse)(nil), // 143: liveagent.gateway.v2.CuaDriverResponse (*ProxyDetectRequest)(nil), // 144: liveagent.gateway.v2.ProxyDetectRequest (*ProxyDetectResponse)(nil), // 145: liveagent.gateway.v2.ProxyDetectResponse - (*FsRoot)(nil), // 146: liveagent.gateway.v2.FsRoot - (*FsRootsRequest)(nil), // 147: liveagent.gateway.v2.FsRootsRequest - (*FsRootsResponse)(nil), // 148: liveagent.gateway.v2.FsRootsResponse - (*FsListDirsRequest)(nil), // 149: liveagent.gateway.v2.FsListDirsRequest - (*FsDirEntry)(nil), // 150: liveagent.gateway.v2.FsDirEntry - (*FsListDirsResponse)(nil), // 151: liveagent.gateway.v2.FsListDirsResponse - (*FsCreateProjectFolderRequest)(nil), // 152: liveagent.gateway.v2.FsCreateProjectFolderRequest - (*FsCreateProjectFolderResponse)(nil), // 153: liveagent.gateway.v2.FsCreateProjectFolderResponse - (*FsListRequest)(nil), // 154: liveagent.gateway.v2.FsListRequest - (*FsListEntry)(nil), // 155: liveagent.gateway.v2.FsListEntry - (*FsListResponse)(nil), // 156: liveagent.gateway.v2.FsListResponse - (*FsReadEditableTextRequest)(nil), // 157: liveagent.gateway.v2.FsReadEditableTextRequest - (*FsReadEditableTextResponse)(nil), // 158: liveagent.gateway.v2.FsReadEditableTextResponse - (*FsReadWorkspaceImageRequest)(nil), // 159: liveagent.gateway.v2.FsReadWorkspaceImageRequest - (*FsReadWorkspaceImageResponse)(nil), // 160: liveagent.gateway.v2.FsReadWorkspaceImageResponse - (*ChatFileOpenRequest)(nil), // 161: liveagent.gateway.v2.ChatFileOpenRequest - (*ChatFileOpenResponse)(nil), // 162: liveagent.gateway.v2.ChatFileOpenResponse - (*FsWriteTextRequest)(nil), // 163: liveagent.gateway.v2.FsWriteTextRequest - (*FsWriteTextResponse)(nil), // 164: liveagent.gateway.v2.FsWriteTextResponse - (*FsCreateDirRequest)(nil), // 165: liveagent.gateway.v2.FsCreateDirRequest - (*FsCreateDirResponse)(nil), // 166: liveagent.gateway.v2.FsCreateDirResponse - (*FsRenameRequest)(nil), // 167: liveagent.gateway.v2.FsRenameRequest - (*FsRenameResponse)(nil), // 168: liveagent.gateway.v2.FsRenameResponse - (*FsDeleteRequest)(nil), // 169: liveagent.gateway.v2.FsDeleteRequest - (*FsDeleteResponse)(nil), // 170: liveagent.gateway.v2.FsDeleteResponse - (*PingRequest)(nil), // 171: liveagent.gateway.v2.PingRequest - (*PongResponse)(nil), // 172: liveagent.gateway.v2.PongResponse - (*ErrorResponse)(nil), // 173: liveagent.gateway.v2.ErrorResponse - (*ProviderCustomHeader)(nil), // 174: liveagent.gateway.v2.ProviderCustomHeader - (*ProviderCustomHeaders)(nil), // 175: liveagent.gateway.v2.ProviderCustomHeaders - (*ProviderModelsRequest)(nil), // 176: liveagent.gateway.v2.ProviderModelsRequest - (*ProviderModelsResponse)(nil), // 177: liveagent.gateway.v2.ProviderModelsResponse - (*ProviderUsageRequest)(nil), // 178: liveagent.gateway.v2.ProviderUsageRequest - (*ProviderUsageResponse)(nil), // 179: liveagent.gateway.v2.ProviderUsageResponse - (*ChatIngressBatch)(nil), // 180: liveagent.gateway.v2.ChatIngressBatch - (*ChatIngressRecord)(nil), // 181: liveagent.gateway.v2.ChatIngressRecord - (*ChatIngressDelta)(nil), // 182: liveagent.gateway.v2.ChatIngressDelta - (*ChatIngressHeartbeat)(nil), // 183: liveagent.gateway.v2.ChatIngressHeartbeat - (*ChatIngressCheckpoint)(nil), // 184: liveagent.gateway.v2.ChatIngressCheckpoint - (*ChatIngressTerminal)(nil), // 185: liveagent.gateway.v2.ChatIngressTerminal - (*ChatIngressResume)(nil), // 186: liveagent.gateway.v2.ChatIngressResume - (*ChatIngressRunResume)(nil), // 187: liveagent.gateway.v2.ChatIngressRunResume - (*ChatIngressFragment)(nil), // 188: liveagent.gateway.v2.ChatIngressFragment - (*ChatIngressAck)(nil), // 189: liveagent.gateway.v2.ChatIngressAck - (*HistorySetCwdRequest)(nil), // 190: liveagent.gateway.v2.HistorySetCwdRequest - (*HistorySetCwdResponse)(nil), // 191: liveagent.gateway.v2.HistorySetCwdResponse - (*WorkspaceRootGrantDraft)(nil), // 192: liveagent.gateway.v2.WorkspaceRootGrantDraft - (*WorkspaceRootGrant)(nil), // 193: liveagent.gateway.v2.WorkspaceRootGrant - (*WorkspaceRootGrantsRequest)(nil), // 194: liveagent.gateway.v2.WorkspaceRootGrantsRequest - (*WorkspaceRootGrantsResponse)(nil), // 195: liveagent.gateway.v2.WorkspaceRootGrantsResponse - (*CheckpointExpectedEntry)(nil), // 196: liveagent.gateway.v2.CheckpointExpectedEntry - (*CheckpointRequest)(nil), // 197: liveagent.gateway.v2.CheckpointRequest - (*CheckpointResponse)(nil), // 198: liveagent.gateway.v2.CheckpointResponse - (*ChatConversationReference)(nil), // 199: liveagent.gateway.v2.ChatConversationReference - (*PlanningRequest)(nil), // 200: liveagent.gateway.v2.PlanningRequest - (*PlanningResponse)(nil), // 201: liveagent.gateway.v2.PlanningResponse - (*PlanningChanged)(nil), // 202: liveagent.gateway.v2.PlanningChanged + (*PermissionRulesRequest)(nil), // 146: liveagent.gateway.v2.PermissionRulesRequest + (*PermissionRulesResponse)(nil), // 147: liveagent.gateway.v2.PermissionRulesResponse + (*FsRoot)(nil), // 148: liveagent.gateway.v2.FsRoot + (*FsRootsRequest)(nil), // 149: liveagent.gateway.v2.FsRootsRequest + (*FsRootsResponse)(nil), // 150: liveagent.gateway.v2.FsRootsResponse + (*FsListDirsRequest)(nil), // 151: liveagent.gateway.v2.FsListDirsRequest + (*FsDirEntry)(nil), // 152: liveagent.gateway.v2.FsDirEntry + (*FsListDirsResponse)(nil), // 153: liveagent.gateway.v2.FsListDirsResponse + (*FsCreateProjectFolderRequest)(nil), // 154: liveagent.gateway.v2.FsCreateProjectFolderRequest + (*FsCreateProjectFolderResponse)(nil), // 155: liveagent.gateway.v2.FsCreateProjectFolderResponse + (*FsListRequest)(nil), // 156: liveagent.gateway.v2.FsListRequest + (*FsListEntry)(nil), // 157: liveagent.gateway.v2.FsListEntry + (*FsListResponse)(nil), // 158: liveagent.gateway.v2.FsListResponse + (*FsReadEditableTextRequest)(nil), // 159: liveagent.gateway.v2.FsReadEditableTextRequest + (*FsReadEditableTextResponse)(nil), // 160: liveagent.gateway.v2.FsReadEditableTextResponse + (*FsReadWorkspaceImageRequest)(nil), // 161: liveagent.gateway.v2.FsReadWorkspaceImageRequest + (*FsReadWorkspaceImageResponse)(nil), // 162: liveagent.gateway.v2.FsReadWorkspaceImageResponse + (*ChatFileOpenRequest)(nil), // 163: liveagent.gateway.v2.ChatFileOpenRequest + (*ChatFileOpenResponse)(nil), // 164: liveagent.gateway.v2.ChatFileOpenResponse + (*FsWriteTextRequest)(nil), // 165: liveagent.gateway.v2.FsWriteTextRequest + (*FsWriteTextResponse)(nil), // 166: liveagent.gateway.v2.FsWriteTextResponse + (*FsCreateDirRequest)(nil), // 167: liveagent.gateway.v2.FsCreateDirRequest + (*FsCreateDirResponse)(nil), // 168: liveagent.gateway.v2.FsCreateDirResponse + (*FsRenameRequest)(nil), // 169: liveagent.gateway.v2.FsRenameRequest + (*FsRenameResponse)(nil), // 170: liveagent.gateway.v2.FsRenameResponse + (*FsDeleteRequest)(nil), // 171: liveagent.gateway.v2.FsDeleteRequest + (*FsDeleteResponse)(nil), // 172: liveagent.gateway.v2.FsDeleteResponse + (*PingRequest)(nil), // 173: liveagent.gateway.v2.PingRequest + (*PongResponse)(nil), // 174: liveagent.gateway.v2.PongResponse + (*ErrorResponse)(nil), // 175: liveagent.gateway.v2.ErrorResponse + (*ProviderCustomHeader)(nil), // 176: liveagent.gateway.v2.ProviderCustomHeader + (*ProviderCustomHeaders)(nil), // 177: liveagent.gateway.v2.ProviderCustomHeaders + (*ProviderModelsRequest)(nil), // 178: liveagent.gateway.v2.ProviderModelsRequest + (*ProviderModelsResponse)(nil), // 179: liveagent.gateway.v2.ProviderModelsResponse + (*ProviderUsageRequest)(nil), // 180: liveagent.gateway.v2.ProviderUsageRequest + (*ProviderUsageResponse)(nil), // 181: liveagent.gateway.v2.ProviderUsageResponse + (*ChatIngressBatch)(nil), // 182: liveagent.gateway.v2.ChatIngressBatch + (*ChatIngressRecord)(nil), // 183: liveagent.gateway.v2.ChatIngressRecord + (*ChatIngressDelta)(nil), // 184: liveagent.gateway.v2.ChatIngressDelta + (*ChatIngressHeartbeat)(nil), // 185: liveagent.gateway.v2.ChatIngressHeartbeat + (*ChatIngressCheckpoint)(nil), // 186: liveagent.gateway.v2.ChatIngressCheckpoint + (*ChatIngressTerminal)(nil), // 187: liveagent.gateway.v2.ChatIngressTerminal + (*ChatIngressResume)(nil), // 188: liveagent.gateway.v2.ChatIngressResume + (*ChatIngressRunResume)(nil), // 189: liveagent.gateway.v2.ChatIngressRunResume + (*ChatIngressFragment)(nil), // 190: liveagent.gateway.v2.ChatIngressFragment + (*ChatIngressAck)(nil), // 191: liveagent.gateway.v2.ChatIngressAck + (*HistorySetCwdRequest)(nil), // 192: liveagent.gateway.v2.HistorySetCwdRequest + (*HistorySetCwdResponse)(nil), // 193: liveagent.gateway.v2.HistorySetCwdResponse + (*WorkspaceRootGrantDraft)(nil), // 194: liveagent.gateway.v2.WorkspaceRootGrantDraft + (*WorkspaceRootGrant)(nil), // 195: liveagent.gateway.v2.WorkspaceRootGrant + (*WorkspaceRootGrantsRequest)(nil), // 196: liveagent.gateway.v2.WorkspaceRootGrantsRequest + (*WorkspaceRootGrantsResponse)(nil), // 197: liveagent.gateway.v2.WorkspaceRootGrantsResponse + (*CheckpointExpectedEntry)(nil), // 198: liveagent.gateway.v2.CheckpointExpectedEntry + (*CheckpointRequest)(nil), // 199: liveagent.gateway.v2.CheckpointRequest + (*CheckpointResponse)(nil), // 200: liveagent.gateway.v2.CheckpointResponse + (*ChatConversationReference)(nil), // 201: liveagent.gateway.v2.ChatConversationReference + (*PlanningRequest)(nil), // 202: liveagent.gateway.v2.PlanningRequest + (*PlanningResponse)(nil), // 203: liveagent.gateway.v2.PlanningResponse + (*PlanningChanged)(nil), // 204: liveagent.gateway.v2.PlanningChanged } var file_proto_v2_gateway_proto_depIdxs = []int32{ 63, // 0: liveagent.gateway.v2.GatewayEnvelope.chat_command:type_name -> liveagent.gateway.v2.ChatCommandRequest @@ -18053,44 +18198,44 @@ var file_proto_v2_gateway_proto_depIdxs = []int32{ 132, // 17: liveagent.gateway.v2.GatewayEnvelope.skill_text_read:type_name -> liveagent.gateway.v2.SkillTextReadRequest 136, // 18: liveagent.gateway.v2.GatewayEnvelope.file_mention_list:type_name -> liveagent.gateway.v2.FileMentionListRequest 14, // 19: liveagent.gateway.v2.GatewayEnvelope.upload_readable_files:type_name -> liveagent.gateway.v2.UploadReadableFilesRequest - 147, // 20: liveagent.gateway.v2.GatewayEnvelope.fs_roots:type_name -> liveagent.gateway.v2.FsRootsRequest - 149, // 21: liveagent.gateway.v2.GatewayEnvelope.fs_list_dirs:type_name -> liveagent.gateway.v2.FsListDirsRequest - 171, // 22: liveagent.gateway.v2.GatewayEnvelope.ping:type_name -> liveagent.gateway.v2.PingRequest + 149, // 20: liveagent.gateway.v2.GatewayEnvelope.fs_roots:type_name -> liveagent.gateway.v2.FsRootsRequest + 151, // 21: liveagent.gateway.v2.GatewayEnvelope.fs_list_dirs:type_name -> liveagent.gateway.v2.FsListDirsRequest + 173, // 22: liveagent.gateway.v2.GatewayEnvelope.ping:type_name -> liveagent.gateway.v2.PingRequest 19, // 23: liveagent.gateway.v2.GatewayEnvelope.uploaded_image_preview:type_name -> liveagent.gateway.v2.UploadedImagePreviewRequest 38, // 24: liveagent.gateway.v2.GatewayEnvelope.memory_manage:type_name -> liveagent.gateway.v2.MemoryManageRequest 134, // 25: liveagent.gateway.v2.GatewayEnvelope.skill_manage:type_name -> liveagent.gateway.v2.SkillManageRequest - 152, // 26: liveagent.gateway.v2.GatewayEnvelope.fs_create_project_folder:type_name -> liveagent.gateway.v2.FsCreateProjectFolderRequest + 154, // 26: liveagent.gateway.v2.GatewayEnvelope.fs_create_project_folder:type_name -> liveagent.gateway.v2.FsCreateProjectFolderRequest 40, // 27: liveagent.gateway.v2.GatewayEnvelope.terminal_request:type_name -> liveagent.gateway.v2.TerminalRequest - 154, // 28: liveagent.gateway.v2.GatewayEnvelope.fs_list:type_name -> liveagent.gateway.v2.FsListRequest - 163, // 29: liveagent.gateway.v2.GatewayEnvelope.fs_write_text:type_name -> liveagent.gateway.v2.FsWriteTextRequest - 165, // 30: liveagent.gateway.v2.GatewayEnvelope.fs_create_dir:type_name -> liveagent.gateway.v2.FsCreateDirRequest - 167, // 31: liveagent.gateway.v2.GatewayEnvelope.fs_rename:type_name -> liveagent.gateway.v2.FsRenameRequest - 169, // 32: liveagent.gateway.v2.GatewayEnvelope.fs_delete:type_name -> liveagent.gateway.v2.FsDeleteRequest + 156, // 28: liveagent.gateway.v2.GatewayEnvelope.fs_list:type_name -> liveagent.gateway.v2.FsListRequest + 165, // 29: liveagent.gateway.v2.GatewayEnvelope.fs_write_text:type_name -> liveagent.gateway.v2.FsWriteTextRequest + 167, // 30: liveagent.gateway.v2.GatewayEnvelope.fs_create_dir:type_name -> liveagent.gateway.v2.FsCreateDirRequest + 169, // 31: liveagent.gateway.v2.GatewayEnvelope.fs_rename:type_name -> liveagent.gateway.v2.FsRenameRequest + 171, // 32: liveagent.gateway.v2.GatewayEnvelope.fs_delete:type_name -> liveagent.gateway.v2.FsDeleteRequest 58, // 33: liveagent.gateway.v2.GatewayEnvelope.git_request:type_name -> liveagent.gateway.v2.GitRequest - 157, // 34: liveagent.gateway.v2.GatewayEnvelope.fs_read_editable_text:type_name -> liveagent.gateway.v2.FsReadEditableTextRequest - 159, // 35: liveagent.gateway.v2.GatewayEnvelope.fs_read_workspace_image:type_name -> liveagent.gateway.v2.FsReadWorkspaceImageRequest + 159, // 34: liveagent.gateway.v2.GatewayEnvelope.fs_read_editable_text:type_name -> liveagent.gateway.v2.FsReadEditableTextRequest + 161, // 35: liveagent.gateway.v2.GatewayEnvelope.fs_read_workspace_image:type_name -> liveagent.gateway.v2.FsReadWorkspaceImageRequest 43, // 36: liveagent.gateway.v2.GatewayEnvelope.sftp_request:type_name -> liveagent.gateway.v2.SftpRequest - 176, // 37: liveagent.gateway.v2.GatewayEnvelope.provider_models:type_name -> liveagent.gateway.v2.ProviderModelsRequest + 178, // 37: liveagent.gateway.v2.GatewayEnvelope.provider_models:type_name -> liveagent.gateway.v2.ProviderModelsRequest 112, // 38: liveagent.gateway.v2.GatewayEnvelope.settings_reset_ssh_known_host:type_name -> liveagent.gateway.v2.SettingsResetSshKnownHostRequest 64, // 39: liveagent.gateway.v2.GatewayEnvelope.chat_queue:type_name -> liveagent.gateway.v2.ChatQueueRequest - 189, // 40: liveagent.gateway.v2.GatewayEnvelope.chat_ingress_ack:type_name -> liveagent.gateway.v2.ChatIngressAck + 191, // 40: liveagent.gateway.v2.GatewayEnvelope.chat_ingress_ack:type_name -> liveagent.gateway.v2.ChatIngressAck 25, // 41: liveagent.gateway.v2.GatewayEnvelope.tunnel_state:type_name -> liveagent.gateway.v2.TunnelStateSnapshot 26, // 42: liveagent.gateway.v2.GatewayEnvelope.tunnel_mutation:type_name -> liveagent.gateway.v2.TunnelMutation 31, // 43: liveagent.gateway.v2.GatewayEnvelope.tunnel_frame:type_name -> liveagent.gateway.v2.TunnelFrame 32, // 44: liveagent.gateway.v2.GatewayEnvelope.workspace_watch:type_name -> liveagent.gateway.v2.WorkspaceWatchRequest 36, // 45: liveagent.gateway.v2.GatewayEnvelope.managed_process_request:type_name -> liveagent.gateway.v2.ManagedProcessRequest 86, // 46: liveagent.gateway.v2.GatewayEnvelope.history_branch:type_name -> liveagent.gateway.v2.HistoryBranchRequest - 178, // 47: liveagent.gateway.v2.GatewayEnvelope.provider_usage:type_name -> liveagent.gateway.v2.ProviderUsageRequest - 161, // 48: liveagent.gateway.v2.GatewayEnvelope.chat_file_open:type_name -> liveagent.gateway.v2.ChatFileOpenRequest - 190, // 49: liveagent.gateway.v2.GatewayEnvelope.history_set_cwd:type_name -> liveagent.gateway.v2.HistorySetCwdRequest - 194, // 50: liveagent.gateway.v2.GatewayEnvelope.workspace_root_grants:type_name -> liveagent.gateway.v2.WorkspaceRootGrantsRequest + 180, // 47: liveagent.gateway.v2.GatewayEnvelope.provider_usage:type_name -> liveagent.gateway.v2.ProviderUsageRequest + 163, // 48: liveagent.gateway.v2.GatewayEnvelope.chat_file_open:type_name -> liveagent.gateway.v2.ChatFileOpenRequest + 192, // 49: liveagent.gateway.v2.GatewayEnvelope.history_set_cwd:type_name -> liveagent.gateway.v2.HistorySetCwdRequest + 196, // 50: liveagent.gateway.v2.GatewayEnvelope.workspace_root_grants:type_name -> liveagent.gateway.v2.WorkspaceRootGrantsRequest 17, // 51: liveagent.gateway.v2.GatewayEnvelope.import_directory:type_name -> liveagent.gateway.v2.ImportDirectoryRequest - 197, // 52: liveagent.gateway.v2.GatewayEnvelope.checkpoint:type_name -> liveagent.gateway.v2.CheckpointRequest + 199, // 52: liveagent.gateway.v2.GatewayEnvelope.checkpoint:type_name -> liveagent.gateway.v2.CheckpointRequest 79, // 53: liveagent.gateway.v2.GatewayEnvelope.trajectory_fetch:type_name -> liveagent.gateway.v2.TrajectoryFetchRequest 139, // 54: liveagent.gateway.v2.GatewayEnvelope.installed_apps_list:type_name -> liveagent.gateway.v2.InstalledAppsListRequest 9, // 55: liveagent.gateway.v2.GatewayEnvelope.clarify_turn:type_name -> liveagent.gateway.v2.ClarifyTurnRequest 142, // 56: liveagent.gateway.v2.GatewayEnvelope.cua_driver:type_name -> liveagent.gateway.v2.CuaDriverRequest - 200, // 57: liveagent.gateway.v2.GatewayEnvelope.planning:type_name -> liveagent.gateway.v2.PlanningRequest + 202, // 57: liveagent.gateway.v2.GatewayEnvelope.planning:type_name -> liveagent.gateway.v2.PlanningRequest 117, // 58: liveagent.gateway.v2.GatewayEnvelope.prompt_template_list:type_name -> liveagent.gateway.v2.PromptTemplateListRequest 120, // 59: liveagent.gateway.v2.GatewayEnvelope.prompt_template_expand:type_name -> liveagent.gateway.v2.PromptTemplateExpandRequest 124, // 60: liveagent.gateway.v2.GatewayEnvelope.slash_command_list:type_name -> liveagent.gateway.v2.SlashCommandListRequest @@ -18098,174 +18243,176 @@ var file_proto_v2_gateway_proto_depIdxs = []int32{ 127, // 62: liveagent.gateway.v2.GatewayEnvelope.loop_manage:type_name -> liveagent.gateway.v2.LoopManageRequest 144, // 63: liveagent.gateway.v2.GatewayEnvelope.proxy_detect:type_name -> liveagent.gateway.v2.ProxyDetectRequest 105, // 64: liveagent.gateway.v2.GatewayEnvelope.history_resync:type_name -> liveagent.gateway.v2.HistoryResyncRequest - 67, // 65: liveagent.gateway.v2.AgentEnvelope.chat_event:type_name -> liveagent.gateway.v2.ChatEvent - 73, // 66: liveagent.gateway.v2.AgentEnvelope.cron_manage_resp:type_name -> liveagent.gateway.v2.CronManageResponse - 75, // 67: liveagent.gateway.v2.AgentEnvelope.history_list_resp:type_name -> liveagent.gateway.v2.HistoryListResponse - 78, // 68: liveagent.gateway.v2.AgentEnvelope.history_get_resp:type_name -> liveagent.gateway.v2.HistoryGetResponse - 85, // 69: liveagent.gateway.v2.AgentEnvelope.history_rename_resp:type_name -> liveagent.gateway.v2.HistoryRenameResponse - 101, // 70: liveagent.gateway.v2.AgentEnvelope.history_delete_resp:type_name -> liveagent.gateway.v2.HistoryDeleteResponse - 102, // 71: liveagent.gateway.v2.AgentEnvelope.history_sync:type_name -> liveagent.gateway.v2.HistorySyncEvent - 83, // 72: liveagent.gateway.v2.AgentEnvelope.history_prefix_resp:type_name -> liveagent.gateway.v2.HistoryPrefixResponse - 89, // 73: liveagent.gateway.v2.AgentEnvelope.history_pin_resp:type_name -> liveagent.gateway.v2.HistoryPinResponse - 92, // 74: liveagent.gateway.v2.AgentEnvelope.history_share_get_resp:type_name -> liveagent.gateway.v2.HistoryShareGetResponse - 94, // 75: liveagent.gateway.v2.AgentEnvelope.history_share_set_resp:type_name -> liveagent.gateway.v2.HistoryShareSetResponse - 96, // 76: liveagent.gateway.v2.AgentEnvelope.history_share_resolve_resp:type_name -> liveagent.gateway.v2.HistoryShareResolveResponse - 99, // 77: liveagent.gateway.v2.AgentEnvelope.history_workdirs_resp:type_name -> liveagent.gateway.v2.HistoryWorkdirsResponse - 107, // 78: liveagent.gateway.v2.AgentEnvelope.provider_list_resp:type_name -> liveagent.gateway.v2.ProviderListResponse - 109, // 79: liveagent.gateway.v2.AgentEnvelope.settings_get_resp:type_name -> liveagent.gateway.v2.SettingsGetResponse - 111, // 80: liveagent.gateway.v2.AgentEnvelope.settings_update_resp:type_name -> liveagent.gateway.v2.SettingsUpdateResponse - 114, // 81: liveagent.gateway.v2.AgentEnvelope.settings_sync:type_name -> liveagent.gateway.v2.SettingsSyncEvent - 116, // 82: liveagent.gateway.v2.AgentEnvelope.skill_files_list_resp:type_name -> liveagent.gateway.v2.SkillFilesListResponse - 131, // 83: liveagent.gateway.v2.AgentEnvelope.skill_metadata_read_resp:type_name -> liveagent.gateway.v2.SkillMetadataReadResponse - 133, // 84: liveagent.gateway.v2.AgentEnvelope.skill_text_read_resp:type_name -> liveagent.gateway.v2.SkillTextReadResponse - 138, // 85: liveagent.gateway.v2.AgentEnvelope.file_mention_list_resp:type_name -> liveagent.gateway.v2.FileMentionListResponse - 15, // 86: liveagent.gateway.v2.AgentEnvelope.upload_readable_files_resp:type_name -> liveagent.gateway.v2.UploadReadableFilesResponse - 148, // 87: liveagent.gateway.v2.AgentEnvelope.fs_roots_resp:type_name -> liveagent.gateway.v2.FsRootsResponse - 172, // 88: liveagent.gateway.v2.AgentEnvelope.pong:type_name -> liveagent.gateway.v2.PongResponse - 151, // 89: liveagent.gateway.v2.AgentEnvelope.fs_list_dirs_resp:type_name -> liveagent.gateway.v2.FsListDirsResponse - 20, // 90: liveagent.gateway.v2.AgentEnvelope.uploaded_image_preview_resp:type_name -> liveagent.gateway.v2.UploadedImagePreviewResponse - 39, // 91: liveagent.gateway.v2.AgentEnvelope.memory_manage_resp:type_name -> liveagent.gateway.v2.MemoryManageResponse - 135, // 92: liveagent.gateway.v2.AgentEnvelope.skill_manage_resp:type_name -> liveagent.gateway.v2.SkillManageResponse - 153, // 93: liveagent.gateway.v2.AgentEnvelope.fs_create_project_folder_resp:type_name -> liveagent.gateway.v2.FsCreateProjectFolderResponse - 55, // 94: liveagent.gateway.v2.AgentEnvelope.terminal_response:type_name -> liveagent.gateway.v2.TerminalResponse - 56, // 95: liveagent.gateway.v2.AgentEnvelope.terminal_event:type_name -> liveagent.gateway.v2.TerminalEvent - 156, // 96: liveagent.gateway.v2.AgentEnvelope.fs_list_resp:type_name -> liveagent.gateway.v2.FsListResponse - 164, // 97: liveagent.gateway.v2.AgentEnvelope.fs_write_text_resp:type_name -> liveagent.gateway.v2.FsWriteTextResponse - 166, // 98: liveagent.gateway.v2.AgentEnvelope.fs_create_dir_resp:type_name -> liveagent.gateway.v2.FsCreateDirResponse - 168, // 99: liveagent.gateway.v2.AgentEnvelope.fs_rename_resp:type_name -> liveagent.gateway.v2.FsRenameResponse - 170, // 100: liveagent.gateway.v2.AgentEnvelope.fs_delete_resp:type_name -> liveagent.gateway.v2.FsDeleteResponse - 59, // 101: liveagent.gateway.v2.AgentEnvelope.git_response:type_name -> liveagent.gateway.v2.GitResponse - 158, // 102: liveagent.gateway.v2.AgentEnvelope.fs_read_editable_text_resp:type_name -> liveagent.gateway.v2.FsReadEditableTextResponse - 160, // 103: liveagent.gateway.v2.AgentEnvelope.fs_read_workspace_image_resp:type_name -> liveagent.gateway.v2.FsReadWorkspaceImageResponse - 46, // 104: liveagent.gateway.v2.AgentEnvelope.sftp_response:type_name -> liveagent.gateway.v2.SftpResponse - 47, // 105: liveagent.gateway.v2.AgentEnvelope.sftp_event:type_name -> liveagent.gateway.v2.SftpEvent - 65, // 106: liveagent.gateway.v2.AgentEnvelope.chat_queue_resp:type_name -> liveagent.gateway.v2.ChatQueueResponse - 66, // 107: liveagent.gateway.v2.AgentEnvelope.chat_queue_event:type_name -> liveagent.gateway.v2.ChatQueueEvent - 68, // 108: liveagent.gateway.v2.AgentEnvelope.chat_control:type_name -> liveagent.gateway.v2.ChatControlEvent - 70, // 109: liveagent.gateway.v2.AgentEnvelope.runtime_status:type_name -> liveagent.gateway.v2.RuntimeStatusEvent - 113, // 110: liveagent.gateway.v2.AgentEnvelope.settings_reset_ssh_known_host_resp:type_name -> liveagent.gateway.v2.SettingsResetSshKnownHostResponse - 69, // 111: liveagent.gateway.v2.AgentEnvelope.chat_runtime_snapshot:type_name -> liveagent.gateway.v2.ChatRuntimeSnapshot - 177, // 112: liveagent.gateway.v2.AgentEnvelope.provider_models_resp:type_name -> liveagent.gateway.v2.ProviderModelsResponse - 22, // 113: liveagent.gateway.v2.AgentEnvelope.tunnel_desired:type_name -> liveagent.gateway.v2.TunnelDesiredState - 27, // 114: liveagent.gateway.v2.AgentEnvelope.tunnel_mutation_result:type_name -> liveagent.gateway.v2.TunnelMutationResult - 31, // 115: liveagent.gateway.v2.AgentEnvelope.tunnel_frame:type_name -> liveagent.gateway.v2.TunnelFrame - 29, // 116: liveagent.gateway.v2.AgentEnvelope.tunnel_probe_report:type_name -> liveagent.gateway.v2.TunnelProbeReport - 33, // 117: liveagent.gateway.v2.AgentEnvelope.workspace_activity:type_name -> liveagent.gateway.v2.WorkspaceActivityEvent - 37, // 118: liveagent.gateway.v2.AgentEnvelope.managed_process_response:type_name -> liveagent.gateway.v2.ManagedProcessResponse - 35, // 119: liveagent.gateway.v2.AgentEnvelope.managed_process_snapshot:type_name -> liveagent.gateway.v2.ManagedProcessSnapshot - 87, // 120: liveagent.gateway.v2.AgentEnvelope.history_branch_resp:type_name -> liveagent.gateway.v2.HistoryBranchResponse - 179, // 121: liveagent.gateway.v2.AgentEnvelope.provider_usage_resp:type_name -> liveagent.gateway.v2.ProviderUsageResponse - 180, // 122: liveagent.gateway.v2.AgentEnvelope.chat_ingress_batch:type_name -> liveagent.gateway.v2.ChatIngressBatch - 186, // 123: liveagent.gateway.v2.AgentEnvelope.chat_ingress_resume:type_name -> liveagent.gateway.v2.ChatIngressResume - 188, // 124: liveagent.gateway.v2.AgentEnvelope.chat_ingress_fragment:type_name -> liveagent.gateway.v2.ChatIngressFragment - 162, // 125: liveagent.gateway.v2.AgentEnvelope.chat_file_open_resp:type_name -> liveagent.gateway.v2.ChatFileOpenResponse - 173, // 126: liveagent.gateway.v2.AgentEnvelope.error:type_name -> liveagent.gateway.v2.ErrorResponse - 191, // 127: liveagent.gateway.v2.AgentEnvelope.history_set_cwd_resp:type_name -> liveagent.gateway.v2.HistorySetCwdResponse - 195, // 128: liveagent.gateway.v2.AgentEnvelope.workspace_root_grants_resp:type_name -> liveagent.gateway.v2.WorkspaceRootGrantsResponse - 18, // 129: liveagent.gateway.v2.AgentEnvelope.import_directory_resp:type_name -> liveagent.gateway.v2.ImportDirectoryResponse - 198, // 130: liveagent.gateway.v2.AgentEnvelope.checkpoint_resp:type_name -> liveagent.gateway.v2.CheckpointResponse - 81, // 131: liveagent.gateway.v2.AgentEnvelope.trajectory_fetch_resp:type_name -> liveagent.gateway.v2.TrajectoryFetchResponse - 141, // 132: liveagent.gateway.v2.AgentEnvelope.installed_apps_list_resp:type_name -> liveagent.gateway.v2.InstalledAppsListResponse - 10, // 133: liveagent.gateway.v2.AgentEnvelope.clarify_turn_resp:type_name -> liveagent.gateway.v2.ClarifyTurnResponse - 143, // 134: liveagent.gateway.v2.AgentEnvelope.cua_driver_resp:type_name -> liveagent.gateway.v2.CuaDriverResponse - 11, // 135: liveagent.gateway.v2.AgentEnvelope.clarify_turn_delta:type_name -> liveagent.gateway.v2.ClarifyTurnDelta - 201, // 136: liveagent.gateway.v2.AgentEnvelope.planning_resp:type_name -> liveagent.gateway.v2.PlanningResponse - 202, // 137: liveagent.gateway.v2.AgentEnvelope.planning_changed:type_name -> liveagent.gateway.v2.PlanningChanged - 119, // 138: liveagent.gateway.v2.AgentEnvelope.prompt_template_list_resp:type_name -> liveagent.gateway.v2.PromptTemplateListResponse - 121, // 139: liveagent.gateway.v2.AgentEnvelope.prompt_template_expand_resp:type_name -> liveagent.gateway.v2.PromptTemplateExpandResponse - 126, // 140: liveagent.gateway.v2.AgentEnvelope.slash_command_list_resp:type_name -> liveagent.gateway.v2.SlashCommandListResponse - 123, // 141: liveagent.gateway.v2.AgentEnvelope.goal_manage_resp:type_name -> liveagent.gateway.v2.GoalManageResponse - 129, // 142: liveagent.gateway.v2.AgentEnvelope.loop_manage_resp:type_name -> liveagent.gateway.v2.LoopManageResponse - 145, // 143: liveagent.gateway.v2.AgentEnvelope.proxy_detect_resp:type_name -> liveagent.gateway.v2.ProxyDetectResponse - 103, // 144: liveagent.gateway.v2.AgentEnvelope.history_sync_status:type_name -> liveagent.gateway.v2.HistorySyncStatus - 104, // 145: liveagent.gateway.v2.AgentEnvelope.history_sync_snapshot:type_name -> liveagent.gateway.v2.HistorySyncSnapshot - 8, // 146: liveagent.gateway.v2.ClarifyTurnRequest.runtime_controls:type_name -> liveagent.gateway.v2.ChatRuntimeControls - 13, // 147: liveagent.gateway.v2.UploadReadableFilesRequest.files:type_name -> liveagent.gateway.v2.UploadReadableFile - 12, // 148: liveagent.gateway.v2.UploadReadableFilesResponse.files:type_name -> liveagent.gateway.v2.ChatUploadedFile - 16, // 149: liveagent.gateway.v2.ImportDirectoryRequest.files:type_name -> liveagent.gateway.v2.ImportDirectoryFile - 0, // 150: liveagent.gateway.v2.ImportDirectoryRequest.operation:type_name -> liveagent.gateway.v2.ImportDirectoryOperation - 21, // 151: liveagent.gateway.v2.TunnelDesiredState.tunnels:type_name -> liveagent.gateway.v2.TunnelSpec - 23, // 152: liveagent.gateway.v2.TunnelStatus.local:type_name -> liveagent.gateway.v2.TunnelHealth - 24, // 153: liveagent.gateway.v2.TunnelStateSnapshot.tunnels:type_name -> liveagent.gateway.v2.TunnelStatus - 23, // 154: liveagent.gateway.v2.TunnelStateSnapshot.relay:type_name -> liveagent.gateway.v2.TunnelHealth - 23, // 155: liveagent.gateway.v2.TunnelProbeResult.local:type_name -> liveagent.gateway.v2.TunnelHealth - 28, // 156: liveagent.gateway.v2.TunnelProbeReport.results:type_name -> liveagent.gateway.v2.TunnelProbeResult - 1, // 157: liveagent.gateway.v2.TunnelFrame.kind:type_name -> liveagent.gateway.v2.TunnelFrameKind - 30, // 158: liveagent.gateway.v2.TunnelFrame.headers:type_name -> liveagent.gateway.v2.TunnelHeader - 2, // 159: liveagent.gateway.v2.TunnelFrame.ws_message_type:type_name -> liveagent.gateway.v2.TunnelWsMessageType - 34, // 160: liveagent.gateway.v2.ManagedProcessSnapshot.processes:type_name -> liveagent.gateway.v2.ManagedProcessRecord - 35, // 161: liveagent.gateway.v2.ManagedProcessResponse.snapshot:type_name -> liveagent.gateway.v2.ManagedProcessSnapshot - 42, // 162: liveagent.gateway.v2.TerminalSession.ssh:type_name -> liveagent.gateway.v2.TerminalSshMetadata - 44, // 163: liveagent.gateway.v2.SftpResponse.entries:type_name -> liveagent.gateway.v2.SftpEntry - 44, // 164: liveagent.gateway.v2.SftpResponse.entry:type_name -> liveagent.gateway.v2.SftpEntry - 45, // 165: liveagent.gateway.v2.SftpResponse.transfer:type_name -> liveagent.gateway.v2.SftpTransfer - 45, // 166: liveagent.gateway.v2.SftpEvent.transfer:type_name -> liveagent.gateway.v2.SftpTransfer - 50, // 167: liveagent.gateway.v2.TerminalSshTabsSnapshot.tabs:type_name -> liveagent.gateway.v2.TerminalSshTab - 52, // 168: liveagent.gateway.v2.TerminalSshLocalForwardsSnapshot.forwards:type_name -> liveagent.gateway.v2.TerminalSshLocalForward - 52, // 169: liveagent.gateway.v2.TerminalSshLocalForwardAction.forward:type_name -> liveagent.gateway.v2.TerminalSshLocalForward - 41, // 170: liveagent.gateway.v2.TerminalResponse.sessions:type_name -> liveagent.gateway.v2.TerminalSession - 41, // 171: liveagent.gateway.v2.TerminalResponse.session:type_name -> liveagent.gateway.v2.TerminalSession - 49, // 172: liveagent.gateway.v2.TerminalResponse.shell_options:type_name -> liveagent.gateway.v2.TerminalShellOption - 48, // 173: liveagent.gateway.v2.TerminalResponse.ssh_prompt:type_name -> liveagent.gateway.v2.TerminalSshPrompt - 51, // 174: liveagent.gateway.v2.TerminalResponse.ssh_tabs:type_name -> liveagent.gateway.v2.TerminalSshTabsSnapshot - 53, // 175: liveagent.gateway.v2.TerminalResponse.ssh_local_forwards:type_name -> liveagent.gateway.v2.TerminalSshLocalForwardsSnapshot - 54, // 176: liveagent.gateway.v2.TerminalResponse.ssh_local_forward:type_name -> liveagent.gateway.v2.TerminalSshLocalForwardAction - 41, // 177: liveagent.gateway.v2.TerminalEvent.session:type_name -> liveagent.gateway.v2.TerminalSession - 51, // 178: liveagent.gateway.v2.TerminalEvent.ssh_tabs:type_name -> liveagent.gateway.v2.TerminalSshTabsSnapshot - 54, // 179: liveagent.gateway.v2.TerminalEvent.ssh_local_forward:type_name -> liveagent.gateway.v2.TerminalSshLocalForwardAction - 41, // 180: liveagent.gateway.v2.TerminalStreamFrame.session:type_name -> liveagent.gateway.v2.TerminalSession - 7, // 181: liveagent.gateway.v2.ChatRequest.selected_model:type_name -> liveagent.gateway.v2.ChatSelectedModel - 12, // 182: liveagent.gateway.v2.ChatRequest.uploaded_files:type_name -> liveagent.gateway.v2.ChatUploadedFile - 8, // 183: liveagent.gateway.v2.ChatRequest.runtime_controls:type_name -> liveagent.gateway.v2.ChatRuntimeControls - 199, // 184: liveagent.gateway.v2.ChatRequest.referenced_conversations:type_name -> liveagent.gateway.v2.ChatConversationReference - 60, // 185: liveagent.gateway.v2.ChatCommandRequest.request:type_name -> liveagent.gateway.v2.ChatRequest - 61, // 186: liveagent.gateway.v2.ChatCommandRequest.base_message_ref:type_name -> liveagent.gateway.v2.ChatMessageRef - 62, // 187: liveagent.gateway.v2.ChatCommandRequest.cancel:type_name -> liveagent.gateway.v2.CancelChatRequest - 3, // 188: liveagent.gateway.v2.ChatEvent.type:type_name -> liveagent.gateway.v2.ChatEvent.ChatEventType - 71, // 189: liveagent.gateway.v2.RuntimeStatusEvent.active_runs:type_name -> liveagent.gateway.v2.ChatRunReport - 71, // 190: liveagent.gateway.v2.RuntimeStatusEvent.finished_runs:type_name -> liveagent.gateway.v2.ChatRunReport - 76, // 191: liveagent.gateway.v2.HistoryListResponse.conversations:type_name -> liveagent.gateway.v2.ConversationSummary - 76, // 192: liveagent.gateway.v2.HistoryGetResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 80, // 193: liveagent.gateway.v2.TrajectoryFetchResponse.sections:type_name -> liveagent.gateway.v2.TrajectorySectionPayload - 61, // 194: liveagent.gateway.v2.HistoryPrefixRequest.base_message_ref:type_name -> liveagent.gateway.v2.ChatMessageRef - 76, // 195: liveagent.gateway.v2.HistoryPrefixResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 76, // 196: liveagent.gateway.v2.HistoryRenameResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 61, // 197: liveagent.gateway.v2.HistoryBranchRequest.base_message_ref:type_name -> liveagent.gateway.v2.ChatMessageRef - 76, // 198: liveagent.gateway.v2.HistoryBranchResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 76, // 199: liveagent.gateway.v2.HistoryPinResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 90, // 200: liveagent.gateway.v2.HistoryShareGetResponse.share:type_name -> liveagent.gateway.v2.HistoryShareStatus - 90, // 201: liveagent.gateway.v2.HistoryShareSetResponse.share:type_name -> liveagent.gateway.v2.HistoryShareStatus - 76, // 202: liveagent.gateway.v2.HistoryShareResolveResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 98, // 203: liveagent.gateway.v2.HistoryWorkdirsResponse.workdirs:type_name -> liveagent.gateway.v2.HistoryWorkdirSummary - 76, // 204: liveagent.gateway.v2.HistorySyncEvent.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 76, // 205: liveagent.gateway.v2.HistorySyncSnapshot.conversations:type_name -> liveagent.gateway.v2.ConversationSummary - 98, // 206: liveagent.gateway.v2.HistorySyncSnapshot.workdirs:type_name -> liveagent.gateway.v2.HistoryWorkdirSummary - 118, // 207: liveagent.gateway.v2.PromptTemplateListResponse.templates:type_name -> liveagent.gateway.v2.PromptTemplateSummary - 125, // 208: liveagent.gateway.v2.SlashCommandListResponse.commands:type_name -> liveagent.gateway.v2.SlashCommandSummary - 128, // 209: liveagent.gateway.v2.LoopManageResponse.loops:type_name -> liveagent.gateway.v2.LoopSummary - 137, // 210: liveagent.gateway.v2.FileMentionListResponse.entries:type_name -> liveagent.gateway.v2.FileMentionEntry - 140, // 211: liveagent.gateway.v2.InstalledAppsListResponse.apps:type_name -> liveagent.gateway.v2.InstalledAppEntry - 146, // 212: liveagent.gateway.v2.FsRootsResponse.roots:type_name -> liveagent.gateway.v2.FsRoot - 150, // 213: liveagent.gateway.v2.FsListDirsResponse.entries:type_name -> liveagent.gateway.v2.FsDirEntry - 155, // 214: liveagent.gateway.v2.FsListResponse.entries:type_name -> liveagent.gateway.v2.FsListEntry - 174, // 215: liveagent.gateway.v2.ProviderCustomHeaders.headers:type_name -> liveagent.gateway.v2.ProviderCustomHeader - 175, // 216: liveagent.gateway.v2.ProviderModelsRequest.custom_headers:type_name -> liveagent.gateway.v2.ProviderCustomHeaders - 181, // 217: liveagent.gateway.v2.ChatIngressBatch.records:type_name -> liveagent.gateway.v2.ChatIngressRecord - 182, // 218: liveagent.gateway.v2.ChatIngressRecord.delta:type_name -> liveagent.gateway.v2.ChatIngressDelta - 184, // 219: liveagent.gateway.v2.ChatIngressRecord.checkpoint:type_name -> liveagent.gateway.v2.ChatIngressCheckpoint - 185, // 220: liveagent.gateway.v2.ChatIngressRecord.terminal:type_name -> liveagent.gateway.v2.ChatIngressTerminal - 183, // 221: liveagent.gateway.v2.ChatIngressRecord.heartbeat:type_name -> liveagent.gateway.v2.ChatIngressHeartbeat - 187, // 222: liveagent.gateway.v2.ChatIngressResume.runs:type_name -> liveagent.gateway.v2.ChatIngressRunResume - 4, // 223: liveagent.gateway.v2.ChatIngressAck.action:type_name -> liveagent.gateway.v2.ChatIngressAck.Action - 76, // 224: liveagent.gateway.v2.HistorySetCwdResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary - 192, // 225: liveagent.gateway.v2.WorkspaceRootGrantsRequest.grants:type_name -> liveagent.gateway.v2.WorkspaceRootGrantDraft - 193, // 226: liveagent.gateway.v2.WorkspaceRootGrantsResponse.grants:type_name -> liveagent.gateway.v2.WorkspaceRootGrant - 196, // 227: liveagent.gateway.v2.CheckpointRequest.expected:type_name -> liveagent.gateway.v2.CheckpointExpectedEntry - 228, // [228:228] is the sub-list for method output_type - 228, // [228:228] is the sub-list for method input_type - 228, // [228:228] is the sub-list for extension type_name - 228, // [228:228] is the sub-list for extension extendee - 0, // [0:228] is the sub-list for field type_name + 146, // 65: liveagent.gateway.v2.GatewayEnvelope.permission_rules:type_name -> liveagent.gateway.v2.PermissionRulesRequest + 67, // 66: liveagent.gateway.v2.AgentEnvelope.chat_event:type_name -> liveagent.gateway.v2.ChatEvent + 73, // 67: liveagent.gateway.v2.AgentEnvelope.cron_manage_resp:type_name -> liveagent.gateway.v2.CronManageResponse + 75, // 68: liveagent.gateway.v2.AgentEnvelope.history_list_resp:type_name -> liveagent.gateway.v2.HistoryListResponse + 78, // 69: liveagent.gateway.v2.AgentEnvelope.history_get_resp:type_name -> liveagent.gateway.v2.HistoryGetResponse + 85, // 70: liveagent.gateway.v2.AgentEnvelope.history_rename_resp:type_name -> liveagent.gateway.v2.HistoryRenameResponse + 101, // 71: liveagent.gateway.v2.AgentEnvelope.history_delete_resp:type_name -> liveagent.gateway.v2.HistoryDeleteResponse + 102, // 72: liveagent.gateway.v2.AgentEnvelope.history_sync:type_name -> liveagent.gateway.v2.HistorySyncEvent + 83, // 73: liveagent.gateway.v2.AgentEnvelope.history_prefix_resp:type_name -> liveagent.gateway.v2.HistoryPrefixResponse + 89, // 74: liveagent.gateway.v2.AgentEnvelope.history_pin_resp:type_name -> liveagent.gateway.v2.HistoryPinResponse + 92, // 75: liveagent.gateway.v2.AgentEnvelope.history_share_get_resp:type_name -> liveagent.gateway.v2.HistoryShareGetResponse + 94, // 76: liveagent.gateway.v2.AgentEnvelope.history_share_set_resp:type_name -> liveagent.gateway.v2.HistoryShareSetResponse + 96, // 77: liveagent.gateway.v2.AgentEnvelope.history_share_resolve_resp:type_name -> liveagent.gateway.v2.HistoryShareResolveResponse + 99, // 78: liveagent.gateway.v2.AgentEnvelope.history_workdirs_resp:type_name -> liveagent.gateway.v2.HistoryWorkdirsResponse + 107, // 79: liveagent.gateway.v2.AgentEnvelope.provider_list_resp:type_name -> liveagent.gateway.v2.ProviderListResponse + 109, // 80: liveagent.gateway.v2.AgentEnvelope.settings_get_resp:type_name -> liveagent.gateway.v2.SettingsGetResponse + 111, // 81: liveagent.gateway.v2.AgentEnvelope.settings_update_resp:type_name -> liveagent.gateway.v2.SettingsUpdateResponse + 114, // 82: liveagent.gateway.v2.AgentEnvelope.settings_sync:type_name -> liveagent.gateway.v2.SettingsSyncEvent + 116, // 83: liveagent.gateway.v2.AgentEnvelope.skill_files_list_resp:type_name -> liveagent.gateway.v2.SkillFilesListResponse + 131, // 84: liveagent.gateway.v2.AgentEnvelope.skill_metadata_read_resp:type_name -> liveagent.gateway.v2.SkillMetadataReadResponse + 133, // 85: liveagent.gateway.v2.AgentEnvelope.skill_text_read_resp:type_name -> liveagent.gateway.v2.SkillTextReadResponse + 138, // 86: liveagent.gateway.v2.AgentEnvelope.file_mention_list_resp:type_name -> liveagent.gateway.v2.FileMentionListResponse + 15, // 87: liveagent.gateway.v2.AgentEnvelope.upload_readable_files_resp:type_name -> liveagent.gateway.v2.UploadReadableFilesResponse + 150, // 88: liveagent.gateway.v2.AgentEnvelope.fs_roots_resp:type_name -> liveagent.gateway.v2.FsRootsResponse + 174, // 89: liveagent.gateway.v2.AgentEnvelope.pong:type_name -> liveagent.gateway.v2.PongResponse + 153, // 90: liveagent.gateway.v2.AgentEnvelope.fs_list_dirs_resp:type_name -> liveagent.gateway.v2.FsListDirsResponse + 20, // 91: liveagent.gateway.v2.AgentEnvelope.uploaded_image_preview_resp:type_name -> liveagent.gateway.v2.UploadedImagePreviewResponse + 39, // 92: liveagent.gateway.v2.AgentEnvelope.memory_manage_resp:type_name -> liveagent.gateway.v2.MemoryManageResponse + 135, // 93: liveagent.gateway.v2.AgentEnvelope.skill_manage_resp:type_name -> liveagent.gateway.v2.SkillManageResponse + 155, // 94: liveagent.gateway.v2.AgentEnvelope.fs_create_project_folder_resp:type_name -> liveagent.gateway.v2.FsCreateProjectFolderResponse + 55, // 95: liveagent.gateway.v2.AgentEnvelope.terminal_response:type_name -> liveagent.gateway.v2.TerminalResponse + 56, // 96: liveagent.gateway.v2.AgentEnvelope.terminal_event:type_name -> liveagent.gateway.v2.TerminalEvent + 158, // 97: liveagent.gateway.v2.AgentEnvelope.fs_list_resp:type_name -> liveagent.gateway.v2.FsListResponse + 166, // 98: liveagent.gateway.v2.AgentEnvelope.fs_write_text_resp:type_name -> liveagent.gateway.v2.FsWriteTextResponse + 168, // 99: liveagent.gateway.v2.AgentEnvelope.fs_create_dir_resp:type_name -> liveagent.gateway.v2.FsCreateDirResponse + 170, // 100: liveagent.gateway.v2.AgentEnvelope.fs_rename_resp:type_name -> liveagent.gateway.v2.FsRenameResponse + 172, // 101: liveagent.gateway.v2.AgentEnvelope.fs_delete_resp:type_name -> liveagent.gateway.v2.FsDeleteResponse + 59, // 102: liveagent.gateway.v2.AgentEnvelope.git_response:type_name -> liveagent.gateway.v2.GitResponse + 160, // 103: liveagent.gateway.v2.AgentEnvelope.fs_read_editable_text_resp:type_name -> liveagent.gateway.v2.FsReadEditableTextResponse + 162, // 104: liveagent.gateway.v2.AgentEnvelope.fs_read_workspace_image_resp:type_name -> liveagent.gateway.v2.FsReadWorkspaceImageResponse + 46, // 105: liveagent.gateway.v2.AgentEnvelope.sftp_response:type_name -> liveagent.gateway.v2.SftpResponse + 47, // 106: liveagent.gateway.v2.AgentEnvelope.sftp_event:type_name -> liveagent.gateway.v2.SftpEvent + 65, // 107: liveagent.gateway.v2.AgentEnvelope.chat_queue_resp:type_name -> liveagent.gateway.v2.ChatQueueResponse + 66, // 108: liveagent.gateway.v2.AgentEnvelope.chat_queue_event:type_name -> liveagent.gateway.v2.ChatQueueEvent + 68, // 109: liveagent.gateway.v2.AgentEnvelope.chat_control:type_name -> liveagent.gateway.v2.ChatControlEvent + 70, // 110: liveagent.gateway.v2.AgentEnvelope.runtime_status:type_name -> liveagent.gateway.v2.RuntimeStatusEvent + 113, // 111: liveagent.gateway.v2.AgentEnvelope.settings_reset_ssh_known_host_resp:type_name -> liveagent.gateway.v2.SettingsResetSshKnownHostResponse + 69, // 112: liveagent.gateway.v2.AgentEnvelope.chat_runtime_snapshot:type_name -> liveagent.gateway.v2.ChatRuntimeSnapshot + 179, // 113: liveagent.gateway.v2.AgentEnvelope.provider_models_resp:type_name -> liveagent.gateway.v2.ProviderModelsResponse + 22, // 114: liveagent.gateway.v2.AgentEnvelope.tunnel_desired:type_name -> liveagent.gateway.v2.TunnelDesiredState + 27, // 115: liveagent.gateway.v2.AgentEnvelope.tunnel_mutation_result:type_name -> liveagent.gateway.v2.TunnelMutationResult + 31, // 116: liveagent.gateway.v2.AgentEnvelope.tunnel_frame:type_name -> liveagent.gateway.v2.TunnelFrame + 29, // 117: liveagent.gateway.v2.AgentEnvelope.tunnel_probe_report:type_name -> liveagent.gateway.v2.TunnelProbeReport + 33, // 118: liveagent.gateway.v2.AgentEnvelope.workspace_activity:type_name -> liveagent.gateway.v2.WorkspaceActivityEvent + 37, // 119: liveagent.gateway.v2.AgentEnvelope.managed_process_response:type_name -> liveagent.gateway.v2.ManagedProcessResponse + 35, // 120: liveagent.gateway.v2.AgentEnvelope.managed_process_snapshot:type_name -> liveagent.gateway.v2.ManagedProcessSnapshot + 87, // 121: liveagent.gateway.v2.AgentEnvelope.history_branch_resp:type_name -> liveagent.gateway.v2.HistoryBranchResponse + 181, // 122: liveagent.gateway.v2.AgentEnvelope.provider_usage_resp:type_name -> liveagent.gateway.v2.ProviderUsageResponse + 182, // 123: liveagent.gateway.v2.AgentEnvelope.chat_ingress_batch:type_name -> liveagent.gateway.v2.ChatIngressBatch + 188, // 124: liveagent.gateway.v2.AgentEnvelope.chat_ingress_resume:type_name -> liveagent.gateway.v2.ChatIngressResume + 190, // 125: liveagent.gateway.v2.AgentEnvelope.chat_ingress_fragment:type_name -> liveagent.gateway.v2.ChatIngressFragment + 164, // 126: liveagent.gateway.v2.AgentEnvelope.chat_file_open_resp:type_name -> liveagent.gateway.v2.ChatFileOpenResponse + 175, // 127: liveagent.gateway.v2.AgentEnvelope.error:type_name -> liveagent.gateway.v2.ErrorResponse + 193, // 128: liveagent.gateway.v2.AgentEnvelope.history_set_cwd_resp:type_name -> liveagent.gateway.v2.HistorySetCwdResponse + 197, // 129: liveagent.gateway.v2.AgentEnvelope.workspace_root_grants_resp:type_name -> liveagent.gateway.v2.WorkspaceRootGrantsResponse + 18, // 130: liveagent.gateway.v2.AgentEnvelope.import_directory_resp:type_name -> liveagent.gateway.v2.ImportDirectoryResponse + 200, // 131: liveagent.gateway.v2.AgentEnvelope.checkpoint_resp:type_name -> liveagent.gateway.v2.CheckpointResponse + 81, // 132: liveagent.gateway.v2.AgentEnvelope.trajectory_fetch_resp:type_name -> liveagent.gateway.v2.TrajectoryFetchResponse + 141, // 133: liveagent.gateway.v2.AgentEnvelope.installed_apps_list_resp:type_name -> liveagent.gateway.v2.InstalledAppsListResponse + 10, // 134: liveagent.gateway.v2.AgentEnvelope.clarify_turn_resp:type_name -> liveagent.gateway.v2.ClarifyTurnResponse + 143, // 135: liveagent.gateway.v2.AgentEnvelope.cua_driver_resp:type_name -> liveagent.gateway.v2.CuaDriverResponse + 11, // 136: liveagent.gateway.v2.AgentEnvelope.clarify_turn_delta:type_name -> liveagent.gateway.v2.ClarifyTurnDelta + 203, // 137: liveagent.gateway.v2.AgentEnvelope.planning_resp:type_name -> liveagent.gateway.v2.PlanningResponse + 204, // 138: liveagent.gateway.v2.AgentEnvelope.planning_changed:type_name -> liveagent.gateway.v2.PlanningChanged + 119, // 139: liveagent.gateway.v2.AgentEnvelope.prompt_template_list_resp:type_name -> liveagent.gateway.v2.PromptTemplateListResponse + 121, // 140: liveagent.gateway.v2.AgentEnvelope.prompt_template_expand_resp:type_name -> liveagent.gateway.v2.PromptTemplateExpandResponse + 126, // 141: liveagent.gateway.v2.AgentEnvelope.slash_command_list_resp:type_name -> liveagent.gateway.v2.SlashCommandListResponse + 123, // 142: liveagent.gateway.v2.AgentEnvelope.goal_manage_resp:type_name -> liveagent.gateway.v2.GoalManageResponse + 129, // 143: liveagent.gateway.v2.AgentEnvelope.loop_manage_resp:type_name -> liveagent.gateway.v2.LoopManageResponse + 145, // 144: liveagent.gateway.v2.AgentEnvelope.proxy_detect_resp:type_name -> liveagent.gateway.v2.ProxyDetectResponse + 103, // 145: liveagent.gateway.v2.AgentEnvelope.history_sync_status:type_name -> liveagent.gateway.v2.HistorySyncStatus + 104, // 146: liveagent.gateway.v2.AgentEnvelope.history_sync_snapshot:type_name -> liveagent.gateway.v2.HistorySyncSnapshot + 147, // 147: liveagent.gateway.v2.AgentEnvelope.permission_rules_resp:type_name -> liveagent.gateway.v2.PermissionRulesResponse + 8, // 148: liveagent.gateway.v2.ClarifyTurnRequest.runtime_controls:type_name -> liveagent.gateway.v2.ChatRuntimeControls + 13, // 149: liveagent.gateway.v2.UploadReadableFilesRequest.files:type_name -> liveagent.gateway.v2.UploadReadableFile + 12, // 150: liveagent.gateway.v2.UploadReadableFilesResponse.files:type_name -> liveagent.gateway.v2.ChatUploadedFile + 16, // 151: liveagent.gateway.v2.ImportDirectoryRequest.files:type_name -> liveagent.gateway.v2.ImportDirectoryFile + 0, // 152: liveagent.gateway.v2.ImportDirectoryRequest.operation:type_name -> liveagent.gateway.v2.ImportDirectoryOperation + 21, // 153: liveagent.gateway.v2.TunnelDesiredState.tunnels:type_name -> liveagent.gateway.v2.TunnelSpec + 23, // 154: liveagent.gateway.v2.TunnelStatus.local:type_name -> liveagent.gateway.v2.TunnelHealth + 24, // 155: liveagent.gateway.v2.TunnelStateSnapshot.tunnels:type_name -> liveagent.gateway.v2.TunnelStatus + 23, // 156: liveagent.gateway.v2.TunnelStateSnapshot.relay:type_name -> liveagent.gateway.v2.TunnelHealth + 23, // 157: liveagent.gateway.v2.TunnelProbeResult.local:type_name -> liveagent.gateway.v2.TunnelHealth + 28, // 158: liveagent.gateway.v2.TunnelProbeReport.results:type_name -> liveagent.gateway.v2.TunnelProbeResult + 1, // 159: liveagent.gateway.v2.TunnelFrame.kind:type_name -> liveagent.gateway.v2.TunnelFrameKind + 30, // 160: liveagent.gateway.v2.TunnelFrame.headers:type_name -> liveagent.gateway.v2.TunnelHeader + 2, // 161: liveagent.gateway.v2.TunnelFrame.ws_message_type:type_name -> liveagent.gateway.v2.TunnelWsMessageType + 34, // 162: liveagent.gateway.v2.ManagedProcessSnapshot.processes:type_name -> liveagent.gateway.v2.ManagedProcessRecord + 35, // 163: liveagent.gateway.v2.ManagedProcessResponse.snapshot:type_name -> liveagent.gateway.v2.ManagedProcessSnapshot + 42, // 164: liveagent.gateway.v2.TerminalSession.ssh:type_name -> liveagent.gateway.v2.TerminalSshMetadata + 44, // 165: liveagent.gateway.v2.SftpResponse.entries:type_name -> liveagent.gateway.v2.SftpEntry + 44, // 166: liveagent.gateway.v2.SftpResponse.entry:type_name -> liveagent.gateway.v2.SftpEntry + 45, // 167: liveagent.gateway.v2.SftpResponse.transfer:type_name -> liveagent.gateway.v2.SftpTransfer + 45, // 168: liveagent.gateway.v2.SftpEvent.transfer:type_name -> liveagent.gateway.v2.SftpTransfer + 50, // 169: liveagent.gateway.v2.TerminalSshTabsSnapshot.tabs:type_name -> liveagent.gateway.v2.TerminalSshTab + 52, // 170: liveagent.gateway.v2.TerminalSshLocalForwardsSnapshot.forwards:type_name -> liveagent.gateway.v2.TerminalSshLocalForward + 52, // 171: liveagent.gateway.v2.TerminalSshLocalForwardAction.forward:type_name -> liveagent.gateway.v2.TerminalSshLocalForward + 41, // 172: liveagent.gateway.v2.TerminalResponse.sessions:type_name -> liveagent.gateway.v2.TerminalSession + 41, // 173: liveagent.gateway.v2.TerminalResponse.session:type_name -> liveagent.gateway.v2.TerminalSession + 49, // 174: liveagent.gateway.v2.TerminalResponse.shell_options:type_name -> liveagent.gateway.v2.TerminalShellOption + 48, // 175: liveagent.gateway.v2.TerminalResponse.ssh_prompt:type_name -> liveagent.gateway.v2.TerminalSshPrompt + 51, // 176: liveagent.gateway.v2.TerminalResponse.ssh_tabs:type_name -> liveagent.gateway.v2.TerminalSshTabsSnapshot + 53, // 177: liveagent.gateway.v2.TerminalResponse.ssh_local_forwards:type_name -> liveagent.gateway.v2.TerminalSshLocalForwardsSnapshot + 54, // 178: liveagent.gateway.v2.TerminalResponse.ssh_local_forward:type_name -> liveagent.gateway.v2.TerminalSshLocalForwardAction + 41, // 179: liveagent.gateway.v2.TerminalEvent.session:type_name -> liveagent.gateway.v2.TerminalSession + 51, // 180: liveagent.gateway.v2.TerminalEvent.ssh_tabs:type_name -> liveagent.gateway.v2.TerminalSshTabsSnapshot + 54, // 181: liveagent.gateway.v2.TerminalEvent.ssh_local_forward:type_name -> liveagent.gateway.v2.TerminalSshLocalForwardAction + 41, // 182: liveagent.gateway.v2.TerminalStreamFrame.session:type_name -> liveagent.gateway.v2.TerminalSession + 7, // 183: liveagent.gateway.v2.ChatRequest.selected_model:type_name -> liveagent.gateway.v2.ChatSelectedModel + 12, // 184: liveagent.gateway.v2.ChatRequest.uploaded_files:type_name -> liveagent.gateway.v2.ChatUploadedFile + 8, // 185: liveagent.gateway.v2.ChatRequest.runtime_controls:type_name -> liveagent.gateway.v2.ChatRuntimeControls + 201, // 186: liveagent.gateway.v2.ChatRequest.referenced_conversations:type_name -> liveagent.gateway.v2.ChatConversationReference + 60, // 187: liveagent.gateway.v2.ChatCommandRequest.request:type_name -> liveagent.gateway.v2.ChatRequest + 61, // 188: liveagent.gateway.v2.ChatCommandRequest.base_message_ref:type_name -> liveagent.gateway.v2.ChatMessageRef + 62, // 189: liveagent.gateway.v2.ChatCommandRequest.cancel:type_name -> liveagent.gateway.v2.CancelChatRequest + 3, // 190: liveagent.gateway.v2.ChatEvent.type:type_name -> liveagent.gateway.v2.ChatEvent.ChatEventType + 71, // 191: liveagent.gateway.v2.RuntimeStatusEvent.active_runs:type_name -> liveagent.gateway.v2.ChatRunReport + 71, // 192: liveagent.gateway.v2.RuntimeStatusEvent.finished_runs:type_name -> liveagent.gateway.v2.ChatRunReport + 76, // 193: liveagent.gateway.v2.HistoryListResponse.conversations:type_name -> liveagent.gateway.v2.ConversationSummary + 76, // 194: liveagent.gateway.v2.HistoryGetResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 80, // 195: liveagent.gateway.v2.TrajectoryFetchResponse.sections:type_name -> liveagent.gateway.v2.TrajectorySectionPayload + 61, // 196: liveagent.gateway.v2.HistoryPrefixRequest.base_message_ref:type_name -> liveagent.gateway.v2.ChatMessageRef + 76, // 197: liveagent.gateway.v2.HistoryPrefixResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 76, // 198: liveagent.gateway.v2.HistoryRenameResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 61, // 199: liveagent.gateway.v2.HistoryBranchRequest.base_message_ref:type_name -> liveagent.gateway.v2.ChatMessageRef + 76, // 200: liveagent.gateway.v2.HistoryBranchResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 76, // 201: liveagent.gateway.v2.HistoryPinResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 90, // 202: liveagent.gateway.v2.HistoryShareGetResponse.share:type_name -> liveagent.gateway.v2.HistoryShareStatus + 90, // 203: liveagent.gateway.v2.HistoryShareSetResponse.share:type_name -> liveagent.gateway.v2.HistoryShareStatus + 76, // 204: liveagent.gateway.v2.HistoryShareResolveResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 98, // 205: liveagent.gateway.v2.HistoryWorkdirsResponse.workdirs:type_name -> liveagent.gateway.v2.HistoryWorkdirSummary + 76, // 206: liveagent.gateway.v2.HistorySyncEvent.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 76, // 207: liveagent.gateway.v2.HistorySyncSnapshot.conversations:type_name -> liveagent.gateway.v2.ConversationSummary + 98, // 208: liveagent.gateway.v2.HistorySyncSnapshot.workdirs:type_name -> liveagent.gateway.v2.HistoryWorkdirSummary + 118, // 209: liveagent.gateway.v2.PromptTemplateListResponse.templates:type_name -> liveagent.gateway.v2.PromptTemplateSummary + 125, // 210: liveagent.gateway.v2.SlashCommandListResponse.commands:type_name -> liveagent.gateway.v2.SlashCommandSummary + 128, // 211: liveagent.gateway.v2.LoopManageResponse.loops:type_name -> liveagent.gateway.v2.LoopSummary + 137, // 212: liveagent.gateway.v2.FileMentionListResponse.entries:type_name -> liveagent.gateway.v2.FileMentionEntry + 140, // 213: liveagent.gateway.v2.InstalledAppsListResponse.apps:type_name -> liveagent.gateway.v2.InstalledAppEntry + 148, // 214: liveagent.gateway.v2.FsRootsResponse.roots:type_name -> liveagent.gateway.v2.FsRoot + 152, // 215: liveagent.gateway.v2.FsListDirsResponse.entries:type_name -> liveagent.gateway.v2.FsDirEntry + 157, // 216: liveagent.gateway.v2.FsListResponse.entries:type_name -> liveagent.gateway.v2.FsListEntry + 176, // 217: liveagent.gateway.v2.ProviderCustomHeaders.headers:type_name -> liveagent.gateway.v2.ProviderCustomHeader + 177, // 218: liveagent.gateway.v2.ProviderModelsRequest.custom_headers:type_name -> liveagent.gateway.v2.ProviderCustomHeaders + 183, // 219: liveagent.gateway.v2.ChatIngressBatch.records:type_name -> liveagent.gateway.v2.ChatIngressRecord + 184, // 220: liveagent.gateway.v2.ChatIngressRecord.delta:type_name -> liveagent.gateway.v2.ChatIngressDelta + 186, // 221: liveagent.gateway.v2.ChatIngressRecord.checkpoint:type_name -> liveagent.gateway.v2.ChatIngressCheckpoint + 187, // 222: liveagent.gateway.v2.ChatIngressRecord.terminal:type_name -> liveagent.gateway.v2.ChatIngressTerminal + 185, // 223: liveagent.gateway.v2.ChatIngressRecord.heartbeat:type_name -> liveagent.gateway.v2.ChatIngressHeartbeat + 189, // 224: liveagent.gateway.v2.ChatIngressResume.runs:type_name -> liveagent.gateway.v2.ChatIngressRunResume + 4, // 225: liveagent.gateway.v2.ChatIngressAck.action:type_name -> liveagent.gateway.v2.ChatIngressAck.Action + 76, // 226: liveagent.gateway.v2.HistorySetCwdResponse.conversation:type_name -> liveagent.gateway.v2.ConversationSummary + 194, // 227: liveagent.gateway.v2.WorkspaceRootGrantsRequest.grants:type_name -> liveagent.gateway.v2.WorkspaceRootGrantDraft + 195, // 228: liveagent.gateway.v2.WorkspaceRootGrantsResponse.grants:type_name -> liveagent.gateway.v2.WorkspaceRootGrant + 198, // 229: liveagent.gateway.v2.CheckpointRequest.expected:type_name -> liveagent.gateway.v2.CheckpointExpectedEntry + 230, // [230:230] is the sub-list for method output_type + 230, // [230:230] is the sub-list for method input_type + 230, // [230:230] is the sub-list for extension type_name + 230, // [230:230] is the sub-list for extension extendee + 0, // [0:230] is the sub-list for field type_name } func init() { file_proto_v2_gateway_proto_init() } @@ -18339,6 +18486,7 @@ func file_proto_v2_gateway_proto_init() { (*GatewayEnvelope_LoopManage)(nil), (*GatewayEnvelope_ProxyDetect)(nil), (*GatewayEnvelope_HistoryResync)(nil), + (*GatewayEnvelope_PermissionRules)(nil), } file_proto_v2_gateway_proto_msgTypes[1].OneofWrappers = []any{ (*AgentEnvelope_ChatEvent)(nil), @@ -18422,30 +18570,31 @@ func file_proto_v2_gateway_proto_init() { (*AgentEnvelope_ProxyDetectResp)(nil), (*AgentEnvelope_HistorySyncStatus)(nil), (*AgentEnvelope_HistorySyncSnapshot)(nil), + (*AgentEnvelope_PermissionRulesResp)(nil), } file_proto_v2_gateway_proto_msgTypes[21].OneofWrappers = []any{} file_proto_v2_gateway_proto_msgTypes[29].OneofWrappers = []any{} file_proto_v2_gateway_proto_msgTypes[74].OneofWrappers = []any{} file_proto_v2_gateway_proto_msgTypes[88].OneofWrappers = []any{} file_proto_v2_gateway_proto_msgTypes[131].OneofWrappers = []any{} - file_proto_v2_gateway_proto_msgTypes[149].OneofWrappers = []any{} - file_proto_v2_gateway_proto_msgTypes[156].OneofWrappers = []any{} - file_proto_v2_gateway_proto_msgTypes[157].OneofWrappers = []any{} - file_proto_v2_gateway_proto_msgTypes[171].OneofWrappers = []any{} - file_proto_v2_gateway_proto_msgTypes[176].OneofWrappers = []any{ + file_proto_v2_gateway_proto_msgTypes[151].OneofWrappers = []any{} + file_proto_v2_gateway_proto_msgTypes[158].OneofWrappers = []any{} + file_proto_v2_gateway_proto_msgTypes[159].OneofWrappers = []any{} + file_proto_v2_gateway_proto_msgTypes[173].OneofWrappers = []any{} + file_proto_v2_gateway_proto_msgTypes[178].OneofWrappers = []any{ (*ChatIngressRecord_Delta)(nil), (*ChatIngressRecord_Checkpoint)(nil), (*ChatIngressRecord_Terminal)(nil), (*ChatIngressRecord_Heartbeat)(nil), } - file_proto_v2_gateway_proto_msgTypes[187].OneofWrappers = []any{} + file_proto_v2_gateway_proto_msgTypes[189].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_v2_gateway_proto_rawDesc), len(file_proto_v2_gateway_proto_rawDesc)), NumEnums: 5, - NumMessages: 198, + NumMessages: 200, NumExtensions: 0, NumServices: 0, }, diff --git a/crates/agent-gateway/internal/protocol/pbws/browser_relay.go b/crates/agent-gateway/internal/protocol/pbws/browser_relay.go index 72e2fb43d..3f88a1b44 100644 --- a/crates/agent-gateway/internal/protocol/pbws/browser_relay.go +++ b/crates/agent-gateway/internal/protocol/pbws/browser_relay.go @@ -175,6 +175,17 @@ func (c *browserConn) handleKBrainMemoryManage(requestID, agentID string, reques c.sendKBrainResponse(requestID, agentID, &gatewayv2.AgentEnvelope{Payload: &gatewayv2.AgentEnvelope_MemoryManageResp{MemoryManageResp: response}}) } +func (c *browserConn) handleKBrainPermissionRules(requestID, agentID string, request *gatewayv2.PermissionRulesRequest) { + ctx, cancel := context.WithTimeout(context.Background(), c.srv.requestTimeout()) + defer cancel() + response, err := c.srv.kbrainRelay.PermissionRules(ctx, request) + if err != nil { + _ = c.sendLocalError(requestID, errorMessage(err)) + return + } + c.sendKBrainResponse(requestID, agentID, &gatewayv2.AgentEnvelope{Payload: &gatewayv2.AgentEnvelope_PermissionRulesResp{PermissionRulesResp: response}}) +} + func (c *browserConn) handleKBrainChatQueue(requestID, agentID string, request *gatewayv2.ChatQueueRequest) { ctx, cancel := context.WithTimeout(context.Background(), c.srv.requestTimeout()) defer cancel() @@ -273,6 +284,14 @@ func (c *browserConn) handleAgentRequest(requestID, agentID string, env *gateway c.handleKBrainMemoryManage(requestID, agentID, memoryManage) return } + if permissionRules := env.GetPermissionRules(); permissionRules != nil { + if err := vetPermissionRules(permissionRules); err != nil { + _ = c.sendLocalError(requestID, err.Error()) + return + } + c.handleKBrainPermissionRules(requestID, agentID, permissionRules) + return + } if historyList := env.GetHistoryList(); historyList != nil { c.handleKBrainHistoryList(requestID, agentID, historyList) return diff --git a/crates/agent-gateway/internal/protocol/pbws/guard.go b/crates/agent-gateway/internal/protocol/pbws/guard.go index f6499c8f0..e8221e76d 100644 --- a/crates/agent-gateway/internal/protocol/pbws/guard.go +++ b/crates/agent-gateway/internal/protocol/pbws/guard.go @@ -1,6 +1,7 @@ package pbws import ( + "encoding/json" "errors" "strings" @@ -18,12 +19,14 @@ const ( defaultHistoryListPageSize = 80 maxWorkspaceRootGrants = 64 maxSlashCommandWorkdirLen = 32768 + maxPermissionRulesInputLen = 32768 ) const ( errComposerCommandsUnsupported = "desktop app does not support composer commands; update LiveAgent on the desktop" errComposerGoalUnsupported = "/goal is not available on a direct K-brain target" errProxyDetectUnsupported = "desktop app does not support proxy detection; update LiveAgent on the desktop" + errPermissionRulesUnsupported = "desktop app does not support permission rules; update LiveAgent on the desktop" ) func requireComposerCommands(sm session.AgentView) error { @@ -123,6 +126,12 @@ func vetAgentRequest(sm session.AgentView, env *gatewayv2.GatewayEnvelope) error return errors.New(errProxyDetectUnsupported) } return vetProxyDetect(payload.ProxyDetect) + // 权限规则(设置 → 权限):只发给声明了 PERMISSION_RULES_V1 的桌面端,理由同上。 + case *gatewayv2.GatewayEnvelope_PermissionRules: + if !sm.SupportsCapability(gatewayv2.PermissionRulesV1Capability) { + return errors.New(errPermissionRulesUnsupported) + } + return vetPermissionRules(payload.PermissionRules) // ---- 带功能门控 / 限额的直通臂 ---- case *gatewayv2.GatewayEnvelope_GitRequest: @@ -186,6 +195,26 @@ func vetCuaDriver(req *gatewayv2.CuaDriverRequest) error { } } +// 权限规则:只放行 list/add/delete,参数是 JSON 对象且限长(解释器命令的建议规则 +// 可能和命令一样长)。字段白名单与作用域由桌面端和直连 K-brain 的 relay 再校验。 +func vetPermissionRules(req *gatewayv2.PermissionRulesRequest) error { + if req == nil || len(req.GetInputJson()) > maxPermissionRulesInputLen { + return errors.New("invalid permission_rules request") + } + switch strings.TrimSpace(req.GetAction()) { + case "list", "add", "delete": + default: + return errors.New("unsupported permission_rules action") + } + if input := strings.TrimSpace(req.GetInputJson()); input != "" { + var object map[string]json.RawMessage + if err := json.Unmarshal([]byte(input), &object); err != nil || object == nil { + return errors.New("permission_rules input must be a JSON object") + } + } + return nil +} + // 代理服务检测:只放行检测与测速两个只读动作,测速参数限长;桌面端另外只接受 // 自己检测到过的代理地址。 func vetProxyDetect(req *gatewayv2.ProxyDetectRequest) error { diff --git a/crates/agent-gateway/internal/protocol/pbws/guard_test.go b/crates/agent-gateway/internal/protocol/pbws/guard_test.go index 719327e0e..4819cf715 100644 --- a/crates/agent-gateway/internal/protocol/pbws/guard_test.go +++ b/crates/agent-gateway/internal/protocol/pbws/guard_test.go @@ -463,3 +463,52 @@ func TestVetAgentRequestRejectsInvalidGoalAndLoopRequests(t *testing.T) { } } } + +// 权限规则(设置 → 权限):只放行 list/add/delete,参数必须是 JSON 对象且限长。 +func TestVetAgentRequestPermissionRulesAllowsOnlyListAddDelete(t *testing.T) { + view := permissionRulesAgentView(t) + for _, req := range []*gatewayv2.PermissionRulesRequest{ + {Action: "list", InputJson: `{"workspace":"D:\\work\\a"}`}, + {Action: "list"}, + {Action: "add", InputJson: `{"rule":"Bash(rm:*)","effect":"deny","scope":"user"}`}, + {Action: "delete", InputJson: `{"id":"r_1","scope":"project","workspace":"/w"}`}, + } { + env := &gatewayv2.GatewayEnvelope{Payload: &gatewayv2.GatewayEnvelope_PermissionRules{PermissionRules: req}} + if err := vetAgentRequest(view, env); err != nil { + t.Fatalf("vetAgentRequest(%q) error = %v", req.GetAction(), err) + } + } + for _, req := range []*gatewayv2.PermissionRulesRequest{ + {Action: "update", InputJson: `{}`}, + {Action: ""}, + {Action: "add", InputJson: `["Bash(rm:*)"]`}, + {Action: "add", InputJson: `{not json`}, + {Action: "add", InputJson: `{"rule":"` + strings.Repeat("x", 40000) + `"}`}, + } { + env := &gatewayv2.GatewayEnvelope{Payload: &gatewayv2.GatewayEnvelope_PermissionRules{PermissionRules: req}} + if err := vetAgentRequest(view, env); err == nil { + t.Fatalf("vetAgentRequest(action=%q, len=%d) expected rejection", req.GetAction(), len(req.GetInputJson())) + } + } +} + +// 旧桌面端不认识 permission_rules 载荷臂,会静默丢弃;网关要立即拒绝并提示更新桌面端。 +func TestVetAgentRequestPermissionRulesRequiresDesktopCapability(t *testing.T) { + env := &gatewayv2.GatewayEnvelope{ + Payload: &gatewayv2.GatewayEnvelope_PermissionRules{PermissionRules: &gatewayv2.PermissionRulesRequest{Action: "list"}}, + } + err := vetAgentRequest(proxyDetectAgentView(t), env) + if err == nil || !strings.Contains(err.Error(), "update LiveAgent") { + t.Fatalf("vetAgentRequest() error = %v, want capability rejection", err) + } +} + +func permissionRulesAgentView(t *testing.T) session.AgentView { + t.Helper() + manager := session.NewManager() + agent := session.NewAgentSession(session.AuthSnapshot{AgentID: "agent-1", SessionID: "session-1"}) + agent.SetCapabilities([]string{gatewayv2.PermissionRulesV1Capability}) + manager.SetSession(agent) + t.Cleanup(func() { manager.ClearSession(agent) }) + return manager.AgentView("agent-1") +} diff --git a/crates/agent-gateway/proto/v2/gateway.proto b/crates/agent-gateway/proto/v2/gateway.proto index 71ada6d74..c3dd03454 100644 --- a/crates/agent-gateway/proto/v2/gateway.proto +++ b/crates/agent-gateway/proto/v2/gateway.proto @@ -77,6 +77,7 @@ message GatewayEnvelope { LoopManageRequest loop_manage = 108; ProxyDetectRequest proxy_detect = 109; HistoryResyncRequest history_resync = 110; + PermissionRulesRequest permission_rules = 111; } // Legacy tunnel control/frame payloads (pre-rewrite protocol) and the @@ -170,6 +171,7 @@ message AgentEnvelope { ProxyDetectResponse proxy_detect_resp = 116; HistorySyncStatus history_sync_status = 117; HistorySyncSnapshot history_sync_snapshot = 118; + PermissionRulesResponse permission_rules_resp = 119; } // Legacy tunnel control/frame payloads (pre-rewrite protocol) and the @@ -1335,6 +1337,21 @@ message ProxyDetectResponse { string result_json = 2; } +// 设置 → 权限:查看、添加、删除 K-brain 记住的权限规则(/v1/permissions/rules)。 +// 桌面端或直连 K-brain 的网关只转发设置页需要的字段,会话规则不经这里。 +message PermissionRulesRequest { + // "list" | "add" | "delete"。 + string action = 1; + // list: {"workspace"?};add: {"rule","effect":"allow"|"deny","scope":"project"|"user","workspace"?}; + // delete: {"id","scope":"project"|"user","workspace"?}。project 作用域必须带 workspace。 + string input_json = 2; +} + +message PermissionRulesResponse { + // K-brain 的应答 JSON 原样返回:list 为 {project_root?, rules[]},add 为新规则,delete 为 {ok}。 + string result_json = 1; +} + message FsRoot { string id = 1; string path = 2; diff --git a/crates/agent-gateway/test/websocket/review_la3b_permission_rules_test.go b/crates/agent-gateway/test/websocket/review_la3b_permission_rules_test.go new file mode 100644 index 000000000..d7ceacfd7 --- /dev/null +++ b/crates/agent-gateway/test/websocket/review_la3b_permission_rules_test.go @@ -0,0 +1,67 @@ +package websocket_test + +import ( + "strings" + "testing" + "time" + + gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" + "github.com/liveagent/agent-gateway/internal/protocol/pbws" + "github.com/liveagent/agent-gateway/internal/session" +) + +// LA-3b review: the desktop arm through the real browser handler. An old desktop is +// refused before anything is queued for it; a desktop with PERMISSION_RULES_V1 gets +// the request byte for byte, and both its answer and its error reach the browser. +func TestReviewLA3bPermissionRulesDesktopArm(t *testing.T) { + sm := session.NewManager() + sm.RecordAuthentication("desktop-agent", "0.9.0", "session-1") + agentSession := session.NewAgentSession(sm.LatestAuthSnapshot("desktop-agent")) + agentSession.SetCapabilities([]string{gatewayv2.ChatIngressV1Capability}) + sm.SetSession(agentSession) + conn, cleanup := dialV2(t, pbws.NewServer(newV2TestConfig(), sm, nil).BrowserHandler()) + defer cleanup() + helloV2(t, conn, "ws-token") + + const input = `{"workspace":"D:\\项目\\a b+c","rule":"Bash(rm:*)","effect":"deny","scope":"project"}` + send := func(id string) { + sendProtoFrame(t, conn, &gatewayv2.WebClientFrame{RequestId: id, AgentId: "desktop-agent", + Payload: &gatewayv2.WebClientFrame_AgentRequest{AgentRequest: &gatewayv2.GatewayEnvelope{ + Payload: &gatewayv2.GatewayEnvelope_PermissionRules{PermissionRules: &gatewayv2.PermissionRulesRequest{Action: "add", InputJson: input}}, + }}}) + } + + send("old") + refused := receiveWebFrameWithID(t, conn, "old") + if message := refused.GetLocalError().GetMessage(); !strings.Contains(message, "update LiveAgent on the desktop") { + t.Fatalf("old desktop: %v", refused) + } + select { + case outbound := <-agentSession.Outbound(): + outbound.Ack(nil) + t.Fatalf("old desktop was sent %v", outbound.GatewayEnvelope) + case <-time.After(100 * time.Millisecond): + } + + agentSession.SetCapabilities([]string{gatewayv2.ChatIngressV1Capability, gatewayv2.PermissionRulesV1Capability}) + send("ok") + forwarded := readOutboundEnvelope(t, agentSession) + if got := forwarded.GetPermissionRules(); got.GetAction() != "add" || got.GetInputJson() != input { + t.Fatalf("forwarded = %v", forwarded) + } + sm.DispatchFromAgent("desktop-agent", &gatewayv2.AgentEnvelope{RequestId: forwarded.GetRequestId(), + Payload: &gatewayv2.AgentEnvelope_PermissionRulesResp{PermissionRulesResp: &gatewayv2.PermissionRulesResponse{ResultJson: `{"id":"r_9"}`}}}) + answered := receiveWebFrameWithID(t, conn, "ok") + if got := answered.GetAgentResponse().GetPermissionRulesResp().GetResultJson(); got != `{"id":"r_9"}` { + t.Fatalf("answer = %v", answered) + } + + send("err") + forwarded = readOutboundEnvelope(t, agentSession) + sm.DispatchFromAgent("desktop-agent", &gatewayv2.AgentEnvelope{RequestId: forwarded.GetRequestId(), + Payload: &gatewayv2.AgentEnvelope_Error{Error: &gatewayv2.ErrorResponse{Code: 400, Message: `permission rule "Bash(": missing closing parenthesis (HTTP 400 Bad Request)`}}}) + failed := receiveWebFrameWithID(t, conn, "err") + if got := failed.GetAgentResponse().GetError().GetMessage(); !strings.Contains(got, "missing closing parenthesis") { + t.Fatalf("error = %v", failed) + } +} diff --git a/crates/agent-gateway/test/websocket/v2_kbrain_permission_rules_test.go b/crates/agent-gateway/test/websocket/v2_kbrain_permission_rules_test.go new file mode 100644 index 000000000..65b401e4f --- /dev/null +++ b/crates/agent-gateway/test/websocket/v2_kbrain_permission_rules_test.go @@ -0,0 +1,78 @@ +package websocket_test + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + + gatewayv2 "github.com/liveagent/agent-gateway/internal/proto/v2" + "github.com/liveagent/agent-gateway/internal/protocol/pbws" + "github.com/liveagent/agent-gateway/internal/session" +) + +// Settings → 权限 on a direct K-brain target: the WebUI's permission_rules request is +// answered by the Gateway's relay without a desktop, and a malformed one never +// reaches K-brain. +func TestV2KBrainPermissionRulesWithoutDesktop(t *testing.T) { + var ( + lock sync.Mutex + seen []string + ) + backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + if strings.HasPrefix(r.URL.Path, "/v1/permissions/") { + lock.Lock() + seen = append(seen, r.Method+" "+r.URL.RequestURI()+" "+string(body)) + lock.Unlock() + } + w.Header().Set("Content-Type", "application/json") + switch { + case r.Method == http.MethodGet && r.URL.Path == "/v1/permissions/rules": + _, _ = w.Write([]byte(`{"rules":[{"id":"r_1","rule":"Bash(ls:*)","effect":"allow","scope":"user"}]}`)) + case r.Method == http.MethodPost && r.URL.Path == "/v1/permissions/rules": + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(`{"error":"permission rule \"Bash(\": missing closing parenthesis"}`)) + default: + http.NotFound(w, r) + } + })) + defer backend.Close() + cfg := newV2TestConfig() + cfg.KBrainURL, cfg.KBrainAgentID = backend.URL, "rules-kbrain" + srv := pbws.NewServer(cfg, session.NewManager(), nil) + defer srv.Close() + mux := http.NewServeMux() + mux.Handle("/ws/v2", srv.BrowserHandler()) + browser, cleanup := dialV2Path(t, mux, "/ws/v2") + defer cleanup() + helloV2(t, browser, cfg.Token) + send := func(id, action, input string) *gatewayv2.WebServerFrame { + env := &gatewayv2.GatewayEnvelope{Payload: &gatewayv2.GatewayEnvelope_PermissionRules{ + PermissionRules: &gatewayv2.PermissionRulesRequest{Action: action, InputJson: input}, + }} + sendProtoFrame(t, browser, &gatewayv2.WebClientFrame{RequestId: id, AgentId: cfg.KBrainAgentID, + Payload: &gatewayv2.WebClientFrame_AgentRequest{AgentRequest: env}}) + return receiveWebFrameWithID(t, browser, id) + } + + listed := send("list", "list", `{}`) + if !strings.Contains(listed.GetAgentResponse().GetPermissionRulesResp().GetResultJson(), `"r_1"`) { + t.Fatalf("list: %v", listed) + } + refused := send("refused", "add", `{"rule":"Bash(","effect":"deny","scope":"user"}`) + if message := refused.GetLocalError().GetMessage(); !strings.Contains(message, "missing closing parenthesis") { + t.Fatalf("refused add: %v", refused) + } + invalid := send("invalid", "rename", `{}`) + if invalid.GetLocalError() == nil { + t.Fatalf("invalid action: %v", invalid) + } + lock.Lock() + defer lock.Unlock() + if len(seen) != 2 { + t.Fatalf("K-brain saw %d requests, want list and add only: %v", len(seen), seen) + } +} diff --git a/crates/agent-gateway/web/src/agent-ui-adapters/settingsExtension.tsx b/crates/agent-gateway/web/src/agent-ui-adapters/settingsExtension.tsx index db8fda6e1..4d315f764 100644 --- a/crates/agent-gateway/web/src/agent-ui-adapters/settingsExtension.tsx +++ b/crates/agent-gateway/web/src/agent-ui-adapters/settingsExtension.tsx @@ -8,6 +8,7 @@ import type { GatewaySettingsSyncUpdatePayload } from "@liveagent/ui/lib/setting import type { KBrainMemoryConfig, KBrainSettingsAdapter, + PermissionRule, } from "@liveagent/ui/pages/settings/kbrainSettingsAdapter"; import { getGatewayWebSocketClient } from "../lib/gatewaySocket"; import { loadToken } from "../lib/storage"; @@ -79,4 +80,46 @@ const computerSettingsAdapter: KBrainSettingsAdapter = { }, }; +type WirePermissionRule = { + id: string; + rule: string; + effect: PermissionRule["effect"]; + scope: PermissionRule["scope"]; + created_at?: string; + source_session?: string; +}; + +function permissionRuleFromWire(rule: WirePermissionRule): PermissionRule { + return { + id: rule.id, + rule: rule.rule, + effect: rule.effect, + scope: rule.scope, + ...(rule.created_at ? { createdAt: rule.created_at } : {}), + ...(rule.source_session ? { sourceSession: rule.source_session } : {}), + }; +} + +const permissionRulesClient = () => getGatewayWebSocketClient(loadToken().trim()); + +// Settings → 权限 on the WebUI: the desktop (or a direct K-brain target) answers through +// the Gateway; an old desktop is refused by the Gateway with an "update LiveAgent" message. +computerSettingsAdapter.listPermissionRules = async ({ workspace }) => { + const list = await permissionRulesClient().permissionRules<{ + project_root?: string; + rules?: WirePermissionRule[]; + }>("list", workspace ? { workspace } : {}); + return { + ...(list.project_root ? { projectRoot: list.project_root } : {}), + rules: (list.rules ?? []).map(permissionRuleFromWire), + }; +}; +computerSettingsAdapter.addPermissionRule = async (input) => + permissionRuleFromWire( + await permissionRulesClient().permissionRules("add", { ...input }), + ); +computerSettingsAdapter.deletePermissionRule = async (ref) => { + await permissionRulesClient().permissionRules("delete", { ...ref }); +}; + export const settingsHostAdapter = { isKbrain: true as const, kbrain: computerSettingsAdapter }; diff --git a/crates/agent-gateway/web/src/lib/gatewaySocketRpc.ts b/crates/agent-gateway/web/src/lib/gatewaySocketRpc.ts index d305280d5..eec1b0b95 100644 --- a/crates/agent-gateway/web/src/lib/gatewaySocketRpc.ts +++ b/crates/agent-gateway/web/src/lib/gatewaySocketRpc.ts @@ -1262,6 +1262,18 @@ export class GatewayWebSocketRpcClient extends GatewayWebSocketTransport { return this.request("proxy.test", input); } + /** 设置 → 权限:K-brain 记住的权限规则。返回 K-brain 的应答 JSON(snake_case)。 + * 只有读取会在重连后重发;添加和删除不重放。 */ + async permissionRules( + action: "list" | "add" | "delete", + input: Record, + ): Promise { + const type = `permission.rules.${action}`; + return action === "list" + ? this.requestWithRecovery(type, input) + : this.request(type, input); + } + async listFsRoots(): Promise { return this.requestWithRecovery("fs.roots", {}); } diff --git a/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts b/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts index 747ce922b..534381433 100644 --- a/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts +++ b/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts @@ -75,6 +75,7 @@ import { LoopManageRequestSchema, ManagedProcessRequestSchema, MemoryManageRequestSchema, + PermissionRulesRequestSchema, PlanningRequestSchema, ProviderCustomHeaderSchema, ProviderCustomHeadersSchema, @@ -150,6 +151,17 @@ function str(value: unknown): string { return typeof value === "string" ? value : ""; } +const PERMISSION_RULES_FIELDS = ["workspace", "rule", "effect", "scope", "id"] as const; + +function permissionRulesInput(body: Record): Record { + const out: Record = {}; + for (const key of PERMISSION_RULES_FIELDS) { + const value = trimStr(body[key]); + if (value) out[key] = value; + } + return out; +} + function trimStr(value: unknown): string { return str(value).trim(); } @@ -801,6 +813,17 @@ function agentRequestPayload(type: string, body: J): GatewayEnvelope["payload"] }), }), }; + // 设置 → 权限:只带设置页需要的字段,会话规则不经这里。 + case "permission.rules.list": + case "permission.rules.add": + case "permission.rules.delete": + return { + case: "permissionRules", + value: create(PermissionRulesRequestSchema, { + action: type.slice("permission.rules.".length), + inputJson: JSON.stringify(permissionRulesInput(body)), + }), + }; case "files.preview": return { case: "uploadedImagePreview", @@ -1601,6 +1624,8 @@ function decodeAgentResponse(envelope: AgentEnvelope, options: { agentOnline: bo return unmarshalJsonPayload(payload.value.resultJson); case "proxyDetectResp": return unmarshalJsonPayload(payload.value.resultJson); + case "permissionRulesResp": + return unmarshalJsonPayload(payload.value.resultJson); case "sftpResponse": return sftpResponsePayload(payload.value); case "chatQueueResp": diff --git a/crates/agent-gateway/web/src/lib/proto/gen/proto/v2/gateway_pb.ts b/crates/agent-gateway/web/src/lib/proto/gen/proto/v2/gateway_pb.ts index 0c579abd9..9b00149a9 100644 --- a/crates/agent-gateway/web/src/lib/proto/gen/proto/v2/gateway_pb.ts +++ b/crates/agent-gateway/web/src/lib/proto/gen/proto/v2/gateway_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file proto/v2/gateway.proto. */ export const file_proto_v2_gateway: GenFile = /*@__PURE__*/ - fileDesc("ChZwcm90by92Mi9nYXRld2F5LnByb3RvEhRsaXZlYWdlbnQuZ2F0ZXdheS52MiKoJAoPR2F0ZXdheUVudmVsb3BlEhIKCnJlcXVlc3RfaWQYASABKAkSEQoJdGltZXN0YW1wGAIgASgDEkAKDGNoYXRfY29tbWFuZBgKIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRDb21tYW5kUmVxdWVzdEgAEj4KC2Nyb25fbWFuYWdlGBQgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuQ3Jvbk1hbmFnZVJlcXVlc3RIABJACgxoaXN0b3J5X2xpc3QYHiABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5TGlzdFJlcXVlc3RIABI+CgtoaXN0b3J5X2dldBgfIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlHZXRSZXF1ZXN0SAASRAoOaGlzdG9yeV9yZW5hbWUYICABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UmVuYW1lUmVxdWVzdEgAEkQKDmhpc3RvcnlfZGVsZXRlGCEgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeURlbGV0ZVJlcXVlc3RIABJECg5oaXN0b3J5X3ByZWZpeBgiIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlQcmVmaXhSZXF1ZXN0SAASPgoLaGlzdG9yeV9waW4YIyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UGluUmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfZ2V0GCQgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlR2V0UmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfc2V0GCUgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlU2V0UmVxdWVzdEgAElEKFWhpc3Rvcnlfc2hhcmVfcmVzb2x2ZRgmIAEoCzIwLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVJlc29sdmVSZXF1ZXN0SAASSAoQaGlzdG9yeV93b3JrZGlycxgnIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlXb3JrZGlyc1JlcXVlc3RIABJCCg1wcm92aWRlcl9saXN0GCggASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJMaXN0UmVxdWVzdEgAEkAKDHNldHRpbmdzX2dldBgpIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzR2V0UmVxdWVzdEgAEkYKD3NldHRpbmdzX3VwZGF0ZRgqIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzVXBkYXRlUmVxdWVzdEgAEkcKEHNraWxsX2ZpbGVzX2xpc3QYKyABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbEZpbGVzTGlzdFJlcXVlc3RIABJNChNza2lsbF9tZXRhZGF0YV9yZWFkGCwgASgLMi4ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxNZXRhZGF0YVJlYWRSZXF1ZXN0SAASRQoPc2tpbGxfdGV4dF9yZWFkGC0gASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxUZXh0UmVhZFJlcXVlc3RIABJJChFmaWxlX21lbnRpb25fbGlzdBguIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZpbGVNZW50aW9uTGlzdFJlcXVlc3RIABJRChV1cGxvYWRfcmVhZGFibGVfZmlsZXMYLyABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRSZWFkYWJsZUZpbGVzUmVxdWVzdEgAEjgKCGZzX3Jvb3RzGDAgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSb290c1JlcXVlc3RIABI/Cgxmc19saXN0X2RpcnMYMSABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3REaXJzUmVxdWVzdEgAEjEKBHBpbmcYMiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5QaW5nUmVxdWVzdEgAElMKFnVwbG9hZGVkX2ltYWdlX3ByZXZpZXcYMyABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRlZEltYWdlUHJldmlld1JlcXVlc3RIABJCCg1tZW1vcnlfbWFuYWdlGDQgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuTWVtb3J5TWFuYWdlUmVxdWVzdEgAEkAKDHNraWxsX21hbmFnZRg1IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsTWFuYWdlUmVxdWVzdEgAElYKGGZzX2NyZWF0ZV9wcm9qZWN0X2ZvbGRlchg2IAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlUHJvamVjdEZvbGRlclJlcXVlc3RIABJBChB0ZXJtaW5hbF9yZXF1ZXN0GDcgASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxSZXF1ZXN0SAASNgoHZnNfbGlzdBg4IAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzTGlzdFJlcXVlc3RIABJBCg1mc193cml0ZV90ZXh0GDkgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNXcml0ZVRleHRSZXF1ZXN0SAASQQoNZnNfY3JlYXRlX2Rpchg6IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlRGlyUmVxdWVzdEgAEjoKCWZzX3JlbmFtZRg7IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVuYW1lUmVxdWVzdEgAEjoKCWZzX2RlbGV0ZRg8IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzRGVsZXRlUmVxdWVzdEgAEjcKC2dpdF9yZXF1ZXN0GD0gASgLMiAubGl2ZWFnZW50LmdhdGV3YXkudjIuR2l0UmVxdWVzdEgAElAKFWZzX3JlYWRfZWRpdGFibGVfdGV4dBg+IAEoCzIvLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZEVkaXRhYmxlVGV4dFJlcXVlc3RIABJUChdmc19yZWFkX3dvcmtzcGFjZV9pbWFnZRg/IAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZFdvcmtzcGFjZUltYWdlUmVxdWVzdEgAEjkKDHNmdHBfcmVxdWVzdBhAIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBSZXF1ZXN0SAASRgoPcHJvdmlkZXJfbW9kZWxzGEEgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJNb2RlbHNSZXF1ZXN0SAASXwodc2V0dGluZ3NfcmVzZXRfc3NoX2tub3duX2hvc3QYSCABKAsyNi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVxdWVzdEgAEjwKCmNoYXRfcXVldWUYSSABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVSZXF1ZXN0SAASQAoQY2hhdF9pbmdyZXNzX2FjaxhLIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzQWNrSAASQQoMdHVubmVsX3N0YXRlGFAgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsU3RhdGVTbmFwc2hvdEgAEj8KD3R1bm5lbF9tdXRhdGlvbhhRIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbE11dGF0aW9uSAASOQoMdHVubmVsX2ZyYW1lGFIgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsRnJhbWVIABJGCg93b3Jrc3BhY2Vfd2F0Y2gYWiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Xb3Jrc3BhY2VXYXRjaFJlcXVlc3RIABJOChdtYW5hZ2VkX3Byb2Nlc3NfcmVxdWVzdBhbIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLk1hbmFnZWRQcm9jZXNzUmVxdWVzdEgAEkQKDmhpc3RvcnlfYnJhbmNoGFwgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeUJyYW5jaFJlcXVlc3RIABJECg5wcm92aWRlcl91c2FnZRhdIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLlByb3ZpZGVyVXNhZ2VSZXF1ZXN0SAASQwoOY2hhdF9maWxlX29wZW4YXiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0RmlsZU9wZW5SZXF1ZXN0SAASRQoPaGlzdG9yeV9zZXRfY3dkGF8gASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNldEN3ZFJlcXVlc3RIABJRChV3b3Jrc3BhY2Vfcm9vdF9ncmFudHMYYCABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Xb3Jrc3BhY2VSb290R3JhbnRzUmVxdWVzdEgAEkgKEGltcG9ydF9kaXJlY3RvcnkYYSABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5JbXBvcnREaXJlY3RvcnlSZXF1ZXN0SAASPQoKY2hlY2twb2ludBhiIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoZWNrcG9pbnRSZXF1ZXN0SAASSAoQdHJhamVjdG9yeV9mZXRjaBhjIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRyYWplY3RvcnlGZXRjaFJlcXVlc3RIABJNChNpbnN0YWxsZWRfYXBwc19saXN0GGQgASgLMi4ubGl2ZWFnZW50LmdhdGV3YXkudjIuSW5zdGFsbGVkQXBwc0xpc3RSZXF1ZXN0SAASQAoMY2xhcmlmeV90dXJuGGUgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2xhcmlmeVR1cm5SZXF1ZXN0SAASPAoKY3VhX2RyaXZlchhmIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkN1YURyaXZlclJlcXVlc3RIABI5CghwbGFubmluZxhnIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlBsYW5uaW5nUmVxdWVzdEgAEk8KFHByb21wdF90ZW1wbGF0ZV9saXN0GGggASgLMi8ubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvbXB0VGVtcGxhdGVMaXN0UmVxdWVzdEgAElMKFnByb21wdF90ZW1wbGF0ZV9leHBhbmQYaSABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Qcm9tcHRUZW1wbGF0ZUV4cGFuZFJlcXVlc3RIABJLChJzbGFzaF9jb21tYW5kX2xpc3QYaiABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TbGFzaENvbW1hbmRMaXN0UmVxdWVzdEgAEj4KC2dvYWxfbWFuYWdlGGsgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuR29hbE1hbmFnZVJlcXVlc3RIABI+Cgtsb29wX21hbmFnZRhsIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkxvb3BNYW5hZ2VSZXF1ZXN0SAASQAoMcHJveHlfZGV0ZWN0GG0gASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJveHlEZXRlY3RSZXF1ZXN0SAASRAoOaGlzdG9yeV9yZXN5bmMYbiABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UmVzeW5jUmVxdWVzdEgAQgkKB3BheWxvYWRKBAhDEERKBAhEEEVKBAhFEEZKBAhKEEsivS8KDUFnZW50RW52ZWxvcGUSEgoKcmVxdWVzdF9pZBgBIAEoCRIRCgl0aW1lc3RhbXAYAiABKAMSNQoKY2hhdF9ldmVudBgKIAEoCzIfLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRFdmVudEgAEkQKEGNyb25fbWFuYWdlX3Jlc3AYFCABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Dcm9uTWFuYWdlUmVzcG9uc2VIABJGChFoaXN0b3J5X2xpc3RfcmVzcBgeIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlMaXN0UmVzcG9uc2VIABJEChBoaXN0b3J5X2dldF9yZXNwGB8gASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeUdldFJlc3BvbnNlSAASSgoTaGlzdG9yeV9yZW5hbWVfcmVzcBggIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlSZW5hbWVSZXNwb25zZUgAEkoKE2hpc3RvcnlfZGVsZXRlX3Jlc3AYISABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5RGVsZXRlUmVzcG9uc2VIABI+CgxoaXN0b3J5X3N5bmMYIiABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U3luY0V2ZW50SAASSgoTaGlzdG9yeV9wcmVmaXhfcmVzcBgjIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlQcmVmaXhSZXNwb25zZUgAEkQKEGhpc3RvcnlfcGluX3Jlc3AYJCABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UGluUmVzcG9uc2VIABJPChZoaXN0b3J5X3NoYXJlX2dldF9yZXNwGCUgASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlR2V0UmVzcG9uc2VIABJPChZoaXN0b3J5X3NoYXJlX3NldF9yZXNwGCYgASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlU2V0UmVzcG9uc2VIABJXChpoaXN0b3J5X3NoYXJlX3Jlc29sdmVfcmVzcBgnIAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVJlc29sdmVSZXNwb25zZUgAEk4KFWhpc3Rvcnlfd29ya2RpcnNfcmVzcBg4IAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlXb3JrZGlyc1Jlc3BvbnNlSAASSAoScHJvdmlkZXJfbGlzdF9yZXNwGCggASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJMaXN0UmVzcG9uc2VIABJGChFzZXR0aW5nc19nZXRfcmVzcBgpIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzR2V0UmVzcG9uc2VIABJMChRzZXR0aW5nc191cGRhdGVfcmVzcBgqIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzVXBkYXRlUmVzcG9uc2VIABJACg1zZXR0aW5nc19zeW5jGCsgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuU2V0dGluZ3NTeW5jRXZlbnRIABJNChVza2lsbF9maWxlc19saXN0X3Jlc3AYLCABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbEZpbGVzTGlzdFJlc3BvbnNlSAASUwoYc2tpbGxfbWV0YWRhdGFfcmVhZF9yZXNwGC0gASgLMi8ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxNZXRhZGF0YVJlYWRSZXNwb25zZUgAEksKFHNraWxsX3RleHRfcmVhZF9yZXNwGC4gASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxUZXh0UmVhZFJlc3BvbnNlSAASTwoWZmlsZV9tZW50aW9uX2xpc3RfcmVzcBgvIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZpbGVNZW50aW9uTGlzdFJlc3BvbnNlSAASVwoadXBsb2FkX3JlYWRhYmxlX2ZpbGVzX3Jlc3AYMCABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRSZWFkYWJsZUZpbGVzUmVzcG9uc2VIABI+Cg1mc19yb290c19yZXNwGDEgASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSb290c1Jlc3BvbnNlSAASMgoEcG9uZxgyIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYyLlBvbmdSZXNwb25zZUgAEkUKEWZzX2xpc3RfZGlyc19yZXNwGDMgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNMaXN0RGlyc1Jlc3BvbnNlSAASWQobdXBsb2FkZWRfaW1hZ2VfcHJldmlld19yZXNwGDQgASgLMjIubGl2ZWFnZW50LmdhdGV3YXkudjIuVXBsb2FkZWRJbWFnZVByZXZpZXdSZXNwb25zZUgAEkgKEm1lbW9yeV9tYW5hZ2VfcmVzcBg1IAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLk1lbW9yeU1hbmFnZVJlc3BvbnNlSAASRgoRc2tpbGxfbWFuYWdlX3Jlc3AYNiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbE1hbmFnZVJlc3BvbnNlSAASXAodZnNfY3JlYXRlX3Byb2plY3RfZm9sZGVyX3Jlc3AYNyABKAsyMy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0NyZWF0ZVByb2plY3RGb2xkZXJSZXNwb25zZUgAEkMKEXRlcm1pbmFsX3Jlc3BvbnNlGDkgASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxSZXNwb25zZUgAEj0KDnRlcm1pbmFsX2V2ZW50GDogASgLMiMubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxFdmVudEgAEjwKDGZzX2xpc3RfcmVzcBg7IAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzTGlzdFJlc3BvbnNlSAASRwoSZnNfd3JpdGVfdGV4dF9yZXNwGDwgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNXcml0ZVRleHRSZXNwb25zZUgAEkcKEmZzX2NyZWF0ZV9kaXJfcmVzcBg9IAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlRGlyUmVzcG9uc2VIABJACg5mc19yZW5hbWVfcmVzcBg+IAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVuYW1lUmVzcG9uc2VIABJACg5mc19kZWxldGVfcmVzcBg/IAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzRGVsZXRlUmVzcG9uc2VIABI5CgxnaXRfcmVzcG9uc2UYQCABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5HaXRSZXNwb25zZUgAElYKGmZzX3JlYWRfZWRpdGFibGVfdGV4dF9yZXNwGEEgASgLMjAubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSZWFkRWRpdGFibGVUZXh0UmVzcG9uc2VIABJaChxmc19yZWFkX3dvcmtzcGFjZV9pbWFnZV9yZXNwGEIgASgLMjIubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSZWFkV29ya3NwYWNlSW1hZ2VSZXNwb25zZUgAEjsKDXNmdHBfcmVzcG9uc2UYSSABKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZnRwUmVzcG9uc2VIABI1CgpzZnRwX2V2ZW50GEogASgLMh8ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cEV2ZW50SAASQgoPY2hhdF9xdWV1ZV9yZXNwGEsgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFF1ZXVlUmVzcG9uc2VIABJAChBjaGF0X3F1ZXVlX2V2ZW50GEwgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFF1ZXVlRXZlbnRIABI+CgxjaGF0X2NvbnRyb2wYRiABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0Q29udHJvbEV2ZW50SAASQgoOcnVudGltZV9zdGF0dXMYRyABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5SdW50aW1lU3RhdHVzRXZlbnRIABJlCiJzZXR0aW5nc19yZXNldF9zc2hfa25vd25faG9zdF9yZXNwGEggASgLMjcubGl2ZWFnZW50LmdhdGV3YXkudjIuU2V0dGluZ3NSZXNldFNzaEtub3duSG9zdFJlc3BvbnNlSAASSgoVY2hhdF9ydW50aW1lX3NuYXBzaG90GE0gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFJ1bnRpbWVTbmFwc2hvdEgAEkwKFHByb3ZpZGVyX21vZGVsc19yZXNwGE8gASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJNb2RlbHNSZXNwb25zZUgAEkIKDnR1bm5lbF9kZXNpcmVkGFAgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsRGVzaXJlZFN0YXRlSAASTAoWdHVubmVsX211dGF0aW9uX3Jlc3VsdBhRIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbE11dGF0aW9uUmVzdWx0SAASOQoMdHVubmVsX2ZyYW1lGFIgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsRnJhbWVIABJGChN0dW5uZWxfcHJvYmVfcmVwb3J0GFMgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsUHJvYmVSZXBvcnRIABJKChJ3b3Jrc3BhY2VfYWN0aXZpdHkYWiABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Xb3Jrc3BhY2VBY3Rpdml0eUV2ZW50SAASUAoYbWFuYWdlZF9wcm9jZXNzX3Jlc3BvbnNlGFsgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuTWFuYWdlZFByb2Nlc3NSZXNwb25zZUgAElAKGG1hbmFnZWRfcHJvY2Vzc19zbmFwc2hvdBhcIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLk1hbmFnZWRQcm9jZXNzU25hcHNob3RIABJKChNoaXN0b3J5X2JyYW5jaF9yZXNwGF0gASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeUJyYW5jaFJlc3BvbnNlSAASSgoTcHJvdmlkZXJfdXNhZ2VfcmVzcBheIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLlByb3ZpZGVyVXNhZ2VSZXNwb25zZUgAEkQKEmNoYXRfaW5ncmVzc19iYXRjaBhfIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzQmF0Y2hIABJGChNjaGF0X2luZ3Jlc3NfcmVzdW1lGGAgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NSZXN1bWVIABJKChVjaGF0X2luZ3Jlc3NfZnJhZ21lbnQYYSABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc0ZyYWdtZW50SAASSQoTY2hhdF9maWxlX29wZW5fcmVzcBhiIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRGaWxlT3BlblJlc3BvbnNlSAASNAoFZXJyb3IYYyABKAsyIy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5FcnJvclJlc3BvbnNlSAASSwoUaGlzdG9yeV9zZXRfY3dkX3Jlc3AYZCABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2V0Q3dkUmVzcG9uc2VIABJXChp3b3Jrc3BhY2Vfcm9vdF9ncmFudHNfcmVzcBhlIAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYyLldvcmtzcGFjZVJvb3RHcmFudHNSZXNwb25zZUgAEk4KFWltcG9ydF9kaXJlY3RvcnlfcmVzcBhmIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLkltcG9ydERpcmVjdG9yeVJlc3BvbnNlSAASQwoPY2hlY2twb2ludF9yZXNwGGcgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hlY2twb2ludFJlc3BvbnNlSAASTgoVdHJhamVjdG9yeV9mZXRjaF9yZXNwGGggASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuVHJhamVjdG9yeUZldGNoUmVzcG9uc2VIABJTChhpbnN0YWxsZWRfYXBwc19saXN0X3Jlc3AYaSABKAsyLy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5JbnN0YWxsZWRBcHBzTGlzdFJlc3BvbnNlSAASRgoRY2xhcmlmeV90dXJuX3Jlc3AYaiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DbGFyaWZ5VHVyblJlc3BvbnNlSAASQgoPY3VhX2RyaXZlcl9yZXNwGGsgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuQ3VhRHJpdmVyUmVzcG9uc2VIABJEChJjbGFyaWZ5X3R1cm5fZGVsdGEYbCABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DbGFyaWZ5VHVybkRlbHRhSAASPwoNcGxhbm5pbmdfcmVzcBhtIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLlBsYW5uaW5nUmVzcG9uc2VIABJBChBwbGFubmluZ19jaGFuZ2VkGG4gASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjIuUGxhbm5pbmdDaGFuZ2VkSAASVQoZcHJvbXB0X3RlbXBsYXRlX2xpc3RfcmVzcBhvIAEoCzIwLmxpdmVhZ2VudC5nYXRld2F5LnYyLlByb21wdFRlbXBsYXRlTGlzdFJlc3BvbnNlSAASWQobcHJvbXB0X3RlbXBsYXRlX2V4cGFuZF9yZXNwGHAgASgLMjIubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvbXB0VGVtcGxhdGVFeHBhbmRSZXNwb25zZUgAElEKF3NsYXNoX2NvbW1hbmRfbGlzdF9yZXNwGHEgASgLMi4ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2xhc2hDb21tYW5kTGlzdFJlc3BvbnNlSAASRAoQZ29hbF9tYW5hZ2VfcmVzcBhyIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkdvYWxNYW5hZ2VSZXNwb25zZUgAEkQKEGxvb3BfbWFuYWdlX3Jlc3AYcyABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Mb29wTWFuYWdlUmVzcG9uc2VIABJGChFwcm94eV9kZXRlY3RfcmVzcBh0IAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLlByb3h5RGV0ZWN0UmVzcG9uc2VIABJGChNoaXN0b3J5X3N5bmNfc3RhdHVzGHUgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVN5bmNTdGF0dXNIABJKChVoaXN0b3J5X3N5bmNfc25hcHNob3QYdiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U3luY1NuYXBzaG90SABCCQoHcGF5bG9hZEoECEMQREoECEQQRUoECEUQRkoECE4QTyJVChFDaGF0U2VsZWN0ZWRNb2RlbBIaChJjdXN0b21fcHJvdmlkZXJfaWQYASABKAkSDQoFbW9kZWwYAiABKAkSFQoNcHJvdmlkZXJfdHlwZRgDIAEoCSKAAQoTQ2hhdFJ1bnRpbWVDb250cm9scxIYChB0aGlua2luZ19lbmFibGVkGAEgASgIEiEKGW5hdGl2ZV93ZWJfc2VhcmNoX2VuYWJsZWQYAiABKAgSEQoJcmVhc29uaW5nGAMgASgJEhkKEXBsYW5fbW9kZV9lbmFibGVkGAQgASgIIpQBChJDbGFyaWZ5VHVyblJlcXVlc3QSFQoNbWVzc2FnZXNfanNvbhgBIAEoCRITCgtwcm92aWRlcl9pZBgCIAEoCRINCgVtb2RlbBgDIAEoCRJDChBydW50aW1lX2NvbnRyb2xzGAQgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFJ1bnRpbWVDb250cm9scyJUChNDbGFyaWZ5VHVyblJlc3BvbnNlEhIKCmZpbmFsX3RleHQYASABKAkSEgoKZXJyb3JfY29kZRgCIAEoCRIVCg1lcnJvcl9tZXNzYWdlGAMgASgJIiAKEENsYXJpZnlUdXJuRGVsdGESDAoEdGV4dBgBIAEoCSKJAQoQQ2hhdFVwbG9hZGVkRmlsZRIVCg1yZWxhdGl2ZV9wYXRoGAEgASgJEhUKDWFic29sdXRlX3BhdGgYAiABKAkSEQoJZmlsZV9uYW1lGAMgASgJEgwKBGtpbmQYBCABKAkSEgoKc2l6ZV9ieXRlcxgFIAEoAxISCgpkZWR1cGVfa2V5GAYgASgJIksKElVwbG9hZFJlYWRhYmxlRmlsZRIRCglmaWxlX25hbWUYASABKAkSEQoJbWltZV90eXBlGAIgASgJEg8KB2NvbnRlbnQYAyABKAwiZgoaVXBsb2FkUmVhZGFibGVGaWxlc1JlcXVlc3QSDwoHd29ya2RpchgBIAEoCRI3CgVmaWxlcxgCIAMoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlVwbG9hZFJlYWRhYmxlRmlsZSJlChtVcGxvYWRSZWFkYWJsZUZpbGVzUmVzcG9uc2USNQoFZmlsZXMYASADKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0VXBsb2FkZWRGaWxlEg8KB3NraXBwZWQYAiADKAkiPQoTSW1wb3J0RGlyZWN0b3J5RmlsZRIVCg1yZWxhdGl2ZV9wYXRoGAEgASgJEg8KB2NvbnRlbnQYAiABKAwiwwIKFkltcG9ydERpcmVjdG9yeVJlcXVlc3QSDAoEbmFtZRgBIAEoCRIOCgZ0YXJnZXQYAiABKAkSPAoFZmlsZXMYAyADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5JbXBvcnREaXJlY3RvcnlGaWxlQgIYARITCgt0cmFuc2Zlcl9pZBgEIAEoCRJBCglvcGVyYXRpb24YBSABKA4yLi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5JbXBvcnREaXJlY3RvcnlPcGVyYXRpb24SEwoLdG90YWxfZmlsZXMYBiABKA0SEwoLdG90YWxfYnl0ZXMYByABKAQSFQoNcmVsYXRpdmVfcGF0aBgIIAEoCRIOCgZvZmZzZXQYCSABKAQSDQoFY2h1bmsYCiABKAwSFQoNZmlsZV9jb21wbGV0ZRgLIAEoCCJ+ChdJbXBvcnREaXJlY3RvcnlSZXNwb25zZRIRCglyb290X3BhdGgYASABKAkSEgoKZmlsZV9jb3VudBgCIAEoBRIPCgdza2lwcGVkGAMgAygJEhMKC3RyYW5zZmVyX2lkGAQgASgJEhYKDnJlY2VpdmVkX2J5dGVzGAUgASgEIkUKG1VwbG9hZGVkSW1hZ2VQcmV2aWV3UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEhUKDWFic29sdXRlX3BhdGgYAiABKAkiPwocVXBsb2FkZWRJbWFnZVByZXZpZXdSZXNwb25zZRIRCgltaW1lX3R5cGUYASABKAkSDAoEZGF0YRgCIAEoCSJ7CgpUdW5uZWxTcGVjEgoKAmlkGAEgASgJEhEKCXNsdWdfaGludBgCIAEoCRIMCgRuYW1lGAMgASgJEhIKCnRhcmdldF91cmwYBCABKAkSEgoKZXhwaXJlc19hdBgFIAEoAxIYChBwcm9qZWN0X3BhdGhfa2V5GAYgASgJIlkKElR1bm5lbERlc2lyZWRTdGF0ZRIxCgd0dW5uZWxzGAEgAygLMiAubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsU3BlYxIQCghyZXZpc2lvbhgCIAEoBCJmCgxUdW5uZWxIZWFsdGgSDgoGc3RhdHVzGAEgASgJEhMKC2h0dHBfc3RhdHVzGAIgASgNEg0KBWVycm9yGAMgASgJEhIKCmNoZWNrZWRfYXQYBCABKAMSDgoGcnR0X21zGAUgASgNIvABCgxUdW5uZWxTdGF0dXMSCgoCaWQYASABKAkSDAoEc2x1ZxgCIAEoCRIMCgRuYW1lGAMgASgJEhIKCnRhcmdldF91cmwYBCABKAkSEwoLcHVibGljX3BhdGgYBSABKAkSEgoKY3JlYXRlZF9hdBgGIAEoAxISCgpleHBpcmVzX2F0GAcgASgDEhoKEmFjdGl2ZV9jb25uZWN0aW9ucxgIIAEoDRIYChBwcm9qZWN0X3BhdGhfa2V5GAkgASgJEjEKBWxvY2FsGAogASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhbHRoIqUBChNUdW5uZWxTdGF0ZVNuYXBzaG90EjMKB3R1bm5lbHMYASADKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxTdGF0dXMSEAoIcmV2aXNpb24YAiABKAQSFAoMYWdlbnRfb25saW5lGAMgASgIEjEKBXJlbGF5GAQgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhbHRoIpkBCg5UdW5uZWxNdXRhdGlvbhIOCgZhY3Rpb24YASABKAkSEQoJdHVubmVsX2lkGAIgASgJEhIKCnRhcmdldF91cmwYAyABKAkSDAoEbmFtZRgEIAEoCRIYCgt0dGxfc2Vjb25kcxgFIAEoDUgAiAEBEhgKEHByb2plY3RfcGF0aF9rZXkYBiABKAlCDgoMX3R0bF9zZWNvbmRzIlQKFFR1bm5lbE11dGF0aW9uUmVzdWx0EhEKCXR1bm5lbF9pZBgBIAEoCRISCgplcnJvcl9jb2RlGAIgASgJEhUKDWVycm9yX21lc3NhZ2UYAyABKAkiWQoRVHVubmVsUHJvYmVSZXN1bHQSEQoJdHVubmVsX2lkGAEgASgJEjEKBWxvY2FsGAIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhbHRoIk0KEVR1bm5lbFByb2JlUmVwb3J0EjgKB3Jlc3VsdHMYASADKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxQcm9iZVJlc3VsdCIrCgxUdW5uZWxIZWFkZXISDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSL1AgoLVHVubmVsRnJhbWUSEQoJc3RyZWFtX2lkGAEgASgJEjMKBGtpbmQYAiABKA4yJS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxGcmFtZUtpbmQSEgoKdGFyZ2V0X3VybBgDIAEoCRIOCgZtZXRob2QYBCABKAkSDAoEcGF0aBgFIAEoCRIzCgdoZWFkZXJzGAYgAygLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhZGVyEg4KBnN0YXR1cxgHIAEoDRIMCgRib2R5GAggASgMEg0KBWVycm9yGAkgASgJEkIKD3dzX21lc3NhZ2VfdHlwZRgKIAEoDjIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbFdzTWVzc2FnZVR5cGUSFgoOd3Nfc3VicHJvdG9jb2wYCyABKAkSFQoNd3NfY2xvc2VfY29kZRgMIAEoDRIXCg93c19jbG9zZV9yZWFzb24YDSABKAkiKQoVV29ya3NwYWNlV2F0Y2hSZXF1ZXN0EhAKCHdvcmtkaXJzGAEgAygJIn4KFldvcmtzcGFjZUFjdGl2aXR5RXZlbnQSDwoHd29ya2RpchgBIAEoCRIQCghyZXZpc2lvbhgCIAEoBBIKCgJmcxgDIAEoCBILCgNnaXQYBCABKAgSFQoNY2hhbmdlZF9wYXRocxgFIAMoCRIRCgl0cnVuY2F0ZWQYBiABKAgilgIKFE1hbmFnZWRQcm9jZXNzUmVjb3JkEgoKAmlkGAEgASgJEg0KBWxhYmVsGAIgASgJEg8KB2NvbW1hbmQYAyABKAkSCwoDY3dkGAQgASgJEg0KBXNoZWxsGAUgASgJEgsKA3BpZBgGIAEoDRIQCghsb2dfcGF0aBgHIAEoCRISCgpzdGFydGVkX2F0GAggASgDEhgKC2ZpbmlzaGVkX2F0GAkgASgDSACIAQESFgoJZXhpdF9jb2RlGAogASgFSAGIAQESDwoHcnVubmluZxgLIAEoCBIQCghpc29sYXRlZBgMIAEoCBIQCghyZXN0b3JlZBgNIAEoCEIOCgxfZmluaXNoZWRfYXRCDAoKX2V4aXRfY29kZSJpChZNYW5hZ2VkUHJvY2Vzc1NuYXBzaG90Ej0KCXByb2Nlc3NlcxgBIAMoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLk1hbmFnZWRQcm9jZXNzUmVjb3JkEhAKCHJldmlzaW9uGAIgASgEIk4KFU1hbmFnZWRQcm9jZXNzUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSEgoKcHJvY2Vzc19pZBgCIAEoCRIRCgltYXhfYnl0ZXMYAyABKA0itwEKFk1hbmFnZWRQcm9jZXNzUmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEj4KCHNuYXBzaG90GAIgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuTWFuYWdlZFByb2Nlc3NTbmFwc2hvdBITCgtsb2dfY29udGVudBgDIAEoCRIQCghsb2dfcGF0aBgEIAEoCRIVCg1sb2dfdHJ1bmNhdGVkGAUgASgIEg8KB3N0b3BwZWQYBiABKAgiOQoTTWVtb3J5TWFuYWdlUmVxdWVzdBIPCgdjb21tYW5kGAEgASgJEhEKCWFyZ3NfanNvbhgCIAEoCSIrChRNZW1vcnlNYW5hZ2VSZXNwb25zZRITCgtyZXN1bHRfanNvbhgBIAEoCSLBAwoPVGVybWluYWxSZXF1ZXN0Eg4KBmFjdGlvbhgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYFiABKAkSDgoGcnVuX2lkGBcgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRILCgNjd2QYBCABKAkSDQoFc2hlbGwYBSABKAkSDQoFdGl0bGUYBiABKAkSDAoEZGF0YRgHIAEoCRIMCgRjb2xzGAggASgNEgwKBHJvd3MYCSABKA0SEQoJbWF4X2J5dGVzGAogASgNEhMKC3NzaF9ob3N0X2lkGAsgASgJEhEKCXByb21wdF9pZBgMIAEoCRIVCg1wcm9tcHRfYW5zd2VyGA0gASgJEhYKDnRydXN0X2hvc3Rfa2V5GA4gASgIEhQKDHNmdHBfZW5hYmxlZBgPIAEoCBIOCgZ0YWJfaWQYECABKAkSEAoIdGFiX2tpbmQYESABKAkSEwoLcmVtb3RlX2hvc3QYEiABKAkSEwoLcmVtb3RlX3BvcnQYEyABKA0SEgoKbG9jYWxfcG9ydBgUIAEoDRISCgpmb3J3YXJkX2lkGBUgASgJIrICCg9UZXJtaW5hbFNlc3Npb24SCgoCaWQYASABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgCIAEoCRILCgNjd2QYAyABKAkSDQoFc2hlbGwYBCABKAkSDQoFdGl0bGUYBSABKAkSCwoDcGlkGAYgASgNEgwKBGNvbHMYByABKA0SDAoEcm93cxgIIAEoDRISCgpjcmVhdGVkX2F0GAkgASgEEhIKCnVwZGF0ZWRfYXQYCiABKAQSEwoLZmluaXNoZWRfYXQYCyABKAQSEQoJZXhpdF9jb2RlGAwgASgFEg8KB3J1bm5pbmcYDSABKAgSDAoEa2luZBgOIAEoCRI2CgNzc2gYDyABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaE1ldGFkYXRhItsBChNUZXJtaW5hbFNzaE1ldGFkYXRhEg8KB2hvc3RfaWQYASABKAkSEQoJaG9zdF9uYW1lGAIgASgJEhAKCHVzZXJuYW1lGAMgASgJEgwKBGhvc3QYBCABKAkSDAoEcG9ydBgFIAEoDRIRCglhdXRoX3R5cGUYBiABKAkSDgoGc3RhdHVzGAcgASgJEhkKEXJlY29ubmVjdF9hdHRlbXB0GAggASgNEh4KFnJlY29ubmVjdF9tYXhfYXR0ZW1wdHMYCSABKA0SFAoMc2Z0cF9lbmFibGVkGAogASgIIpEDCgtTZnRwUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEg8KB3dvcmtkaXIYBCABKAkSEgoKbG9jYWxfcGF0aBgFIAEoCRITCgtyZW1vdGVfcGF0aBgGIAEoCRIRCglmcm9tX3BhdGgYByABKAkSDwoHdG9fcGF0aBgIIAEoCRIRCglkaXJlY3Rpb24YCSABKAkSEwoLdGFyZ2V0X3BhdGgYCiABKAkSEQoJcmVjdXJzaXZlGAsgASgIEhEKCW92ZXJ3cml0ZRgMIAEoCBIPCgdjb250ZW50GA0gASgJEg4KBm9mZnNldBgOIAEoBBIRCgltYXhfYnl0ZXMYDyABKAQSEwoLc3RyaWN0X3V0ZjgYECABKAgSFgoOZXhwZWN0ZWRfbXRpbWUYESABKAQSGwoTZXhwZWN0ZWRfc2l6ZV9ieXRlcxgSIAEoBBIaChJjcmVhdGVfcGFyZW50X2RpcnMYEyABKAgiWAoJU2Z0cEVudHJ5EgwKBHBhdGgYASABKAkSDAoEbmFtZRgCIAEoCRIMCgRraW5kGAMgASgJEhIKCnNpemVfYnl0ZXMYBCABKAQSDQoFbXRpbWUYBSABKAQi8gEKDFNmdHBUcmFuc2ZlchIKCgJpZBgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhEKCWRpcmVjdGlvbhgDIAEoCRIOCgZzdGF0dXMYBCABKAkSEwoLc291cmNlX3BhdGgYBSABKAkSEwoLdGFyZ2V0X3BhdGgYBiABKAkSFAoMY3VycmVudF9wYXRoGAcgASgJEhIKCmJ5dGVzX2RvbmUYCCABKAQSEwoLYnl0ZXNfdG90YWwYCSABKAQSEgoKZmlsZXNfZG9uZRgKIAEoDRITCgtmaWxlc190b3RhbBgLIAEoDRINCgVlcnJvchgMIAEoCSKwAgoMU2Z0cFJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRIMCgRwYXRoGAIgASgJEjAKB2VudHJpZXMYAyADKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZnRwRW50cnkSLgoFZW50cnkYBCABKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZnRwRW50cnkSDgoGZXhpc3RzGAUgASgIEjQKCHRyYW5zZmVyGAYgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cFRyYW5zZmVyEg8KB2NvbnRlbnQYByABKAkSDgoGb2Zmc2V0GAggASgEEhIKCmJ5dGVzX3JlYWQYCSABKAQSEgoKc2l6ZV9ieXRlcxgKIAEoBBIRCgl0cnVuY2F0ZWQYCyABKAgiTwoJU2Z0cEV2ZW50EgwKBGtpbmQYASABKAkSNAoIdHJhbnNmZXIYAiABKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZnRwVHJhbnNmZXIiwQEKEVRlcm1pbmFsU3NoUHJvbXB0EgoKAmlkGAEgASgJEgwKBGtpbmQYAiABKAkSDwoHaG9zdF9pZBgDIAEoCRIRCglob3N0X25hbWUYBCABKAkSDAoEaG9zdBgFIAEoCRIMCgRwb3J0GAYgASgNEg8KB21lc3NhZ2UYByABKAkSGgoSZmluZ2VycHJpbnRfc2hhMjU2GAggASgJEhAKCGtleV90eXBlGAkgASgJEhMKC2Fuc3dlcl9lY2hvGAogASgIIkEKE1Rlcm1pbmFsU2hlbGxPcHRpb24SCgoCaWQYASABKAkSDQoFbGFiZWwYAiABKAkSDwoHY29tbWFuZBgDIAEoCSKAAQoOVGVybWluYWxTc2hUYWISCgoCaWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEgwKBGtpbmQYBCABKAkSEgoKY3JlYXRlZF9hdBgFIAEoBBISCgp1cGRhdGVkX2F0GAYgASgEIn8KF1Rlcm1pbmFsU3NoVGFic1NuYXBzaG90EhgKEHByb2plY3RfcGF0aF9rZXkYASABKAkSMgoEdGFicxgCIAMoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoVGFiEhAKCHJldmlzaW9uGAQgASgESgQIAxAEIv0BChdUZXJtaW5hbFNzaExvY2FsRm9yd2FyZBIKCgJpZBgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAyABKAkSEgoKbG9jYWxfaG9zdBgEIAEoCRISCgpsb2NhbF9wb3J0GAUgASgNEg8KB2FkZHJlc3MYBiABKAkSEwoLcmVtb3RlX2hvc3QYByABKAkSEwoLcmVtb3RlX3BvcnQYCCABKA0SDgoGc3RhdHVzGAkgASgJEhIKCmNyZWF0ZWRfYXQYCiABKAQSEgoKdXBkYXRlZF9hdBgLIAEoBBINCgVlcnJvchgMIAEoCSJ1CiBUZXJtaW5hbFNzaExvY2FsRm9yd2FyZHNTbmFwc2hvdBI/Cghmb3J3YXJkcxgBIAMoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoTG9jYWxGb3J3YXJkEhAKCHJldmlzaW9uGAIgASgEIn8KHVRlcm1pbmFsU3NoTG9jYWxGb3J3YXJkQWN0aW9uEj4KB2ZvcndhcmQYASABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaExvY2FsRm9yd2FyZBIQCghyZXZpc2lvbhgCIAEoBBIMCgRraW5kGAMgASgJIqcFChBUZXJtaW5hbFJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRI3CghzZXNzaW9ucxgCIAMoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhI2CgdzZXNzaW9uGAMgASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTZXNzaW9uEg4KBm91dHB1dBgEIAEoDBIRCgl0cnVuY2F0ZWQYBSABKAgSQAoNc2hlbGxfb3B0aW9ucxgGIAMoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2hlbGxPcHRpb24SFQoNZGVmYXVsdF9zaGVsbBgHIAEoCRIbChNvdXRwdXRfc3RhcnRfb2Zmc2V0GAggASgEEhkKEW91dHB1dF9lbmRfb2Zmc2V0GAkgASgEEjsKCnNzaF9wcm9tcHQYCiABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaFByb21wdBISCgpsYXRlbmN5X21zGAsgASgNEj8KCHNzaF90YWJzGAwgASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hUYWJzU25hcHNob3QSUgoSc3NoX2xvY2FsX2ZvcndhcmRzGA0gASgLMjYubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hMb2NhbEZvcndhcmRzU25hcHNob3QSTgoRc3NoX2xvY2FsX2ZvcndhcmQYDiABKAsyMy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaExvY2FsRm9yd2FyZEFjdGlvbhIoCiBzc2hfbG9jYWxfZm9yd2FyZF9wb3J0X2F2YWlsYWJsZRgPIAEoCCLaAgoNVGVybWluYWxFdmVudBIMCgRraW5kGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRI2CgdzZXNzaW9uGAQgASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTZXNzaW9uEgwKBGRhdGEYBSABKAwSGwoTb3V0cHV0X3N0YXJ0X29mZnNldBgGIAEoBBIZChFvdXRwdXRfZW5kX29mZnNldBgHIAEoBBI/Cghzc2hfdGFicxgIIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoVGFic1NuYXBzaG90Ek4KEXNzaF9sb2NhbF9mb3J3YXJkGAkgASgLMjMubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hMb2NhbEZvcndhcmRBY3Rpb24isgIKE1Rlcm1pbmFsU3RyZWFtRnJhbWUSDAoEa2luZBgBIAEoCRIRCglzdHJlYW1faWQYAiABKAkSEgoKc2Vzc2lvbl9pZBgDIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAQgASgJEgsKA3NlcRgFIAEoBBIUCgxzdGFydF9vZmZzZXQYBiABKAQSEgoKZW5kX29mZnNldBgHIAEoBBIMCgRjb2xzGAggASgNEgwKBHJvd3MYCSABKA0SEQoJbWF4X2J5dGVzGAogASgNEhEKCXRydW5jYXRlZBgLIAEoCBINCgVlcnJvchgMIAEoCRI2CgdzZXNzaW9uGA0gASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTZXNzaW9uEgwKBGRhdGEYDiABKAwiQAoKR2l0UmVxdWVzdBIOCgZhY3Rpb24YASABKAkSDwoHd29ya2RpchgCIAEoCRIRCglhcmdzX2pzb24YAyABKAkiMgoLR2l0UmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEhMKC3Jlc3VsdF9qc29uGAIgASgJIroECgtDaGF0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHbWVzc2FnZRgCIAEoCRI/Cg5zZWxlY3RlZF9tb2RlbBgDIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRTZWxlY3RlZE1vZGVsEhYKDmV4ZWN1dGlvbl9tb2RlGAQgASgJEg8KB3dvcmtkaXIYBSABKAkSPgoOdXBsb2FkZWRfZmlsZXMYByADKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0VXBsb2FkZWRGaWxlEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAggASgJEkMKEHJ1bnRpbWVfY29udHJvbHMYCSABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UnVudGltZUNvbnRyb2xzEhQKDHF1ZXVlX3BvbGljeRgKIAEoCRIbChNjb21tYW5kX3NhZmV0eV9tb2RlGAsgASgJElEKGHJlZmVyZW5jZWRfY29udmVyc2F0aW9ucxgMIAMoCzIvLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRDb252ZXJzYXRpb25SZWZlcmVuY2USEwoLZ29hbF9hY3Rpb24YDSABKAkSEQoJZ29hbF90ZXh0GA4gASgJEhYKDnByb21wdF9jb21tYW5kGA8gASgIEhQKDHBsYW5fY29tbWFuZBgQIAEoCEoECAYQB1IVc2VsZWN0ZWRfc3lzdGVtX3Rvb2xzIooBCg5DaGF0TWVzc2FnZVJlZhIVCg1zZWdtZW50X2luZGV4GAEgASgFEhUKDW1lc3NhZ2VfaW5kZXgYAiABKAUSEgoKc2VnbWVudF9pZBgDIAEoCRISCgptZXNzYWdlX2lkGAQgASgJEgwKBHJvbGUYBSABKAkSFAoMY29udGVudF9oYXNoGAYgASgJIjwKEUNhbmNlbENoYXRSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIOCgZydW5faWQYAiABKAkizwEKEkNoYXRDb21tYW5kUmVxdWVzdBIMCgR0eXBlGAEgASgJEjIKB3JlcXVlc3QYAiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UmVxdWVzdBI+ChBiYXNlX21lc3NhZ2VfcmVmGAMgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdE1lc3NhZ2VSZWYSNwoGY2FuY2VsGAQgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2FuY2VsQ2hhdFJlcXVlc3QiuAEKEENoYXRRdWV1ZVJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRIPCgdpdGVtX2lkGAMgASgJEhEKCWRpcmVjdGlvbhgEIAEoCRIQCghyZXZpc2lvbhgFIAEoBBISCgpkcmFmdF9qc29uGAYgASgJEhsKE3VwbG9hZGVkX2ZpbGVzX2pzb24YByABKAkSFAoMcmVxdWVzdF9qc29uGAggASgJIoYBChFDaGF0UXVldWVSZXNwb25zZRIQCghhY2NlcHRlZBgBIAEoCBIPCgdtZXNzYWdlGAIgASgJEhUKDXNuYXBzaG90X2pzb24YAyABKAkSEQoJaXRlbV9qc29uGAQgASgJEhIKCmVycm9yX2NvZGUYBSABKAkSEAoIcmV2aXNpb24YBiABKAQiUgoOQ2hhdFF1ZXVlRXZlbnQSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEhUKDXNuYXBzaG90X2pzb24YAiABKAkSEAoIcmV2aXNpb24YAyABKAQilQIKCUNoYXRFdmVudBI7CgR0eXBlGAEgASgOMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEV2ZW50LkNoYXRFdmVudFR5cGUSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEgwKBGRhdGEYAyABKAkiowEKDUNoYXRFdmVudFR5cGUSCQoFVE9LRU4QABIMCghUSElOS0lORxABEg0KCVRPT0xfQ0FMTBACEg8KC1RPT0xfUkVTVUxUEAMSCAoERE9ORRAEEgkKBUVSUk9SEAUSDwoLVE9PTF9TVEFUVVMQBhIRCg1IT1NURURfU0VBUkNIEAcSEAoMVVNFUl9NRVNTQUdFEAgSDgoKVFJBSkVDVE9SWRAJIrwBChBDaGF0Q29udHJvbEV2ZW50EhIKCnJlcXVlc3RfaWQYASABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAiABKAkSFwoPY29udmVyc2F0aW9uX2lkGAMgASgJEhEKCXJ1bl9lcG9jaBgEIAEoAxIMCgR0eXBlGAUgASgJEg0KBXN0YXRlGAYgASgJEhIKCmVycm9yX2NvZGUYByABKAkSDwoHbWVzc2FnZRgIIAEoCRILCgNzZXEYCSABKAMi/AEKE0NoYXRSdW50aW1lU25hcHNob3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgDIAEoCRIRCgl3b3JrZXJfaWQYBCABKAkSDQoFc3RhdGUYBSABKAkSCwoDY3dkGAYgASgJEhIKCnVwZGF0ZWRfYXQYByABKAMSEAoIcmV2aXNpb24YCCABKAMSFAoMZW50cmllc19qc29uGAkgASgJEhMKC3Rvb2xfc3RhdHVzGAogASgJEiEKGXRvb2xfc3RhdHVzX2lzX2NvbXBhY3Rpb24YCyABKAgi6gEKElJ1bnRpbWVTdGF0dXNFdmVudBIRCgl3b3JrZXJfaWQYASABKAkSDQoFc3RhdGUYAiABKAkSDwoHdmlzaWJsZRgDIAEoCBIYChBhY3RpdmVfcnVuX2NvdW50GAQgASgNEhEKCXRpbWVzdGFtcBgFIAEoAxI4CgthY3RpdmVfcnVucxgGIAMoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRSdW5SZXBvcnQSOgoNZmluaXNoZWRfcnVucxgHIAMoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRSdW5SZXBvcnQigAEKDUNoYXRSdW5SZXBvcnQSDgoGcnVuX2lkGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRINCgVzdGF0ZRgDIAEoCRISCgplcnJvcl9jb2RlGAQgASgJEg8KB21lc3NhZ2UYBSABKAkSEgoKdXBkYXRlZF9hdBgGIAEoAyJHChFDcm9uTWFuYWdlUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSDwoHdGFza19pZBgCIAEoCRIRCgl0YXNrX2pzb24YAyABKAkiOQoSQ3Jvbk1hbmFnZVJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRITCgtyZXN1bHRfanNvbhgCIAEoCSJVChJIaXN0b3J5TGlzdFJlcXVlc3QSDAoEcGFnZRgBIAEoBRIRCglwYWdlX3NpemUYAiABKAUSCwoDY3dkGAMgASgJEhEKCWN3ZF9lbXB0eRgEIAEoCCJsChNIaXN0b3J5TGlzdFJlc3BvbnNlEkAKDWNvbnZlcnNhdGlvbnMYASADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5EhMKC3RvdGFsX2NvdW50GAIgASgFIooCChNDb252ZXJzYXRpb25TdW1tYXJ5EgoKAmlkGAEgASgJEg0KBXRpdGxlGAIgASgJEhIKCmNyZWF0ZWRfYXQYAyABKAMSEgoKdXBkYXRlZF9hdBgEIAEoAxIVCg1tZXNzYWdlX2NvdW50GAUgASgFEhMKC3Byb3ZpZGVyX2lkGAYgASgJEg0KBW1vZGVsGAcgASgJEhIKCnNlc3Npb25faWQYCCABKAkSCwoDY3dkGAkgASgJEhEKCWlzX3Bpbm5lZBgKIAEoCBIRCglwaW5uZWRfYXQYCyABKAMSEQoJaXNfc2hhcmVkGAwgASgIEhsKE3NlbGVjdGVkX21vZGVsX2pzb24YDSABKAkiQgoRSGlzdG9yeUdldFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEhQKDG1heF9tZXNzYWdlcxgCIAEoBSLUAQoSSGlzdG9yeUdldFJlc3BvbnNlEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1tZXNzYWdlc19qc29uGAIgASgJEhsKE3RvdGFsX21lc3NhZ2VfY291bnQYAyABKAUSHgoWcmV0dXJuZWRfbWVzc2FnZV9jb3VudBgEIAEoBRIQCghoYXNfbW9yZRgFIAEoCBI/Cgxjb252ZXJzYXRpb24YBiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5ItEBChZUcmFqZWN0b3J5RmV0Y2hSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRITCgtzZWN0aW9uX2lkcxgCIAMoCRIUCgxtYXhfc2VnbWVudHMYAyABKAUSIQoUYmVmb3JlX3NlZ21lbnRfaW5kZXgYBCABKAVIAIgBARIdChVpbmNsdWRlX3N1YmFnZW50X3J1bnMYBSABKAgSGAoQc3ViYWdlbnRfcnVuX2lkcxgGIAMoCUIXChVfYmVmb3JlX3NlZ21lbnRfaW5kZXgiXAoYVHJhamVjdG9yeVNlY3Rpb25QYXlsb2FkEhIKCnNlY3Rpb25faWQYASABKAkSDAoEc2xvdBgCIAEoCRIPCgdjb250ZW50GAMgASgJEg0KBWJ5dGVzGAQgASgDIqwCChdUcmFqZWN0b3J5RmV0Y2hSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSEwoLZXZlbnRzX2pzb24YAiABKAkSEQoJdHJ1bmNhdGVkGAMgASgIEkAKCHNlY3Rpb25zGAQgAygLMi4ubGl2ZWFnZW50LmdhdGV3YXkudjIuVHJhamVjdG9yeVNlY3Rpb25QYXlsb2FkEhwKFG9sZGVzdF9zZWdtZW50X2luZGV4GAUgASgFEh4KFnJldHVybmVkX3NlZ21lbnRfY291bnQYBiABKAUSGwoTdG90YWxfc2VnbWVudF9jb3VudBgHIAEoBRIXCg9oYXNfbW9yZV9iZWZvcmUYCCABKAgSGgoSc3ViYWdlbnRfcnVuc19qc29uGAkgASgJIoUBChRIaXN0b3J5UHJlZml4UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFAoMbWF4X21lc3NhZ2VzGAIgASgFEj4KEGJhc2VfbWVzc2FnZV9yZWYYAyABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0TWVzc2FnZVJlZiLXAQoVSGlzdG9yeVByZWZpeFJlc3BvbnNlEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1tZXNzYWdlc19qc29uGAIgASgJEhsKE3RvdGFsX21lc3NhZ2VfY291bnQYAyABKAUSHgoWcmV0dXJuZWRfbWVzc2FnZV9jb3VudBgEIAEoBRIQCghoYXNfbW9yZRgFIAEoCBI/Cgxjb252ZXJzYXRpb24YBiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5Ij4KFEhpc3RvcnlSZW5hbWVSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRINCgV0aXRsZRgCIAEoCSJYChVIaXN0b3J5UmVuYW1lUmVzcG9uc2USPwoMY29udmVyc2F0aW9uGAEgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeSJvChRIaXN0b3J5QnJhbmNoUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSPgoQYmFzZV9tZXNzYWdlX3JlZhgCIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRNZXNzYWdlUmVmIlgKFUhpc3RvcnlCcmFuY2hSZXNwb25zZRI/Cgxjb252ZXJzYXRpb24YASABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5Ij8KEUhpc3RvcnlQaW5SZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIRCglpc19waW5uZWQYAiABKAgiVQoSSGlzdG9yeVBpblJlc3BvbnNlEj8KDGNvbnZlcnNhdGlvbhgBIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNvbnZlcnNhdGlvblN1bW1hcnkikgEKEkhpc3RvcnlTaGFyZVN0YXR1cxIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBINCgV0b2tlbhgDIAEoCRISCgpjcmVhdGVkX2F0GAQgASgDEhIKCnVwZGF0ZWRfYXQYBSABKAMSGwoTcmVkYWN0X3Rvb2xfY29udGVudBgGIAEoCCIxChZIaXN0b3J5U2hhcmVHZXRSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCSJSChdIaXN0b3J5U2hhcmVHZXRSZXNwb25zZRI3CgVzaGFyZRgBIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVN0YXR1cyJ8ChZIaXN0b3J5U2hhcmVTZXRSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIPCgdlbmFibGVkGAIgASgIEiAKE3JlZGFjdF90b29sX2NvbnRlbnQYAyABKAhIAIgBAUIWChRfcmVkYWN0X3Rvb2xfY29udGVudCJSChdIaXN0b3J5U2hhcmVTZXRSZXNwb25zZRI3CgVzaGFyZRgBIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVN0YXR1cyIrChpIaXN0b3J5U2hhcmVSZXNvbHZlUmVxdWVzdBINCgV0b2tlbhgBIAEoCSLIAQobSGlzdG9yeVNoYXJlUmVzb2x2ZVJlc3BvbnNlEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1tZXNzYWdlc19qc29uGAIgASgJEhsKE3RvdGFsX21lc3NhZ2VfY291bnQYAyABKAUSPwoMY29udmVyc2F0aW9uGAQgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeRIbChNyZWRhY3RfdG9vbF9jb250ZW50GAUgASgIIhgKFkhpc3RvcnlXb3JrZGlyc1JlcXVlc3QiVQoVSGlzdG9yeVdvcmtkaXJTdW1tYXJ5EgwKBHBhdGgYASABKAkSGgoSY29udmVyc2F0aW9uX2NvdW50GAIgASgFEhIKCnVwZGF0ZWRfYXQYAyABKAMiWAoXSGlzdG9yeVdvcmtkaXJzUmVzcG9uc2USPQoId29ya2RpcnMYASADKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5V29ya2RpclN1bW1hcnkiLwoUSGlzdG9yeURlbGV0ZVJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJIhcKFUhpc3RvcnlEZWxldGVSZXNwb25zZSK3AQoQSGlzdG9yeVN5bmNFdmVudBIMCgRraW5kGAEgASgJEj8KDGNvbnZlcnNhdGlvbhgCIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNvbnZlcnNhdGlvblN1bW1hcnkSFwoPY29udmVyc2F0aW9uX2lkGAMgASgJEhEKCXNvdXJjZV9pZBgEIAEoCRISCgpnZW5lcmF0aW9uGAUgASgJEhQKDGRlbGl2ZXJ5X3NlcRgGIAEoBCKWAQoRSGlzdG9yeVN5bmNTdGF0dXMSEQoJc291cmNlX2lkGAEgASgJEhIKCmdlbmVyYXRpb24YAiABKAkSGwoTbGF0ZXN0X2RlbGl2ZXJ5X3NlcRgDIAEoBBIRCglhdmFpbGFibGUYBCABKAgSFAoMbmVlZHNfcmVzeW5jGAUgASgIEhQKDHJlc3luY190b2tlbhgGIAEoCSKnAgoTSGlzdG9yeVN5bmNTbmFwc2hvdBIRCglzb3VyY2VfaWQYASABKAkSEgoKZ2VuZXJhdGlvbhgCIAEoCRIYChBjdXRfZGVsaXZlcnlfc2VxGAMgASgEEhQKDHJlc3luY190b2tlbhgEIAEoCRJACg1jb252ZXJzYXRpb25zGAUgAygLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeRI9Cgh3b3JrZGlycxgGIAMoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlXb3JrZGlyU3VtbWFyeRIOCgZvZmZzZXQYByABKA0SEwoLbmV4dF9vZmZzZXQYCCABKA0SEwoLdG90YWxfY291bnQYCSABKA0iSwoUSGlzdG9yeVJlc3luY1JlcXVlc3QSFAoMcmVzeW5jX3Rva2VuGAEgASgJEg4KBm9mZnNldBgCIAEoDRINCgVsaW1pdBgDIAEoDSIVChNQcm92aWRlckxpc3RSZXF1ZXN0Ii4KFFByb3ZpZGVyTGlzdFJlc3BvbnNlEhYKDnByb3ZpZGVyc19qc29uGAEgASgJIhQKElNldHRpbmdzR2V0UmVxdWVzdCIsChNTZXR0aW5nc0dldFJlc3BvbnNlEhUKDXNldHRpbmdzX2pzb24YASABKAkiLgoVU2V0dGluZ3NVcGRhdGVSZXF1ZXN0EhUKDXNldHRpbmdzX2pzb24YASABKAkiOwoWU2V0dGluZ3NVcGRhdGVSZXNwb25zZRIQCghhY2NlcHRlZBgBIAEoCBIPCgdtZXNzYWdlGAIgASgJIj4KIFNldHRpbmdzUmVzZXRTc2hLbm93bkhvc3RSZXF1ZXN0EgwKBGhvc3QYASABKAkSDAoEcG9ydBgCIAEoDSI0CiFTZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVzcG9uc2USDwoHZGVsZXRlZBgBIAEoDSIqChFTZXR0aW5nc1N5bmNFdmVudBIVCg1zZXR0aW5nc19qc29uGAEgASgJIhcKFVNraWxsRmlsZXNMaXN0UmVxdWVzdCJMChZTa2lsbEZpbGVzTGlzdFJlc3BvbnNlEhAKCHJvb3RfZGlyGAEgASgJEg0KBXBhdGhzGAIgAygJEhEKCXRydW5jYXRlZBgDIAEoCCIsChlQcm9tcHRUZW1wbGF0ZUxpc3RSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkiUQoVUHJvbXB0VGVtcGxhdGVTdW1tYXJ5EgwKBG5hbWUYASABKAkSEwoLZGVzY3JpcHRpb24YAiABKAkSFQoNYXJndW1lbnRfaGludBgDIAEoCSJcChpQcm9tcHRUZW1wbGF0ZUxpc3RSZXNwb25zZRI+Cgl0ZW1wbGF0ZXMYASADKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Qcm9tcHRUZW1wbGF0ZVN1bW1hcnkiSgobUHJvbXB0VGVtcGxhdGVFeHBhbmRSZXF1ZXN0EgwKBG5hbWUYASABKAkSDAoEdGV4dBgCIAEoCRIPCgd3b3JrZGlyGAMgASgJIjoKHFByb21wdFRlbXBsYXRlRXhwYW5kUmVzcG9uc2USDAoEbmFtZRgBIAEoCRIMCgR0ZXh0GAIgASgJIjwKEUdvYWxNYW5hZ2VSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIOCgZhY3Rpb24YAiABKAkifgoSR29hbE1hbmFnZVJlc3BvbnNlEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIMCgRnb2FsGAIgASgJEg4KBnN0YXR1cxgDIAEoCRINCgVyb3VuZBgEIAEoBRISCgptYXhfcm91bmRzGAUgASgFEg4KBnJlYXNvbhgGIAEoCSIqChdTbGFzaENvbW1hbmRMaXN0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJIm0KE1NsYXNoQ29tbWFuZFN1bW1hcnkSDAoEbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIVCg1hcmd1bWVudF9oaW50GAMgASgJEgwKBGtpbmQYBCABKAkSDgoGc291cmNlGAUgASgJIlcKGFNsYXNoQ29tbWFuZExpc3RSZXNwb25zZRI7Cghjb21tYW5kcxgBIAMoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNsYXNoQ29tbWFuZFN1bW1hcnkikAEKEUxvb3BNYW5hZ2VSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIOCgZhY3Rpb24YAiABKAkSDAoEY3JvbhgDIAEoCRIQCghpbnRlcnZhbBgEIAEoCRIOCgZwcm9tcHQYBSABKAkSEQoJcmVhc29uaW5nGAYgASgJEg8KB2xvb3BfaWQYByABKAkiWgoLTG9vcFN1bW1hcnkSCgoCaWQYASABKAkSEAoIaW50ZXJ2YWwYAiABKAkSDAoEY3JvbhgDIAEoCRIOCgZwcm9tcHQYBCABKAkSDwoHZW5hYmxlZBgFIAEoCCJGChJMb29wTWFuYWdlUmVzcG9uc2USMAoFbG9vcHMYASADKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Mb29wU3VtbWFyeSIoChhTa2lsbE1ldGFkYXRhUmVhZFJlcXVlc3QSDAoEcGF0aBgBIAEoCSI+ChlTa2lsbE1ldGFkYXRhUmVhZFJlc3BvbnNlEgwKBG5hbWUYASABKAkSEwoLZGVzY3JpcHRpb24YAiABKAkiRAoUU2tpbGxUZXh0UmVhZFJlcXVlc3QSDAoEcGF0aBgBIAEoCRIOCgZvZmZzZXQYAiABKA0SDgoGbGVuZ3RoGAMgASgNIjsKFVNraWxsVGV4dFJlYWRSZXNwb25zZRIPCgdjb250ZW50GAEgASgJEhEKCXRydW5jYXRlZBgCIAEoCCIqChJTa2lsbE1hbmFnZVJlcXVlc3QSFAoMcGF5bG9hZF9qc29uGAEgASgJIioKE1NraWxsTWFuYWdlUmVzcG9uc2USEwoLcmVzdWx0X2pzb24YASABKAkidwoWRmlsZU1lbnRpb25MaXN0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEhMKC21heF9yZXN1bHRzGAIgASgNEg0KBXF1ZXJ5GAMgASgJEhgKC3Nob3dfaGlkZGVuGAQgASgISACIAQFCDgoMX3Nob3dfaGlkZGVuIj4KEEZpbGVNZW50aW9uRW50cnkSDAoEcGF0aBgBIAEoCRIMCgRraW5kGAIgASgJEg4KBmhpZGRlbhgDIAEoCCJlChdGaWxlTWVudGlvbkxpc3RSZXNwb25zZRI3CgdlbnRyaWVzGAEgAygLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuRmlsZU1lbnRpb25FbnRyeRIRCgl0cnVuY2F0ZWQYAiABKAgiGgoYSW5zdGFsbGVkQXBwc0xpc3RSZXF1ZXN0IlkKEUluc3RhbGxlZEFwcEVudHJ5EgwKBG5hbWUYASABKAkSEQoJYnVuZGxlX2lkGAIgASgJEgwKBHBhdGgYAyABKAkSFQoNaWNvbl9kYXRhX3VybBgEIAEoCSJSChlJbnN0YWxsZWRBcHBzTGlzdFJlc3BvbnNlEjUKBGFwcHMYASADKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5JbnN0YWxsZWRBcHBFbnRyeSIiChBDdWFEcml2ZXJSZXF1ZXN0Eg4KBmFjdGlvbhgBIAEoCSI4ChFDdWFEcml2ZXJSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSEwoLcmVzdWx0X2pzb24YAiABKAkiOAoSUHJveHlEZXRlY3RSZXF1ZXN0Eg4KBmFjdGlvbhgBIAEoCRISCgppbnB1dF9qc29uGAIgASgJIjoKE1Byb3h5RGV0ZWN0UmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEhMKC3Jlc3VsdF9qc29uGAIgASgJIj8KBkZzUm9vdBIKCgJpZBgBIAEoCRIMCgRwYXRoGAIgASgJEgwKBGtpbmQYAyABKAkSDQoFbGFiZWwYBCABKAkiEAoORnNSb290c1JlcXVlc3QiPgoPRnNSb290c1Jlc3BvbnNlEisKBXJvb3RzGAEgAygLMhwubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSb290IjYKEUZzTGlzdERpcnNSZXF1ZXN0EgwKBHBhdGgYASABKAkSEwoLbWF4X3Jlc3VsdHMYAiABKA0iKAoKRnNEaXJFbnRyeRIMCgRwYXRoGAEgASgJEgwKBG5hbWUYAiABKAkiaAoSRnNMaXN0RGlyc1Jlc3BvbnNlEgwKBHBhdGgYASABKAkSMQoHZW50cmllcxgCIAMoCzIgLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzRGlyRW50cnkSEQoJdHJ1bmNhdGVkGAMgASgIIjwKHEZzQ3JlYXRlUHJvamVjdEZvbGRlclJlcXVlc3QSDgoGcGFyZW50GAEgASgJEgwKBG5hbWUYAiABKAkiLQodRnNDcmVhdGVQcm9qZWN0Rm9sZGVyUmVzcG9uc2USDAoEcGF0aBgBIAEoCSKMAQoNRnNMaXN0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkSDQoFZGVwdGgYAyABKA0SDgoGb2Zmc2V0GAQgASgNEhMKC21heF9yZXN1bHRzGAUgASgNEhgKC3Nob3dfaGlkZGVuGAYgASgISACIAQFCDgoMX3Nob3dfaGlkZGVuIjkKC0ZzTGlzdEVudHJ5EgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCRIOCgZoaWRkZW4YAyABKAgiuQEKDkZzTGlzdFJlc3BvbnNlEgwKBHBhdGgYASABKAkSEAoIaGFzX3BhdGgYAiABKAgSDQoFZGVwdGgYAyABKA0SDgoGb2Zmc2V0GAQgASgNEhMKC21heF9yZXN1bHRzGAUgASgNEg0KBXRvdGFsGAYgASgNEhAKCGhhc19tb3JlGAcgASgIEjIKB2VudHJpZXMYCCADKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3RFbnRyeSI6ChlGc1JlYWRFZGl0YWJsZVRleHRSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSDAoEcGF0aBgCIAEoCSKMAQoaRnNSZWFkRWRpdGFibGVUZXh0UmVzcG9uc2USDAoEcGF0aBgBIAEoCRIPCgdjb250ZW50GAIgASgJEhAKCG10aW1lX21zGAMgASgEEhQKDGNvbnRlbnRfaGFzaBgEIAEoCRISCgpzaXplX2J5dGVzGAUgASgEEhMKC3RvdGFsX2xpbmVzGAYgASgEIjwKG0ZzUmVhZFdvcmtzcGFjZUltYWdlUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkiiQEKHEZzUmVhZFdvcmtzcGFjZUltYWdlUmVzcG9uc2USDAoEcGF0aBgBIAEoCRIRCgltaW1lX3R5cGUYAiABKAkSDAoEZGF0YRgDIAEoCRISCgpzaXplX2J5dGVzGAQgASgEEhAKCG10aW1lX21zGAUgASgEEhQKDGNvbnRlbnRfaGFzaBgGIAEoCSLbAQoTQ2hhdEZpbGVPcGVuUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHd29ya2RpchgCIAEoCRIMCgRwYXRoGAMgASgJEg4KBnNvdXJjZRgEIAEoCRIRCgRsaW5lGAUgASgNSACIAQESFQoIZW5kX2xpbmUYBiABKA1IAYgBARITCgZjb2x1bW4YByABKA1IAogBARIcChRvcGVuX2luX2ZpbGVfbWFuYWdlchgIIAEoCEIHCgVfbGluZUILCglfZW5kX2xpbmVCCQoHX2NvbHVtbiLOAQoUQ2hhdEZpbGVPcGVuUmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEgwKBGtpbmQYAiABKAkSDwoHd29ya2RpchgDIAEoCRIMCgRwYXRoGAQgASgJEhEKBGxpbmUYBSABKA1IAIgBARIVCghlbmRfbGluZRgGIAEoDUgBiAEBEhMKBmNvbHVtbhgHIAEoDUgCiAEBEhkKEW91dHNpZGVfd29ya3NwYWNlGAggASgIQgcKBV9saW5lQgsKCV9lbmRfbGluZUIJCgdfY29sdW1uIs4BChJGc1dyaXRlVGV4dFJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIMCgRwYXRoGAIgASgJEg8KB2NvbnRlbnQYAyABKAkSDAoEbW9kZRgEIAEoCRIZChFleHBlY3RlZF9tdGltZV9tcxgFIAEoBBIdChVleHBlY3RlZF9jb250ZW50X2hhc2gYBiABKAkSHQoVaGFzX2V4cGVjdGVkX210aW1lX21zGAcgASgIEiEKGWhhc19leHBlY3RlZF9jb250ZW50X2hhc2gYCCABKAginQEKE0ZzV3JpdGVUZXh0UmVzcG9uc2USDAoEcGF0aBgBIAEoCRIMCgRtb2RlGAIgASgJEhYKDmV4aXN0ZWRfYmVmb3JlGAMgASgIEhUKDWJ5dGVzX3dyaXR0ZW4YBCABKAQSEAoIbXRpbWVfbXMYBSABKAQSFAoMY29udGVudF9oYXNoGAYgASgJEhMKC3RvdGFsX2xpbmVzGAcgASgEIjMKEkZzQ3JlYXRlRGlyUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkiMQoTRnNDcmVhdGVEaXJSZXNwb25zZRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkiRgoPRnNSZW5hbWVSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSEQoJZnJvbV9wYXRoGAIgASgJEg8KB3RvX3BhdGgYAyABKAkiQQoQRnNSZW5hbWVSZXNwb25zZRIRCglmcm9tX3BhdGgYASABKAkSDAoEcGF0aBgCIAEoCRIMCgRraW5kGAMgASgJIjAKD0ZzRGVsZXRlUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkiLgoQRnNEZWxldGVSZXNwb25zZRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkiIAoLUGluZ1JlcXVlc3QSEQoJdGltZXN0YW1wGAEgASgDIiEKDFBvbmdSZXNwb25zZRIRCgl0aW1lc3RhbXAYASABKAMiLgoNRXJyb3JSZXNwb25zZRIMCgRjb2RlGAEgASgFEg8KB21lc3NhZ2UYAiABKAkiMwoUUHJvdmlkZXJDdXN0b21IZWFkZXISDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSJUChVQcm92aWRlckN1c3RvbUhlYWRlcnMSOwoHaGVhZGVycxgBIAMoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLlByb3ZpZGVyQ3VzdG9tSGVhZGVyIpsCChVQcm92aWRlck1vZGVsc1JlcXVlc3QSFQoNcHJvdmlkZXJfdHlwZRgBIAEoCRIQCghiYXNlX3VybBgCIAEoCRIPCgdhcGlfa2V5GAMgASgJEhgKEHVzZV9zeXN0ZW1fcHJveHkYBCABKAgSEgoKbW9kZWxzX3VybBgFIAEoCRITCgtwcm92aWRlcl9pZBgGIAEoCRIYCgtpc19mdWxsX3VybBgHIAEoCEgAiAEBEkMKDmN1c3RvbV9oZWFkZXJzGAggASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJDdXN0b21IZWFkZXJzEhYKDnJlcXVlc3RfZm9ybWF0GAkgASgJQg4KDF9pc19mdWxsX3VybCItChZQcm92aWRlck1vZGVsc1Jlc3BvbnNlEhMKC21vZGVsc19qc29uGAEgASgJIlEKFFByb3ZpZGVyVXNhZ2VSZXF1ZXN0EhMKC3Byb3ZpZGVyX2lkGAEgASgJEg8KB3JlZnJlc2gYAiABKAgSEwoLY29uZmlnX2pzb24YAyABKAkiLAoVUHJvdmlkZXJVc2FnZVJlc3BvbnNlEhMKC3Jlc3VsdF9qc29uGAEgASgJIogBChBDaGF0SW5ncmVzc0JhdGNoEg4KBnJ1bl9pZBgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSEQoJZmlyc3Rfc2VxGAMgASgEEjgKB3JlY29yZHMYBCADKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc1JlY29yZCKaAgoRQ2hhdEluZ3Jlc3NSZWNvcmQSNwoFZGVsdGEYASABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc0RlbHRhSAASQQoKY2hlY2twb2ludBgCIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzQ2hlY2twb2ludEgAEj0KCHRlcm1pbmFsGAMgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NUZXJtaW5hbEgAEj8KCWhlYXJ0YmVhdBgEIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzSGVhcnRiZWF0SABCCQoHcGF5bG9hZCI5ChBDaGF0SW5ncmVzc0RlbHRhEhIKCmV2ZW50X2pzb24YASABKAkSEQoJd29ya2VyX2lkGAIgASgJIioKFENoYXRJbmdyZXNzSGVhcnRiZWF0EhIKCnVwZGF0ZWRfYXQYASABKAMixAEKFUNoYXRJbmdyZXNzQ2hlY2twb2ludBIaChJjb3ZlcnNfdGhyb3VnaF9zZXEYASABKAQSEAoIcmV2aXNpb24YAiABKAQSHQoVY29tcHJlc3NlZF9wcm9qZWN0aW9uGAMgASgMEhoKEnVuY29tcHJlc3NlZF9ieXRlcxgEIAEoBBIOCgZzaGEyNTYYBSABKAkSGAoQY29udGVudF9jb21wbGV0ZRgGIAEoCBIYChBoaXN0b3J5X3JlcXVpcmVkGAcgASgIIvwBChNDaGF0SW5ncmVzc1Rlcm1pbmFsEhoKEmNvdmVyc190aHJvdWdoX3NlcRgBIAEoBBIQCghyZXZpc2lvbhgCIAEoBBIdChVjb21wcmVzc2VkX3Byb2plY3Rpb24YAyABKAwSGgoSdW5jb21wcmVzc2VkX2J5dGVzGAQgASgEEg4KBnNoYTI1NhgFIAEoCRIYChBjb250ZW50X2NvbXBsZXRlGAYgASgIEhgKEGhpc3RvcnlfcmVxdWlyZWQYByABKAgSDQoFc3RhdGUYCCABKAkSEgoKZXJyb3JfY29kZRgJIAEoCRIVCg1lcnJvcl9tZXNzYWdlGAogASgJIk0KEUNoYXRJbmdyZXNzUmVzdW1lEjgKBHJ1bnMYASADKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc1J1blJlc3VtZSLVAQoUQ2hhdEluZ3Jlc3NSdW5SZXN1bWUSDgoGcnVuX2lkGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRIXCg9yZXBsYXlfZnJvbV9zZXEYAyABKAQSGgoScmVwbGF5X3Rocm91Z2hfc2VxGAQgASgEEhAKCG5leHRfc2VxGAUgASgEEh0KFWxhdGVzdF9jaGVja3BvaW50X3NlcRgGIAEoBBIUCgx0ZXJtaW5hbF9zZXEYByABKAQSGAoQdGVybWluYWxfcGVuZGluZxgIIAEoCCLOAQoTQ2hhdEluZ3Jlc3NGcmFnbWVudBIOCgZydW5faWQYASABKAkSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEhIKCnNvdXJjZV9zZXEYAyABKAQSFgoOZnJhZ21lbnRfaW5kZXgYBCABKA0SFgoOZnJhZ21lbnRfY291bnQYBSABKA0SHAoUZW5jb2RlZF9yZWNvcmRfY2h1bmsYBiABKAwSHAoUZW5jb2RlZF9yZWNvcmRfYnl0ZXMYByABKAQSDgoGc2hhMjU2GAggASgJItwCCg5DaGF0SW5ncmVzc0FjaxIOCgZydW5faWQYASABKAkSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEhkKEWNvbW1pdHRlZF90aHJvdWdoGAMgASgEEhUKDWV4cGVjdGVkX25leHQYBCABKAQSOwoGYWN0aW9uGAUgASgOMisubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NBY2suQWN0aW9uEhoKEnRlcm1pbmFsX2NvbW1pdHRlZBgGIAEoCBISCgplcnJvcl9jb2RlGAcgASgJEhUKDWVycm9yX21lc3NhZ2UYCCABKAkiawoGQWN0aW9uEhYKEkFDVElPTl9VTlNQRUNJRklFRBAAEgwKCENPTlRJTlVFEAESGAoUUkVQTEFZX0ZST01fRVhQRUNURUQQAhITCg9TRU5EX0NIRUNLUE9JTlQQAxIMCghSRUpFQ1RFRBAEIjwKFEhpc3RvcnlTZXRDd2RSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRILCgNjd2QYAiABKAkiWAoVSGlzdG9yeVNldEN3ZFJlc3BvbnNlEj8KDGNvbnZlcnNhdGlvbhgBIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNvbnZlcnNhdGlvblN1bW1hcnkiZgoXV29ya3NwYWNlUm9vdEdyYW50RHJhZnQSDwoCaWQYASABKAlIAIgBARINCgVhbGlhcxgCIAEoCRIUCgxkaXNwbGF5X3BhdGgYAyABKAkSDgoGYWNjZXNzGAQgASgJQgUKA19pZCLSAQoSV29ya3NwYWNlUm9vdEdyYW50EgoKAmlkGAEgASgJEhIKCnByb2plY3RfaWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRINCgVhbGlhcxgEIAEoCRIUCgxkaXNwbGF5X3BhdGgYBSABKAkSFgoOY2Fub25pY2FsX3BhdGgYBiABKAkSDgoGYWNjZXNzGAcgASgJEg0KBXN0YXRlGAggASgJEhIKCmNyZWF0ZWRfYXQYCSABKAMSEgoKdXBkYXRlZF9hdBgKIAEoAyKVAQoaV29ya3NwYWNlUm9vdEdyYW50c1JlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhIKCnByb2plY3RfaWQYAiABKAkSFAoMcHJvamVjdF9wYXRoGAMgASgJEj0KBmdyYW50cxgEIAMoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLldvcmtzcGFjZVJvb3RHcmFudERyYWZ0IlcKG1dvcmtzcGFjZVJvb3RHcmFudHNSZXNwb25zZRI4CgZncmFudHMYASADKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Xb3Jrc3BhY2VSb290R3JhbnQiPAoXQ2hlY2twb2ludEV4cGVjdGVkRW50cnkSCwoDa2V5GAEgASgJEhQKDGN1cnJlbnRfaGFzaBgCIAEoCSKpAQoRQ2hlY2twb2ludFJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRIQCgh0dXJuX3NlcRgDIAEoBBIYChBhdXRob3JpemVkX3Jvb3RzGAQgAygJEj8KCGV4cGVjdGVkGAUgAygLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hlY2twb2ludEV4cGVjdGVkRW50cnkiOQoSQ2hlY2twb2ludFJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRITCgtyZXN1bHRfanNvbhgCIAEoCSJXChlDaGF0Q29udmVyc2F0aW9uUmVmZXJlbmNlEgoKAmlkGAEgASgJEg0KBXRpdGxlGAIgASgJEgsKA2N3ZBgDIAEoCRISCgp1cGRhdGVkX2F0GAQgASgDIjUKD1BsYW5uaW5nUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSEgoKaW5wdXRfanNvbhgCIAEoCSInChBQbGFubmluZ1Jlc3BvbnNlEhMKC3Jlc3VsdF9qc29uGAEgASgJIh4KD1BsYW5uaW5nQ2hhbmdlZBILCgNzZXEYASABKAQq5QEKGEltcG9ydERpcmVjdG9yeU9wZXJhdGlvbhIqCiZJTVBPUlRfRElSRUNUT1JZX09QRVJBVElPTl9VTlNQRUNJRklFRBAAEiQKIElNUE9SVF9ESVJFQ1RPUllfT1BFUkFUSU9OX1NUQVJUEAESKgomSU1QT1JUX0RJUkVDVE9SWV9PUEVSQVRJT05fV1JJVEVfQ0hVTksQAhIlCiFJTVBPUlRfRElSRUNUT1JZX09QRVJBVElPTl9DT01NSVQQAxIkCiBJTVBPUlRfRElSRUNUT1JZX09QRVJBVElPTl9BQk9SVBAEKsYECg9UdW5uZWxGcmFtZUtpbmQSIQodVFVOTkVMX0ZSQU1FX0tJTkRfVU5TUEVDSUZJRUQQABIoCiRUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFUVVFU1RfU1RBUlQQARInCiNUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFUVVFU1RfQk9EWRACEiYKIlRVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVRVUVTVF9FTkQQAxIpCiVUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFU1BPTlNFX1NUQVJUEAQSKAokVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVNQT05TRV9CT0RZEAUSJwojVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVNQT05TRV9FTkQQBhIdChlUVU5ORUxfRlJBTUVfS0lORF9XU19ESUFMEAcSIAocVFVOTkVMX0ZSQU1FX0tJTkRfV1NfRElBTF9PSxAIEiMKH1RVTk5FTF9GUkFNRV9LSU5EX1dTX0RJQUxfRVJST1IQCRIeChpUVU5ORUxfRlJBTUVfS0lORF9XU19GUkFNRRAKEh4KGlRVTk5FTF9GUkFNRV9LSU5EX1dTX0NMT1NFEAsSGwoXVFVOTkVMX0ZSQU1FX0tJTkRfRVJST1IQDBIcChhUVU5ORUxfRlJBTUVfS0lORF9DQU5DRUwQDRIaChZUVU5ORUxfRlJBTUVfS0lORF9QSU5HEA4SGgoWVFVOTkVMX0ZSQU1FX0tJTkRfUE9ORxAPKoEBChNUdW5uZWxXc01lc3NhZ2VUeXBlEiYKIlRVTk5FTF9XU19NRVNTQUdFX1RZUEVfVU5TUEVDSUZJRUQQABIfChtUVU5ORUxfV1NfTUVTU0FHRV9UWVBFX1RFWFQQARIhCh1UVU5ORUxfV1NfTUVTU0FHRV9UWVBFX0JJTkFSWRACQkBaPmdpdGh1Yi5jb20vbGl2ZWFnZW50L2FnZW50LWdhdGV3YXkvaW50ZXJuYWwvcHJvdG8vdjI7Z2F0ZXdheXYyYgZwcm90bzM"); + fileDesc("ChZwcm90by92Mi9nYXRld2F5LnByb3RvEhRsaXZlYWdlbnQuZ2F0ZXdheS52MiLyJAoPR2F0ZXdheUVudmVsb3BlEhIKCnJlcXVlc3RfaWQYASABKAkSEQoJdGltZXN0YW1wGAIgASgDEkAKDGNoYXRfY29tbWFuZBgKIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRDb21tYW5kUmVxdWVzdEgAEj4KC2Nyb25fbWFuYWdlGBQgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuQ3Jvbk1hbmFnZVJlcXVlc3RIABJACgxoaXN0b3J5X2xpc3QYHiABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5TGlzdFJlcXVlc3RIABI+CgtoaXN0b3J5X2dldBgfIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlHZXRSZXF1ZXN0SAASRAoOaGlzdG9yeV9yZW5hbWUYICABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UmVuYW1lUmVxdWVzdEgAEkQKDmhpc3RvcnlfZGVsZXRlGCEgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeURlbGV0ZVJlcXVlc3RIABJECg5oaXN0b3J5X3ByZWZpeBgiIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlQcmVmaXhSZXF1ZXN0SAASPgoLaGlzdG9yeV9waW4YIyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UGluUmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfZ2V0GCQgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlR2V0UmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfc2V0GCUgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlU2V0UmVxdWVzdEgAElEKFWhpc3Rvcnlfc2hhcmVfcmVzb2x2ZRgmIAEoCzIwLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVJlc29sdmVSZXF1ZXN0SAASSAoQaGlzdG9yeV93b3JrZGlycxgnIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlXb3JrZGlyc1JlcXVlc3RIABJCCg1wcm92aWRlcl9saXN0GCggASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJMaXN0UmVxdWVzdEgAEkAKDHNldHRpbmdzX2dldBgpIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzR2V0UmVxdWVzdEgAEkYKD3NldHRpbmdzX3VwZGF0ZRgqIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzVXBkYXRlUmVxdWVzdEgAEkcKEHNraWxsX2ZpbGVzX2xpc3QYKyABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbEZpbGVzTGlzdFJlcXVlc3RIABJNChNza2lsbF9tZXRhZGF0YV9yZWFkGCwgASgLMi4ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxNZXRhZGF0YVJlYWRSZXF1ZXN0SAASRQoPc2tpbGxfdGV4dF9yZWFkGC0gASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxUZXh0UmVhZFJlcXVlc3RIABJJChFmaWxlX21lbnRpb25fbGlzdBguIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZpbGVNZW50aW9uTGlzdFJlcXVlc3RIABJRChV1cGxvYWRfcmVhZGFibGVfZmlsZXMYLyABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRSZWFkYWJsZUZpbGVzUmVxdWVzdEgAEjgKCGZzX3Jvb3RzGDAgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSb290c1JlcXVlc3RIABI/Cgxmc19saXN0X2RpcnMYMSABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3REaXJzUmVxdWVzdEgAEjEKBHBpbmcYMiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5QaW5nUmVxdWVzdEgAElMKFnVwbG9hZGVkX2ltYWdlX3ByZXZpZXcYMyABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRlZEltYWdlUHJldmlld1JlcXVlc3RIABJCCg1tZW1vcnlfbWFuYWdlGDQgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuTWVtb3J5TWFuYWdlUmVxdWVzdEgAEkAKDHNraWxsX21hbmFnZRg1IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsTWFuYWdlUmVxdWVzdEgAElYKGGZzX2NyZWF0ZV9wcm9qZWN0X2ZvbGRlchg2IAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlUHJvamVjdEZvbGRlclJlcXVlc3RIABJBChB0ZXJtaW5hbF9yZXF1ZXN0GDcgASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxSZXF1ZXN0SAASNgoHZnNfbGlzdBg4IAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzTGlzdFJlcXVlc3RIABJBCg1mc193cml0ZV90ZXh0GDkgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNXcml0ZVRleHRSZXF1ZXN0SAASQQoNZnNfY3JlYXRlX2Rpchg6IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlRGlyUmVxdWVzdEgAEjoKCWZzX3JlbmFtZRg7IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVuYW1lUmVxdWVzdEgAEjoKCWZzX2RlbGV0ZRg8IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzRGVsZXRlUmVxdWVzdEgAEjcKC2dpdF9yZXF1ZXN0GD0gASgLMiAubGl2ZWFnZW50LmdhdGV3YXkudjIuR2l0UmVxdWVzdEgAElAKFWZzX3JlYWRfZWRpdGFibGVfdGV4dBg+IAEoCzIvLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZEVkaXRhYmxlVGV4dFJlcXVlc3RIABJUChdmc19yZWFkX3dvcmtzcGFjZV9pbWFnZRg/IAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZFdvcmtzcGFjZUltYWdlUmVxdWVzdEgAEjkKDHNmdHBfcmVxdWVzdBhAIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBSZXF1ZXN0SAASRgoPcHJvdmlkZXJfbW9kZWxzGEEgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJNb2RlbHNSZXF1ZXN0SAASXwodc2V0dGluZ3NfcmVzZXRfc3NoX2tub3duX2hvc3QYSCABKAsyNi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVxdWVzdEgAEjwKCmNoYXRfcXVldWUYSSABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVSZXF1ZXN0SAASQAoQY2hhdF9pbmdyZXNzX2FjaxhLIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzQWNrSAASQQoMdHVubmVsX3N0YXRlGFAgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsU3RhdGVTbmFwc2hvdEgAEj8KD3R1bm5lbF9tdXRhdGlvbhhRIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbE11dGF0aW9uSAASOQoMdHVubmVsX2ZyYW1lGFIgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsRnJhbWVIABJGCg93b3Jrc3BhY2Vfd2F0Y2gYWiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Xb3Jrc3BhY2VXYXRjaFJlcXVlc3RIABJOChdtYW5hZ2VkX3Byb2Nlc3NfcmVxdWVzdBhbIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLk1hbmFnZWRQcm9jZXNzUmVxdWVzdEgAEkQKDmhpc3RvcnlfYnJhbmNoGFwgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeUJyYW5jaFJlcXVlc3RIABJECg5wcm92aWRlcl91c2FnZRhdIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLlByb3ZpZGVyVXNhZ2VSZXF1ZXN0SAASQwoOY2hhdF9maWxlX29wZW4YXiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0RmlsZU9wZW5SZXF1ZXN0SAASRQoPaGlzdG9yeV9zZXRfY3dkGF8gASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNldEN3ZFJlcXVlc3RIABJRChV3b3Jrc3BhY2Vfcm9vdF9ncmFudHMYYCABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Xb3Jrc3BhY2VSb290R3JhbnRzUmVxdWVzdEgAEkgKEGltcG9ydF9kaXJlY3RvcnkYYSABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5JbXBvcnREaXJlY3RvcnlSZXF1ZXN0SAASPQoKY2hlY2twb2ludBhiIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoZWNrcG9pbnRSZXF1ZXN0SAASSAoQdHJhamVjdG9yeV9mZXRjaBhjIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRyYWplY3RvcnlGZXRjaFJlcXVlc3RIABJNChNpbnN0YWxsZWRfYXBwc19saXN0GGQgASgLMi4ubGl2ZWFnZW50LmdhdGV3YXkudjIuSW5zdGFsbGVkQXBwc0xpc3RSZXF1ZXN0SAASQAoMY2xhcmlmeV90dXJuGGUgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2xhcmlmeVR1cm5SZXF1ZXN0SAASPAoKY3VhX2RyaXZlchhmIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkN1YURyaXZlclJlcXVlc3RIABI5CghwbGFubmluZxhnIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlBsYW5uaW5nUmVxdWVzdEgAEk8KFHByb21wdF90ZW1wbGF0ZV9saXN0GGggASgLMi8ubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvbXB0VGVtcGxhdGVMaXN0UmVxdWVzdEgAElMKFnByb21wdF90ZW1wbGF0ZV9leHBhbmQYaSABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Qcm9tcHRUZW1wbGF0ZUV4cGFuZFJlcXVlc3RIABJLChJzbGFzaF9jb21tYW5kX2xpc3QYaiABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TbGFzaENvbW1hbmRMaXN0UmVxdWVzdEgAEj4KC2dvYWxfbWFuYWdlGGsgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuR29hbE1hbmFnZVJlcXVlc3RIABI+Cgtsb29wX21hbmFnZRhsIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkxvb3BNYW5hZ2VSZXF1ZXN0SAASQAoMcHJveHlfZGV0ZWN0GG0gASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJveHlEZXRlY3RSZXF1ZXN0SAASRAoOaGlzdG9yeV9yZXN5bmMYbiABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UmVzeW5jUmVxdWVzdEgAEkgKEHBlcm1pc3Npb25fcnVsZXMYbyABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5QZXJtaXNzaW9uUnVsZXNSZXF1ZXN0SABCCQoHcGF5bG9hZEoECEMQREoECEQQRUoECEUQRkoECEoQSyKNMAoNQWdlbnRFbnZlbG9wZRISCgpyZXF1ZXN0X2lkGAEgASgJEhEKCXRpbWVzdGFtcBgCIAEoAxI1CgpjaGF0X2V2ZW50GAogASgLMh8ubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEV2ZW50SAASRAoQY3Jvbl9tYW5hZ2VfcmVzcBgUIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNyb25NYW5hZ2VSZXNwb25zZUgAEkYKEWhpc3RvcnlfbGlzdF9yZXNwGB4gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeUxpc3RSZXNwb25zZUgAEkQKEGhpc3RvcnlfZ2V0X3Jlc3AYHyABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5R2V0UmVzcG9uc2VIABJKChNoaXN0b3J5X3JlbmFtZV9yZXNwGCAgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVJlbmFtZVJlc3BvbnNlSAASSgoTaGlzdG9yeV9kZWxldGVfcmVzcBghIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlEZWxldGVSZXNwb25zZUgAEj4KDGhpc3Rvcnlfc3luYxgiIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTeW5jRXZlbnRIABJKChNoaXN0b3J5X3ByZWZpeF9yZXNwGCMgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVByZWZpeFJlc3BvbnNlSAASRAoQaGlzdG9yeV9waW5fcmVzcBgkIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlQaW5SZXNwb25zZUgAEk8KFmhpc3Rvcnlfc2hhcmVfZ2V0X3Jlc3AYJSABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2hhcmVHZXRSZXNwb25zZUgAEk8KFmhpc3Rvcnlfc2hhcmVfc2V0X3Jlc3AYJiABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2hhcmVTZXRSZXNwb25zZUgAElcKGmhpc3Rvcnlfc2hhcmVfcmVzb2x2ZV9yZXNwGCcgASgLMjEubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlUmVzb2x2ZVJlc3BvbnNlSAASTgoVaGlzdG9yeV93b3JrZGlyc19yZXNwGDggASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVdvcmtkaXJzUmVzcG9uc2VIABJIChJwcm92aWRlcl9saXN0X3Jlc3AYKCABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Qcm92aWRlckxpc3RSZXNwb25zZUgAEkYKEXNldHRpbmdzX2dldF9yZXNwGCkgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuU2V0dGluZ3NHZXRSZXNwb25zZUgAEkwKFHNldHRpbmdzX3VwZGF0ZV9yZXNwGCogASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuU2V0dGluZ3NVcGRhdGVSZXNwb25zZUgAEkAKDXNldHRpbmdzX3N5bmMYKyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1N5bmNFdmVudEgAEk0KFXNraWxsX2ZpbGVzX2xpc3RfcmVzcBgsIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsRmlsZXNMaXN0UmVzcG9uc2VIABJTChhza2lsbF9tZXRhZGF0YV9yZWFkX3Jlc3AYLSABKAsyLy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbE1ldGFkYXRhUmVhZFJlc3BvbnNlSAASSwoUc2tpbGxfdGV4dF9yZWFkX3Jlc3AYLiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbFRleHRSZWFkUmVzcG9uc2VIABJPChZmaWxlX21lbnRpb25fbGlzdF9yZXNwGC8gASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuRmlsZU1lbnRpb25MaXN0UmVzcG9uc2VIABJXChp1cGxvYWRfcmVhZGFibGVfZmlsZXNfcmVzcBgwIAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYyLlVwbG9hZFJlYWRhYmxlRmlsZXNSZXNwb25zZUgAEj4KDWZzX3Jvb3RzX3Jlc3AYMSABKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1Jvb3RzUmVzcG9uc2VIABIyCgRwb25nGDIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuUG9uZ1Jlc3BvbnNlSAASRQoRZnNfbGlzdF9kaXJzX3Jlc3AYMyABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3REaXJzUmVzcG9uc2VIABJZCht1cGxvYWRlZF9pbWFnZV9wcmV2aWV3X3Jlc3AYNCABKAsyMi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRlZEltYWdlUHJldmlld1Jlc3BvbnNlSAASSAoSbWVtb3J5X21hbmFnZV9yZXNwGDUgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuTWVtb3J5TWFuYWdlUmVzcG9uc2VIABJGChFza2lsbF9tYW5hZ2VfcmVzcBg2IAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsTWFuYWdlUmVzcG9uc2VIABJcCh1mc19jcmVhdGVfcHJvamVjdF9mb2xkZXJfcmVzcBg3IAEoCzIzLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlUHJvamVjdEZvbGRlclJlc3BvbnNlSAASQwoRdGVybWluYWxfcmVzcG9uc2UYOSABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFJlc3BvbnNlSAASPQoOdGVybWluYWxfZXZlbnQYOiABKAsyIy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbEV2ZW50SAASPAoMZnNfbGlzdF9yZXNwGDsgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNMaXN0UmVzcG9uc2VIABJHChJmc193cml0ZV90ZXh0X3Jlc3AYPCABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1dyaXRlVGV4dFJlc3BvbnNlSAASRwoSZnNfY3JlYXRlX2Rpcl9yZXNwGD0gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNDcmVhdGVEaXJSZXNwb25zZUgAEkAKDmZzX3JlbmFtZV9yZXNwGD4gASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSZW5hbWVSZXNwb25zZUgAEkAKDmZzX2RlbGV0ZV9yZXNwGD8gASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNEZWxldGVSZXNwb25zZUgAEjkKDGdpdF9yZXNwb25zZRhAIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYyLkdpdFJlc3BvbnNlSAASVgoaZnNfcmVhZF9lZGl0YWJsZV90ZXh0X3Jlc3AYQSABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1JlYWRFZGl0YWJsZVRleHRSZXNwb25zZUgAEloKHGZzX3JlYWRfd29ya3NwYWNlX2ltYWdlX3Jlc3AYQiABKAsyMi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1JlYWRXb3Jrc3BhY2VJbWFnZVJlc3BvbnNlSAASOwoNc2Z0cF9yZXNwb25zZRhJIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBSZXNwb25zZUgAEjUKCnNmdHBfZXZlbnQYSiABKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZnRwRXZlbnRIABJCCg9jaGF0X3F1ZXVlX3Jlc3AYSyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVSZXNwb25zZUgAEkAKEGNoYXRfcXVldWVfZXZlbnQYTCABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVFdmVudEgAEj4KDGNoYXRfY29udHJvbBhGIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRDb250cm9sRXZlbnRIABJCCg5ydW50aW1lX3N0YXR1cxhHIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlJ1bnRpbWVTdGF0dXNFdmVudEgAEmUKInNldHRpbmdzX3Jlc2V0X3NzaF9rbm93bl9ob3N0X3Jlc3AYSCABKAsyNy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVzcG9uc2VIABJKChVjaGF0X3J1bnRpbWVfc25hcHNob3QYTSABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UnVudGltZVNuYXBzaG90SAASTAoUcHJvdmlkZXJfbW9kZWxzX3Jlc3AYTyABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Qcm92aWRlck1vZGVsc1Jlc3BvbnNlSAASQgoOdHVubmVsX2Rlc2lyZWQYUCABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxEZXNpcmVkU3RhdGVIABJMChZ0dW5uZWxfbXV0YXRpb25fcmVzdWx0GFEgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsTXV0YXRpb25SZXN1bHRIABI5Cgx0dW5uZWxfZnJhbWUYUiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxGcmFtZUgAEkYKE3R1bm5lbF9wcm9iZV9yZXBvcnQYUyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxQcm9iZVJlcG9ydEgAEkoKEndvcmtzcGFjZV9hY3Rpdml0eRhaIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLldvcmtzcGFjZUFjdGl2aXR5RXZlbnRIABJQChhtYW5hZ2VkX3Byb2Nlc3NfcmVzcG9uc2UYWyABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5NYW5hZ2VkUHJvY2Vzc1Jlc3BvbnNlSAASUAoYbWFuYWdlZF9wcm9jZXNzX3NuYXBzaG90GFwgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuTWFuYWdlZFByb2Nlc3NTbmFwc2hvdEgAEkoKE2hpc3RvcnlfYnJhbmNoX3Jlc3AYXSABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5QnJhbmNoUmVzcG9uc2VIABJKChNwcm92aWRlcl91c2FnZV9yZXNwGF4gASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJVc2FnZVJlc3BvbnNlSAASRAoSY2hhdF9pbmdyZXNzX2JhdGNoGF8gASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NCYXRjaEgAEkYKE2NoYXRfaW5ncmVzc19yZXN1bWUYYCABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc1Jlc3VtZUgAEkoKFWNoYXRfaW5ncmVzc19mcmFnbWVudBhhIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzRnJhZ21lbnRIABJJChNjaGF0X2ZpbGVfb3Blbl9yZXNwGGIgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEZpbGVPcGVuUmVzcG9uc2VIABI0CgVlcnJvchhjIAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkVycm9yUmVzcG9uc2VIABJLChRoaXN0b3J5X3NldF9jd2RfcmVzcBhkIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTZXRDd2RSZXNwb25zZUgAElcKGndvcmtzcGFjZV9yb290X2dyYW50c19yZXNwGGUgASgLMjEubGl2ZWFnZW50LmdhdGV3YXkudjIuV29ya3NwYWNlUm9vdEdyYW50c1Jlc3BvbnNlSAASTgoVaW1wb3J0X2RpcmVjdG9yeV9yZXNwGGYgASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuSW1wb3J0RGlyZWN0b3J5UmVzcG9uc2VIABJDCg9jaGVja3BvaW50X3Jlc3AYZyABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGVja3BvaW50UmVzcG9uc2VIABJOChV0cmFqZWN0b3J5X2ZldGNoX3Jlc3AYaCABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UcmFqZWN0b3J5RmV0Y2hSZXNwb25zZUgAElMKGGluc3RhbGxlZF9hcHBzX2xpc3RfcmVzcBhpIAEoCzIvLmxpdmVhZ2VudC5nYXRld2F5LnYyLkluc3RhbGxlZEFwcHNMaXN0UmVzcG9uc2VIABJGChFjbGFyaWZ5X3R1cm5fcmVzcBhqIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNsYXJpZnlUdXJuUmVzcG9uc2VIABJCCg9jdWFfZHJpdmVyX3Jlc3AYayABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DdWFEcml2ZXJSZXNwb25zZUgAEkQKEmNsYXJpZnlfdHVybl9kZWx0YRhsIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNsYXJpZnlUdXJuRGVsdGFIABI/Cg1wbGFubmluZ19yZXNwGG0gASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuUGxhbm5pbmdSZXNwb25zZUgAEkEKEHBsYW5uaW5nX2NoYW5nZWQYbiABKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5QbGFubmluZ0NoYW5nZWRIABJVChlwcm9tcHRfdGVtcGxhdGVfbGlzdF9yZXNwGG8gASgLMjAubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvbXB0VGVtcGxhdGVMaXN0UmVzcG9uc2VIABJZChtwcm9tcHRfdGVtcGxhdGVfZXhwYW5kX3Jlc3AYcCABKAsyMi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Qcm9tcHRUZW1wbGF0ZUV4cGFuZFJlc3BvbnNlSAASUQoXc2xhc2hfY29tbWFuZF9saXN0X3Jlc3AYcSABKAsyLi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TbGFzaENvbW1hbmRMaXN0UmVzcG9uc2VIABJEChBnb2FsX21hbmFnZV9yZXNwGHIgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuR29hbE1hbmFnZVJlc3BvbnNlSAASRAoQbG9vcF9tYW5hZ2VfcmVzcBhzIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkxvb3BNYW5hZ2VSZXNwb25zZUgAEkYKEXByb3h5X2RldGVjdF9yZXNwGHQgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJveHlEZXRlY3RSZXNwb25zZUgAEkYKE2hpc3Rvcnlfc3luY19zdGF0dXMYdSABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U3luY1N0YXR1c0gAEkoKFWhpc3Rvcnlfc3luY19zbmFwc2hvdBh2IAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTeW5jU25hcHNob3RIABJOChVwZXJtaXNzaW9uX3J1bGVzX3Jlc3AYdyABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5QZXJtaXNzaW9uUnVsZXNSZXNwb25zZUgAQgkKB3BheWxvYWRKBAhDEERKBAhEEEVKBAhFEEZKBAhOEE8iVQoRQ2hhdFNlbGVjdGVkTW9kZWwSGgoSY3VzdG9tX3Byb3ZpZGVyX2lkGAEgASgJEg0KBW1vZGVsGAIgASgJEhUKDXByb3ZpZGVyX3R5cGUYAyABKAkigAEKE0NoYXRSdW50aW1lQ29udHJvbHMSGAoQdGhpbmtpbmdfZW5hYmxlZBgBIAEoCBIhChluYXRpdmVfd2ViX3NlYXJjaF9lbmFibGVkGAIgASgIEhEKCXJlYXNvbmluZxgDIAEoCRIZChFwbGFuX21vZGVfZW5hYmxlZBgEIAEoCCKUAQoSQ2xhcmlmeVR1cm5SZXF1ZXN0EhUKDW1lc3NhZ2VzX2pzb24YASABKAkSEwoLcHJvdmlkZXJfaWQYAiABKAkSDQoFbW9kZWwYAyABKAkSQwoQcnVudGltZV9jb250cm9scxgEIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRSdW50aW1lQ29udHJvbHMiVAoTQ2xhcmlmeVR1cm5SZXNwb25zZRISCgpmaW5hbF90ZXh0GAEgASgJEhIKCmVycm9yX2NvZGUYAiABKAkSFQoNZXJyb3JfbWVzc2FnZRgDIAEoCSIgChBDbGFyaWZ5VHVybkRlbHRhEgwKBHRleHQYASABKAkiiQEKEENoYXRVcGxvYWRlZEZpbGUSFQoNcmVsYXRpdmVfcGF0aBgBIAEoCRIVCg1hYnNvbHV0ZV9wYXRoGAIgASgJEhEKCWZpbGVfbmFtZRgDIAEoCRIMCgRraW5kGAQgASgJEhIKCnNpemVfYnl0ZXMYBSABKAMSEgoKZGVkdXBlX2tleRgGIAEoCSJLChJVcGxvYWRSZWFkYWJsZUZpbGUSEQoJZmlsZV9uYW1lGAEgASgJEhEKCW1pbWVfdHlwZRgCIAEoCRIPCgdjb250ZW50GAMgASgMImYKGlVwbG9hZFJlYWRhYmxlRmlsZXNSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSNwoFZmlsZXMYAiADKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRSZWFkYWJsZUZpbGUiZQobVXBsb2FkUmVhZGFibGVGaWxlc1Jlc3BvbnNlEjUKBWZpbGVzGAEgAygLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFVwbG9hZGVkRmlsZRIPCgdza2lwcGVkGAIgAygJIj0KE0ltcG9ydERpcmVjdG9yeUZpbGUSFQoNcmVsYXRpdmVfcGF0aBgBIAEoCRIPCgdjb250ZW50GAIgASgMIsMCChZJbXBvcnREaXJlY3RvcnlSZXF1ZXN0EgwKBG5hbWUYASABKAkSDgoGdGFyZ2V0GAIgASgJEjwKBWZpbGVzGAMgAygLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuSW1wb3J0RGlyZWN0b3J5RmlsZUICGAESEwoLdHJhbnNmZXJfaWQYBCABKAkSQQoJb3BlcmF0aW9uGAUgASgOMi4ubGl2ZWFnZW50LmdhdGV3YXkudjIuSW1wb3J0RGlyZWN0b3J5T3BlcmF0aW9uEhMKC3RvdGFsX2ZpbGVzGAYgASgNEhMKC3RvdGFsX2J5dGVzGAcgASgEEhUKDXJlbGF0aXZlX3BhdGgYCCABKAkSDgoGb2Zmc2V0GAkgASgEEg0KBWNodW5rGAogASgMEhUKDWZpbGVfY29tcGxldGUYCyABKAgifgoXSW1wb3J0RGlyZWN0b3J5UmVzcG9uc2USEQoJcm9vdF9wYXRoGAEgASgJEhIKCmZpbGVfY291bnQYAiABKAUSDwoHc2tpcHBlZBgDIAMoCRITCgt0cmFuc2Zlcl9pZBgEIAEoCRIWCg5yZWNlaXZlZF9ieXRlcxgFIAEoBCJFChtVcGxvYWRlZEltYWdlUHJldmlld1JlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIVCg1hYnNvbHV0ZV9wYXRoGAIgASgJIj8KHFVwbG9hZGVkSW1hZ2VQcmV2aWV3UmVzcG9uc2USEQoJbWltZV90eXBlGAEgASgJEgwKBGRhdGEYAiABKAkiewoKVHVubmVsU3BlYxIKCgJpZBgBIAEoCRIRCglzbHVnX2hpbnQYAiABKAkSDAoEbmFtZRgDIAEoCRISCgp0YXJnZXRfdXJsGAQgASgJEhIKCmV4cGlyZXNfYXQYBSABKAMSGAoQcHJvamVjdF9wYXRoX2tleRgGIAEoCSJZChJUdW5uZWxEZXNpcmVkU3RhdGUSMQoHdHVubmVscxgBIAMoCzIgLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbFNwZWMSEAoIcmV2aXNpb24YAiABKAQiZgoMVHVubmVsSGVhbHRoEg4KBnN0YXR1cxgBIAEoCRITCgtodHRwX3N0YXR1cxgCIAEoDRINCgVlcnJvchgDIAEoCRISCgpjaGVja2VkX2F0GAQgASgDEg4KBnJ0dF9tcxgFIAEoDSLwAQoMVHVubmVsU3RhdHVzEgoKAmlkGAEgASgJEgwKBHNsdWcYAiABKAkSDAoEbmFtZRgDIAEoCRISCgp0YXJnZXRfdXJsGAQgASgJEhMKC3B1YmxpY19wYXRoGAUgASgJEhIKCmNyZWF0ZWRfYXQYBiABKAMSEgoKZXhwaXJlc19hdBgHIAEoAxIaChJhY3RpdmVfY29ubmVjdGlvbnMYCCABKA0SGAoQcHJvamVjdF9wYXRoX2tleRgJIAEoCRIxCgVsb2NhbBgKIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbEhlYWx0aCKlAQoTVHVubmVsU3RhdGVTbmFwc2hvdBIzCgd0dW5uZWxzGAEgAygLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsU3RhdHVzEhAKCHJldmlzaW9uGAIgASgEEhQKDGFnZW50X29ubGluZRgDIAEoCBIxCgVyZWxheRgEIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbEhlYWx0aCKZAQoOVHVubmVsTXV0YXRpb24SDgoGYWN0aW9uGAEgASgJEhEKCXR1bm5lbF9pZBgCIAEoCRISCgp0YXJnZXRfdXJsGAMgASgJEgwKBG5hbWUYBCABKAkSGAoLdHRsX3NlY29uZHMYBSABKA1IAIgBARIYChBwcm9qZWN0X3BhdGhfa2V5GAYgASgJQg4KDF90dGxfc2Vjb25kcyJUChRUdW5uZWxNdXRhdGlvblJlc3VsdBIRCgl0dW5uZWxfaWQYASABKAkSEgoKZXJyb3JfY29kZRgCIAEoCRIVCg1lcnJvcl9tZXNzYWdlGAMgASgJIlkKEVR1bm5lbFByb2JlUmVzdWx0EhEKCXR1bm5lbF9pZBgBIAEoCRIxCgVsb2NhbBgCIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbEhlYWx0aCJNChFUdW5uZWxQcm9iZVJlcG9ydBI4CgdyZXN1bHRzGAEgAygLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsUHJvYmVSZXN1bHQiKwoMVHVubmVsSGVhZGVyEgwKBG5hbWUYASABKAkSDQoFdmFsdWUYAiABKAki9QIKC1R1bm5lbEZyYW1lEhEKCXN0cmVhbV9pZBgBIAEoCRIzCgRraW5kGAIgASgOMiUubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsRnJhbWVLaW5kEhIKCnRhcmdldF91cmwYAyABKAkSDgoGbWV0aG9kGAQgASgJEgwKBHBhdGgYBSABKAkSMwoHaGVhZGVycxgGIAMoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbEhlYWRlchIOCgZzdGF0dXMYByABKA0SDAoEYm9keRgIIAEoDBINCgVlcnJvchgJIAEoCRJCCg93c19tZXNzYWdlX3R5cGUYCiABKA4yKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxXc01lc3NhZ2VUeXBlEhYKDndzX3N1YnByb3RvY29sGAsgASgJEhUKDXdzX2Nsb3NlX2NvZGUYDCABKA0SFwoPd3NfY2xvc2VfcmVhc29uGA0gASgJIikKFVdvcmtzcGFjZVdhdGNoUmVxdWVzdBIQCgh3b3JrZGlycxgBIAMoCSJ+ChZXb3Jrc3BhY2VBY3Rpdml0eUV2ZW50Eg8KB3dvcmtkaXIYASABKAkSEAoIcmV2aXNpb24YAiABKAQSCgoCZnMYAyABKAgSCwoDZ2l0GAQgASgIEhUKDWNoYW5nZWRfcGF0aHMYBSADKAkSEQoJdHJ1bmNhdGVkGAYgASgIIpYCChRNYW5hZ2VkUHJvY2Vzc1JlY29yZBIKCgJpZBgBIAEoCRINCgVsYWJlbBgCIAEoCRIPCgdjb21tYW5kGAMgASgJEgsKA2N3ZBgEIAEoCRINCgVzaGVsbBgFIAEoCRILCgNwaWQYBiABKA0SEAoIbG9nX3BhdGgYByABKAkSEgoKc3RhcnRlZF9hdBgIIAEoAxIYCgtmaW5pc2hlZF9hdBgJIAEoA0gAiAEBEhYKCWV4aXRfY29kZRgKIAEoBUgBiAEBEg8KB3J1bm5pbmcYCyABKAgSEAoIaXNvbGF0ZWQYDCABKAgSEAoIcmVzdG9yZWQYDSABKAhCDgoMX2ZpbmlzaGVkX2F0QgwKCl9leGl0X2NvZGUiaQoWTWFuYWdlZFByb2Nlc3NTbmFwc2hvdBI9Cglwcm9jZXNzZXMYASADKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5NYW5hZ2VkUHJvY2Vzc1JlY29yZBIQCghyZXZpc2lvbhgCIAEoBCJOChVNYW5hZ2VkUHJvY2Vzc1JlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhIKCnByb2Nlc3NfaWQYAiABKAkSEQoJbWF4X2J5dGVzGAMgASgNIrcBChZNYW5hZ2VkUHJvY2Vzc1Jlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRI+CghzbmFwc2hvdBgCIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLk1hbmFnZWRQcm9jZXNzU25hcHNob3QSEwoLbG9nX2NvbnRlbnQYAyABKAkSEAoIbG9nX3BhdGgYBCABKAkSFQoNbG9nX3RydW5jYXRlZBgFIAEoCBIPCgdzdG9wcGVkGAYgASgIIjkKE01lbW9yeU1hbmFnZVJlcXVlc3QSDwoHY29tbWFuZBgBIAEoCRIRCglhcmdzX2pzb24YAiABKAkiKwoUTWVtb3J5TWFuYWdlUmVzcG9uc2USEwoLcmVzdWx0X2pzb24YASABKAkiwQMKD1Rlcm1pbmFsUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSFwoPY29udmVyc2F0aW9uX2lkGBYgASgJEg4KBnJ1bl9pZBgXIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAyABKAkSCwoDY3dkGAQgASgJEg0KBXNoZWxsGAUgASgJEg0KBXRpdGxlGAYgASgJEgwKBGRhdGEYByABKAkSDAoEY29scxgIIAEoDRIMCgRyb3dzGAkgASgNEhEKCW1heF9ieXRlcxgKIAEoDRITCgtzc2hfaG9zdF9pZBgLIAEoCRIRCglwcm9tcHRfaWQYDCABKAkSFQoNcHJvbXB0X2Fuc3dlchgNIAEoCRIWCg50cnVzdF9ob3N0X2tleRgOIAEoCBIUCgxzZnRwX2VuYWJsZWQYDyABKAgSDgoGdGFiX2lkGBAgASgJEhAKCHRhYl9raW5kGBEgASgJEhMKC3JlbW90ZV9ob3N0GBIgASgJEhMKC3JlbW90ZV9wb3J0GBMgASgNEhIKCmxvY2FsX3BvcnQYFCABKA0SEgoKZm9yd2FyZF9pZBgVIAEoCSKyAgoPVGVybWluYWxTZXNzaW9uEgoKAmlkGAEgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAiABKAkSCwoDY3dkGAMgASgJEg0KBXNoZWxsGAQgASgJEg0KBXRpdGxlGAUgASgJEgsKA3BpZBgGIAEoDRIMCgRjb2xzGAcgASgNEgwKBHJvd3MYCCABKA0SEgoKY3JlYXRlZF9hdBgJIAEoBBISCgp1cGRhdGVkX2F0GAogASgEEhMKC2ZpbmlzaGVkX2F0GAsgASgEEhEKCWV4aXRfY29kZRgMIAEoBRIPCgdydW5uaW5nGA0gASgIEgwKBGtpbmQYDiABKAkSNgoDc3NoGA8gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hNZXRhZGF0YSLbAQoTVGVybWluYWxTc2hNZXRhZGF0YRIPCgdob3N0X2lkGAEgASgJEhEKCWhvc3RfbmFtZRgCIAEoCRIQCgh1c2VybmFtZRgDIAEoCRIMCgRob3N0GAQgASgJEgwKBHBvcnQYBSABKA0SEQoJYXV0aF90eXBlGAYgASgJEg4KBnN0YXR1cxgHIAEoCRIZChFyZWNvbm5lY3RfYXR0ZW1wdBgIIAEoDRIeChZyZWNvbm5lY3RfbWF4X2F0dGVtcHRzGAkgASgNEhQKDHNmdHBfZW5hYmxlZBgKIAEoCCKRAwoLU2Z0cFJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRIPCgd3b3JrZGlyGAQgASgJEhIKCmxvY2FsX3BhdGgYBSABKAkSEwoLcmVtb3RlX3BhdGgYBiABKAkSEQoJZnJvbV9wYXRoGAcgASgJEg8KB3RvX3BhdGgYCCABKAkSEQoJZGlyZWN0aW9uGAkgASgJEhMKC3RhcmdldF9wYXRoGAogASgJEhEKCXJlY3Vyc2l2ZRgLIAEoCBIRCglvdmVyd3JpdGUYDCABKAgSDwoHY29udGVudBgNIAEoCRIOCgZvZmZzZXQYDiABKAQSEQoJbWF4X2J5dGVzGA8gASgEEhMKC3N0cmljdF91dGY4GBAgASgIEhYKDmV4cGVjdGVkX210aW1lGBEgASgEEhsKE2V4cGVjdGVkX3NpemVfYnl0ZXMYEiABKAQSGgoSY3JlYXRlX3BhcmVudF9kaXJzGBMgASgIIlgKCVNmdHBFbnRyeRIMCgRwYXRoGAEgASgJEgwKBG5hbWUYAiABKAkSDAoEa2luZBgDIAEoCRISCgpzaXplX2J5dGVzGAQgASgEEg0KBW10aW1lGAUgASgEIvIBCgxTZnRwVHJhbnNmZXISCgoCaWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIRCglkaXJlY3Rpb24YAyABKAkSDgoGc3RhdHVzGAQgASgJEhMKC3NvdXJjZV9wYXRoGAUgASgJEhMKC3RhcmdldF9wYXRoGAYgASgJEhQKDGN1cnJlbnRfcGF0aBgHIAEoCRISCgpieXRlc19kb25lGAggASgEEhMKC2J5dGVzX3RvdGFsGAkgASgEEhIKCmZpbGVzX2RvbmUYCiABKA0SEwoLZmlsZXNfdG90YWwYCyABKA0SDQoFZXJyb3IYDCABKAkisAIKDFNmdHBSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSDAoEcGF0aBgCIAEoCRIwCgdlbnRyaWVzGAMgAygLMh8ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cEVudHJ5Ei4KBWVudHJ5GAQgASgLMh8ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cEVudHJ5Eg4KBmV4aXN0cxgFIAEoCBI0Cgh0cmFuc2ZlchgGIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBUcmFuc2ZlchIPCgdjb250ZW50GAcgASgJEg4KBm9mZnNldBgIIAEoBBISCgpieXRlc19yZWFkGAkgASgEEhIKCnNpemVfYnl0ZXMYCiABKAQSEQoJdHJ1bmNhdGVkGAsgASgIIk8KCVNmdHBFdmVudBIMCgRraW5kGAEgASgJEjQKCHRyYW5zZmVyGAIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cFRyYW5zZmVyIsEBChFUZXJtaW5hbFNzaFByb21wdBIKCgJpZBgBIAEoCRIMCgRraW5kGAIgASgJEg8KB2hvc3RfaWQYAyABKAkSEQoJaG9zdF9uYW1lGAQgASgJEgwKBGhvc3QYBSABKAkSDAoEcG9ydBgGIAEoDRIPCgdtZXNzYWdlGAcgASgJEhoKEmZpbmdlcnByaW50X3NoYTI1NhgIIAEoCRIQCghrZXlfdHlwZRgJIAEoCRITCgthbnN3ZXJfZWNobxgKIAEoCCJBChNUZXJtaW5hbFNoZWxsT3B0aW9uEgoKAmlkGAEgASgJEg0KBWxhYmVsGAIgASgJEg8KB2NvbW1hbmQYAyABKAkigAEKDlRlcm1pbmFsU3NoVGFiEgoKAmlkGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRIMCgRraW5kGAQgASgJEhIKCmNyZWF0ZWRfYXQYBSABKAQSEgoKdXBkYXRlZF9hdBgGIAEoBCJ/ChdUZXJtaW5hbFNzaFRhYnNTbmFwc2hvdBIYChBwcm9qZWN0X3BhdGhfa2V5GAEgASgJEjIKBHRhYnMYAiADKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaFRhYhIQCghyZXZpc2lvbhgEIAEoBEoECAMQBCL9AQoXVGVybWluYWxTc2hMb2NhbEZvcndhcmQSCgoCaWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEhIKCmxvY2FsX2hvc3QYBCABKAkSEgoKbG9jYWxfcG9ydBgFIAEoDRIPCgdhZGRyZXNzGAYgASgJEhMKC3JlbW90ZV9ob3N0GAcgASgJEhMKC3JlbW90ZV9wb3J0GAggASgNEg4KBnN0YXR1cxgJIAEoCRISCgpjcmVhdGVkX2F0GAogASgEEhIKCnVwZGF0ZWRfYXQYCyABKAQSDQoFZXJyb3IYDCABKAkidQogVGVybWluYWxTc2hMb2NhbEZvcndhcmRzU25hcHNob3QSPwoIZm9yd2FyZHMYASADKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaExvY2FsRm9yd2FyZBIQCghyZXZpc2lvbhgCIAEoBCJ/Ch1UZXJtaW5hbFNzaExvY2FsRm9yd2FyZEFjdGlvbhI+Cgdmb3J3YXJkGAEgASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hMb2NhbEZvcndhcmQSEAoIcmV2aXNpb24YAiABKAQSDAoEa2luZBgDIAEoCSKnBQoQVGVybWluYWxSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSNwoIc2Vzc2lvbnMYAiADKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNlc3Npb24SNgoHc2Vzc2lvbhgDIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhIOCgZvdXRwdXQYBCABKAwSEQoJdHJ1bmNhdGVkGAUgASgIEkAKDXNoZWxsX29wdGlvbnMYBiADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNoZWxsT3B0aW9uEhUKDWRlZmF1bHRfc2hlbGwYByABKAkSGwoTb3V0cHV0X3N0YXJ0X29mZnNldBgIIAEoBBIZChFvdXRwdXRfZW5kX29mZnNldBgJIAEoBBI7Cgpzc2hfcHJvbXB0GAogASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hQcm9tcHQSEgoKbGF0ZW5jeV9tcxgLIAEoDRI/Cghzc2hfdGFicxgMIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoVGFic1NuYXBzaG90ElIKEnNzaF9sb2NhbF9mb3J3YXJkcxgNIAEoCzI2LmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoTG9jYWxGb3J3YXJkc1NuYXBzaG90Ek4KEXNzaF9sb2NhbF9mb3J3YXJkGA4gASgLMjMubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hMb2NhbEZvcndhcmRBY3Rpb24SKAogc3NoX2xvY2FsX2ZvcndhcmRfcG9ydF9hdmFpbGFibGUYDyABKAgi2gIKDVRlcm1pbmFsRXZlbnQSDAoEa2luZBgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAyABKAkSNgoHc2Vzc2lvbhgEIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhIMCgRkYXRhGAUgASgMEhsKE291dHB1dF9zdGFydF9vZmZzZXQYBiABKAQSGQoRb3V0cHV0X2VuZF9vZmZzZXQYByABKAQSPwoIc3NoX3RhYnMYCCABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaFRhYnNTbmFwc2hvdBJOChFzc2hfbG9jYWxfZm9yd2FyZBgJIAEoCzIzLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoTG9jYWxGb3J3YXJkQWN0aW9uIrICChNUZXJtaW5hbFN0cmVhbUZyYW1lEgwKBGtpbmQYASABKAkSEQoJc3RyZWFtX2lkGAIgASgJEhIKCnNlc3Npb25faWQYAyABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgEIAEoCRILCgNzZXEYBSABKAQSFAoMc3RhcnRfb2Zmc2V0GAYgASgEEhIKCmVuZF9vZmZzZXQYByABKAQSDAoEY29scxgIIAEoDRIMCgRyb3dzGAkgASgNEhEKCW1heF9ieXRlcxgKIAEoDRIRCgl0cnVuY2F0ZWQYCyABKAgSDQoFZXJyb3IYDCABKAkSNgoHc2Vzc2lvbhgNIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhIMCgRkYXRhGA4gASgMIkAKCkdpdFJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEg8KB3dvcmtkaXIYAiABKAkSEQoJYXJnc19qc29uGAMgASgJIjIKC0dpdFJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRITCgtyZXN1bHRfanNvbhgCIAEoCSK6BAoLQ2hhdFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSPwoOc2VsZWN0ZWRfbW9kZWwYAyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0U2VsZWN0ZWRNb2RlbBIWCg5leGVjdXRpb25fbW9kZRgEIAEoCRIPCgd3b3JrZGlyGAUgASgJEj4KDnVwbG9hZGVkX2ZpbGVzGAcgAygLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFVwbG9hZGVkRmlsZRIZChFjbGllbnRfcmVxdWVzdF9pZBgIIAEoCRJDChBydW50aW1lX2NvbnRyb2xzGAkgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFJ1bnRpbWVDb250cm9scxIUCgxxdWV1ZV9wb2xpY3kYCiABKAkSGwoTY29tbWFuZF9zYWZldHlfbW9kZRgLIAEoCRJRChhyZWZlcmVuY2VkX2NvbnZlcnNhdGlvbnMYDCADKAsyLy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0Q29udmVyc2F0aW9uUmVmZXJlbmNlEhMKC2dvYWxfYWN0aW9uGA0gASgJEhEKCWdvYWxfdGV4dBgOIAEoCRIWCg5wcm9tcHRfY29tbWFuZBgPIAEoCBIUCgxwbGFuX2NvbW1hbmQYECABKAhKBAgGEAdSFXNlbGVjdGVkX3N5c3RlbV90b29scyKKAQoOQ2hhdE1lc3NhZ2VSZWYSFQoNc2VnbWVudF9pbmRleBgBIAEoBRIVCg1tZXNzYWdlX2luZGV4GAIgASgFEhIKCnNlZ21lbnRfaWQYAyABKAkSEgoKbWVzc2FnZV9pZBgEIAEoCRIMCgRyb2xlGAUgASgJEhQKDGNvbnRlbnRfaGFzaBgGIAEoCSI8ChFDYW5jZWxDaGF0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDgoGcnVuX2lkGAIgASgJIs8BChJDaGF0Q29tbWFuZFJlcXVlc3QSDAoEdHlwZRgBIAEoCRIyCgdyZXF1ZXN0GAIgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFJlcXVlc3QSPgoQYmFzZV9tZXNzYWdlX3JlZhgDIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRNZXNzYWdlUmVmEjcKBmNhbmNlbBgEIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNhbmNlbENoYXRSZXF1ZXN0IrgBChBDaGF0UXVldWVSZXF1ZXN0Eg4KBmFjdGlvbhgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSDwoHaXRlbV9pZBgDIAEoCRIRCglkaXJlY3Rpb24YBCABKAkSEAoIcmV2aXNpb24YBSABKAQSEgoKZHJhZnRfanNvbhgGIAEoCRIbChN1cGxvYWRlZF9maWxlc19qc29uGAcgASgJEhQKDHJlcXVlc3RfanNvbhgIIAEoCSKGAQoRQ2hhdFF1ZXVlUmVzcG9uc2USEAoIYWNjZXB0ZWQYASABKAgSDwoHbWVzc2FnZRgCIAEoCRIVCg1zbmFwc2hvdF9qc29uGAMgASgJEhEKCWl0ZW1fanNvbhgEIAEoCRISCgplcnJvcl9jb2RlGAUgASgJEhAKCHJldmlzaW9uGAYgASgEIlIKDkNoYXRRdWV1ZUV2ZW50EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1zbmFwc2hvdF9qc29uGAIgASgJEhAKCHJldmlzaW9uGAMgASgEIpUCCglDaGF0RXZlbnQSOwoEdHlwZRgBIAEoDjItLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRFdmVudC5DaGF0RXZlbnRUeXBlEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRIMCgRkYXRhGAMgASgJIqMBCg1DaGF0RXZlbnRUeXBlEgkKBVRPS0VOEAASDAoIVEhJTktJTkcQARINCglUT09MX0NBTEwQAhIPCgtUT09MX1JFU1VMVBADEggKBERPTkUQBBIJCgVFUlJPUhAFEg8KC1RPT0xfU1RBVFVTEAYSEQoNSE9TVEVEX1NFQVJDSBAHEhAKDFVTRVJfTUVTU0FHRRAIEg4KClRSQUpFQ1RPUlkQCSK8AQoQQ2hhdENvbnRyb2xFdmVudBISCgpyZXF1ZXN0X2lkGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgDIAEoCRIRCglydW5fZXBvY2gYBCABKAMSDAoEdHlwZRgFIAEoCRINCgVzdGF0ZRgGIAEoCRISCgplcnJvcl9jb2RlGAcgASgJEg8KB21lc3NhZ2UYCCABKAkSCwoDc2VxGAkgASgDIvwBChNDaGF0UnVudGltZVNuYXBzaG90EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIOCgZydW5faWQYAiABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAyABKAkSEQoJd29ya2VyX2lkGAQgASgJEg0KBXN0YXRlGAUgASgJEgsKA2N3ZBgGIAEoCRISCgp1cGRhdGVkX2F0GAcgASgDEhAKCHJldmlzaW9uGAggASgDEhQKDGVudHJpZXNfanNvbhgJIAEoCRITCgt0b29sX3N0YXR1cxgKIAEoCRIhChl0b29sX3N0YXR1c19pc19jb21wYWN0aW9uGAsgASgIIuoBChJSdW50aW1lU3RhdHVzRXZlbnQSEQoJd29ya2VyX2lkGAEgASgJEg0KBXN0YXRlGAIgASgJEg8KB3Zpc2libGUYAyABKAgSGAoQYWN0aXZlX3J1bl9jb3VudBgEIAEoDRIRCgl0aW1lc3RhbXAYBSABKAMSOAoLYWN0aXZlX3J1bnMYBiADKAsyIy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UnVuUmVwb3J0EjoKDWZpbmlzaGVkX3J1bnMYByADKAsyIy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UnVuUmVwb3J0IoABCg1DaGF0UnVuUmVwb3J0Eg4KBnJ1bl9pZBgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSDQoFc3RhdGUYAyABKAkSEgoKZXJyb3JfY29kZRgEIAEoCRIPCgdtZXNzYWdlGAUgASgJEhIKCnVwZGF0ZWRfYXQYBiABKAMiRwoRQ3Jvbk1hbmFnZVJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEg8KB3Rhc2tfaWQYAiABKAkSEQoJdGFza19qc29uGAMgASgJIjkKEkNyb25NYW5hZ2VSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSEwoLcmVzdWx0X2pzb24YAiABKAkiVQoSSGlzdG9yeUxpc3RSZXF1ZXN0EgwKBHBhZ2UYASABKAUSEQoJcGFnZV9zaXplGAIgASgFEgsKA2N3ZBgDIAEoCRIRCgljd2RfZW1wdHkYBCABKAgibAoTSGlzdG9yeUxpc3RSZXNwb25zZRJACg1jb252ZXJzYXRpb25zGAEgAygLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeRITCgt0b3RhbF9jb3VudBgCIAEoBSKKAgoTQ29udmVyc2F0aW9uU3VtbWFyeRIKCgJpZBgBIAEoCRINCgV0aXRsZRgCIAEoCRISCgpjcmVhdGVkX2F0GAMgASgDEhIKCnVwZGF0ZWRfYXQYBCABKAMSFQoNbWVzc2FnZV9jb3VudBgFIAEoBRITCgtwcm92aWRlcl9pZBgGIAEoCRINCgVtb2RlbBgHIAEoCRISCgpzZXNzaW9uX2lkGAggASgJEgsKA2N3ZBgJIAEoCRIRCglpc19waW5uZWQYCiABKAgSEQoJcGlubmVkX2F0GAsgASgDEhEKCWlzX3NoYXJlZBgMIAEoCBIbChNzZWxlY3RlZF9tb2RlbF9qc29uGA0gASgJIkIKEUhpc3RvcnlHZXRSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIUCgxtYXhfbWVzc2FnZXMYAiABKAUi1AEKEkhpc3RvcnlHZXRSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNbWVzc2FnZXNfanNvbhgCIAEoCRIbChN0b3RhbF9tZXNzYWdlX2NvdW50GAMgASgFEh4KFnJldHVybmVkX21lc3NhZ2VfY291bnQYBCABKAUSEAoIaGFzX21vcmUYBSABKAgSPwoMY29udmVyc2F0aW9uGAYgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeSLRAQoWVHJhamVjdG9yeUZldGNoUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSEwoLc2VjdGlvbl9pZHMYAiADKAkSFAoMbWF4X3NlZ21lbnRzGAMgASgFEiEKFGJlZm9yZV9zZWdtZW50X2luZGV4GAQgASgFSACIAQESHQoVaW5jbHVkZV9zdWJhZ2VudF9ydW5zGAUgASgIEhgKEHN1YmFnZW50X3J1bl9pZHMYBiADKAlCFwoVX2JlZm9yZV9zZWdtZW50X2luZGV4IlwKGFRyYWplY3RvcnlTZWN0aW9uUGF5bG9hZBISCgpzZWN0aW9uX2lkGAEgASgJEgwKBHNsb3QYAiABKAkSDwoHY29udGVudBgDIAEoCRINCgVieXRlcxgEIAEoAyKsAgoXVHJhamVjdG9yeUZldGNoUmVzcG9uc2USFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEhMKC2V2ZW50c19qc29uGAIgASgJEhEKCXRydW5jYXRlZBgDIAEoCBJACghzZWN0aW9ucxgEIAMoCzIuLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRyYWplY3RvcnlTZWN0aW9uUGF5bG9hZBIcChRvbGRlc3Rfc2VnbWVudF9pbmRleBgFIAEoBRIeChZyZXR1cm5lZF9zZWdtZW50X2NvdW50GAYgASgFEhsKE3RvdGFsX3NlZ21lbnRfY291bnQYByABKAUSFwoPaGFzX21vcmVfYmVmb3JlGAggASgIEhoKEnN1YmFnZW50X3J1bnNfanNvbhgJIAEoCSKFAQoUSGlzdG9yeVByZWZpeFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEhQKDG1heF9tZXNzYWdlcxgCIAEoBRI+ChBiYXNlX21lc3NhZ2VfcmVmGAMgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdE1lc3NhZ2VSZWYi1wEKFUhpc3RvcnlQcmVmaXhSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNbWVzc2FnZXNfanNvbhgCIAEoCRIbChN0b3RhbF9tZXNzYWdlX2NvdW50GAMgASgFEh4KFnJldHVybmVkX21lc3NhZ2VfY291bnQYBCABKAUSEAoIaGFzX21vcmUYBSABKAgSPwoMY29udmVyc2F0aW9uGAYgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeSI+ChRIaXN0b3J5UmVuYW1lUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDQoFdGl0bGUYAiABKAkiWAoVSGlzdG9yeVJlbmFtZVJlc3BvbnNlEj8KDGNvbnZlcnNhdGlvbhgBIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNvbnZlcnNhdGlvblN1bW1hcnkibwoUSGlzdG9yeUJyYW5jaFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEj4KEGJhc2VfbWVzc2FnZV9yZWYYAiABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0TWVzc2FnZVJlZiJYChVIaXN0b3J5QnJhbmNoUmVzcG9uc2USPwoMY29udmVyc2F0aW9uGAEgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeSI/ChFIaXN0b3J5UGluUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSEQoJaXNfcGlubmVkGAIgASgIIlUKEkhpc3RvcnlQaW5SZXNwb25zZRI/Cgxjb252ZXJzYXRpb24YASABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5IpIBChJIaXN0b3J5U2hhcmVTdGF0dXMSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg8KB2VuYWJsZWQYAiABKAgSDQoFdG9rZW4YAyABKAkSEgoKY3JlYXRlZF9hdBgEIAEoAxISCgp1cGRhdGVkX2F0GAUgASgDEhsKE3JlZGFjdF90b29sX2NvbnRlbnQYBiABKAgiMQoWSGlzdG9yeVNoYXJlR2V0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkiUgoXSGlzdG9yeVNoYXJlR2V0UmVzcG9uc2USNwoFc2hhcmUYASABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2hhcmVTdGF0dXMifAoWSGlzdG9yeVNoYXJlU2V0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBIgChNyZWRhY3RfdG9vbF9jb250ZW50GAMgASgISACIAQFCFgoUX3JlZGFjdF90b29sX2NvbnRlbnQiUgoXSGlzdG9yeVNoYXJlU2V0UmVzcG9uc2USNwoFc2hhcmUYASABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2hhcmVTdGF0dXMiKwoaSGlzdG9yeVNoYXJlUmVzb2x2ZVJlcXVlc3QSDQoFdG9rZW4YASABKAkiyAEKG0hpc3RvcnlTaGFyZVJlc29sdmVSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNbWVzc2FnZXNfanNvbhgCIAEoCRIbChN0b3RhbF9tZXNzYWdlX2NvdW50GAMgASgFEj8KDGNvbnZlcnNhdGlvbhgEIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNvbnZlcnNhdGlvblN1bW1hcnkSGwoTcmVkYWN0X3Rvb2xfY29udGVudBgFIAEoCCIYChZIaXN0b3J5V29ya2RpcnNSZXF1ZXN0IlUKFUhpc3RvcnlXb3JrZGlyU3VtbWFyeRIMCgRwYXRoGAEgASgJEhoKEmNvbnZlcnNhdGlvbl9jb3VudBgCIAEoBRISCgp1cGRhdGVkX2F0GAMgASgDIlgKF0hpc3RvcnlXb3JrZGlyc1Jlc3BvbnNlEj0KCHdvcmtkaXJzGAEgAygLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVdvcmtkaXJTdW1tYXJ5Ii8KFEhpc3RvcnlEZWxldGVSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCSIXChVIaXN0b3J5RGVsZXRlUmVzcG9uc2UitwEKEEhpc3RvcnlTeW5jRXZlbnQSDAoEa2luZBgBIAEoCRI/Cgxjb252ZXJzYXRpb24YAiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5EhcKD2NvbnZlcnNhdGlvbl9pZBgDIAEoCRIRCglzb3VyY2VfaWQYBCABKAkSEgoKZ2VuZXJhdGlvbhgFIAEoCRIUCgxkZWxpdmVyeV9zZXEYBiABKAQilgEKEUhpc3RvcnlTeW5jU3RhdHVzEhEKCXNvdXJjZV9pZBgBIAEoCRISCgpnZW5lcmF0aW9uGAIgASgJEhsKE2xhdGVzdF9kZWxpdmVyeV9zZXEYAyABKAQSEQoJYXZhaWxhYmxlGAQgASgIEhQKDG5lZWRzX3Jlc3luYxgFIAEoCBIUCgxyZXN5bmNfdG9rZW4YBiABKAkipwIKE0hpc3RvcnlTeW5jU25hcHNob3QSEQoJc291cmNlX2lkGAEgASgJEhIKCmdlbmVyYXRpb24YAiABKAkSGAoQY3V0X2RlbGl2ZXJ5X3NlcRgDIAEoBBIUCgxyZXN5bmNfdG9rZW4YBCABKAkSQAoNY29udmVyc2F0aW9ucxgFIAMoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNvbnZlcnNhdGlvblN1bW1hcnkSPQoId29ya2RpcnMYBiADKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5V29ya2RpclN1bW1hcnkSDgoGb2Zmc2V0GAcgASgNEhMKC25leHRfb2Zmc2V0GAggASgNEhMKC3RvdGFsX2NvdW50GAkgASgNIksKFEhpc3RvcnlSZXN5bmNSZXF1ZXN0EhQKDHJlc3luY190b2tlbhgBIAEoCRIOCgZvZmZzZXQYAiABKA0SDQoFbGltaXQYAyABKA0iFQoTUHJvdmlkZXJMaXN0UmVxdWVzdCIuChRQcm92aWRlckxpc3RSZXNwb25zZRIWCg5wcm92aWRlcnNfanNvbhgBIAEoCSIUChJTZXR0aW5nc0dldFJlcXVlc3QiLAoTU2V0dGluZ3NHZXRSZXNwb25zZRIVCg1zZXR0aW5nc19qc29uGAEgASgJIi4KFVNldHRpbmdzVXBkYXRlUmVxdWVzdBIVCg1zZXR0aW5nc19qc29uGAEgASgJIjsKFlNldHRpbmdzVXBkYXRlUmVzcG9uc2USEAoIYWNjZXB0ZWQYASABKAgSDwoHbWVzc2FnZRgCIAEoCSI+CiBTZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVxdWVzdBIMCgRob3N0GAEgASgJEgwKBHBvcnQYAiABKA0iNAohU2V0dGluZ3NSZXNldFNzaEtub3duSG9zdFJlc3BvbnNlEg8KB2RlbGV0ZWQYASABKA0iKgoRU2V0dGluZ3NTeW5jRXZlbnQSFQoNc2V0dGluZ3NfanNvbhgBIAEoCSIXChVTa2lsbEZpbGVzTGlzdFJlcXVlc3QiTAoWU2tpbGxGaWxlc0xpc3RSZXNwb25zZRIQCghyb290X2RpchgBIAEoCRINCgVwYXRocxgCIAMoCRIRCgl0cnVuY2F0ZWQYAyABKAgiLAoZUHJvbXB0VGVtcGxhdGVMaXN0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJIlEKFVByb21wdFRlbXBsYXRlU3VtbWFyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEhUKDWFyZ3VtZW50X2hpbnQYAyABKAkiXAoaUHJvbXB0VGVtcGxhdGVMaXN0UmVzcG9uc2USPgoJdGVtcGxhdGVzGAEgAygLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvbXB0VGVtcGxhdGVTdW1tYXJ5IkoKG1Byb21wdFRlbXBsYXRlRXhwYW5kUmVxdWVzdBIMCgRuYW1lGAEgASgJEgwKBHRleHQYAiABKAkSDwoHd29ya2RpchgDIAEoCSI6ChxQcm9tcHRUZW1wbGF0ZUV4cGFuZFJlc3BvbnNlEgwKBG5hbWUYASABKAkSDAoEdGV4dBgCIAEoCSI8ChFHb2FsTWFuYWdlUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDgoGYWN0aW9uGAIgASgJIn4KEkdvYWxNYW5hZ2VSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDAoEZ29hbBgCIAEoCRIOCgZzdGF0dXMYAyABKAkSDQoFcm91bmQYBCABKAUSEgoKbWF4X3JvdW5kcxgFIAEoBRIOCgZyZWFzb24YBiABKAkiKgoXU2xhc2hDb21tYW5kTGlzdFJlcXVlc3QSDwoHd29ya2RpchgBIAEoCSJtChNTbGFzaENvbW1hbmRTdW1tYXJ5EgwKBG5hbWUYASABKAkSEwoLZGVzY3JpcHRpb24YAiABKAkSFQoNYXJndW1lbnRfaGludBgDIAEoCRIMCgRraW5kGAQgASgJEg4KBnNvdXJjZRgFIAEoCSJXChhTbGFzaENvbW1hbmRMaXN0UmVzcG9uc2USOwoIY29tbWFuZHMYASADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TbGFzaENvbW1hbmRTdW1tYXJ5IpABChFMb29wTWFuYWdlUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDgoGYWN0aW9uGAIgASgJEgwKBGNyb24YAyABKAkSEAoIaW50ZXJ2YWwYBCABKAkSDgoGcHJvbXB0GAUgASgJEhEKCXJlYXNvbmluZxgGIAEoCRIPCgdsb29wX2lkGAcgASgJIloKC0xvb3BTdW1tYXJ5EgoKAmlkGAEgASgJEhAKCGludGVydmFsGAIgASgJEgwKBGNyb24YAyABKAkSDgoGcHJvbXB0GAQgASgJEg8KB2VuYWJsZWQYBSABKAgiRgoSTG9vcE1hbmFnZVJlc3BvbnNlEjAKBWxvb3BzGAEgAygLMiEubGl2ZWFnZW50LmdhdGV3YXkudjIuTG9vcFN1bW1hcnkiKAoYU2tpbGxNZXRhZGF0YVJlYWRSZXF1ZXN0EgwKBHBhdGgYASABKAkiPgoZU2tpbGxNZXRhZGF0YVJlYWRSZXNwb25zZRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJIkQKFFNraWxsVGV4dFJlYWRSZXF1ZXN0EgwKBHBhdGgYASABKAkSDgoGb2Zmc2V0GAIgASgNEg4KBmxlbmd0aBgDIAEoDSI7ChVTa2lsbFRleHRSZWFkUmVzcG9uc2USDwoHY29udGVudBgBIAEoCRIRCgl0cnVuY2F0ZWQYAiABKAgiKgoSU2tpbGxNYW5hZ2VSZXF1ZXN0EhQKDHBheWxvYWRfanNvbhgBIAEoCSIqChNTa2lsbE1hbmFnZVJlc3BvbnNlEhMKC3Jlc3VsdF9qc29uGAEgASgJIncKFkZpbGVNZW50aW9uTGlzdFJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRITCgttYXhfcmVzdWx0cxgCIAEoDRINCgVxdWVyeRgDIAEoCRIYCgtzaG93X2hpZGRlbhgEIAEoCEgAiAEBQg4KDF9zaG93X2hpZGRlbiI+ChBGaWxlTWVudGlvbkVudHJ5EgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCRIOCgZoaWRkZW4YAyABKAgiZQoXRmlsZU1lbnRpb25MaXN0UmVzcG9uc2USNwoHZW50cmllcxgBIAMoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZpbGVNZW50aW9uRW50cnkSEQoJdHJ1bmNhdGVkGAIgASgIIhoKGEluc3RhbGxlZEFwcHNMaXN0UmVxdWVzdCJZChFJbnN0YWxsZWRBcHBFbnRyeRIMCgRuYW1lGAEgASgJEhEKCWJ1bmRsZV9pZBgCIAEoCRIMCgRwYXRoGAMgASgJEhUKDWljb25fZGF0YV91cmwYBCABKAkiUgoZSW5zdGFsbGVkQXBwc0xpc3RSZXNwb25zZRI1CgRhcHBzGAEgAygLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuSW5zdGFsbGVkQXBwRW50cnkiIgoQQ3VhRHJpdmVyUmVxdWVzdBIOCgZhY3Rpb24YASABKAkiOAoRQ3VhRHJpdmVyUmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEhMKC3Jlc3VsdF9qc29uGAIgASgJIjgKElByb3h5RGV0ZWN0UmVxdWVzdBIOCgZhY3Rpb24YASABKAkSEgoKaW5wdXRfanNvbhgCIAEoCSI6ChNQcm94eURldGVjdFJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRITCgtyZXN1bHRfanNvbhgCIAEoCSI8ChZQZXJtaXNzaW9uUnVsZXNSZXF1ZXN0Eg4KBmFjdGlvbhgBIAEoCRISCgppbnB1dF9qc29uGAIgASgJIi4KF1Blcm1pc3Npb25SdWxlc1Jlc3BvbnNlEhMKC3Jlc3VsdF9qc29uGAEgASgJIj8KBkZzUm9vdBIKCgJpZBgBIAEoCRIMCgRwYXRoGAIgASgJEgwKBGtpbmQYAyABKAkSDQoFbGFiZWwYBCABKAkiEAoORnNSb290c1JlcXVlc3QiPgoPRnNSb290c1Jlc3BvbnNlEisKBXJvb3RzGAEgAygLMhwubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSb290IjYKEUZzTGlzdERpcnNSZXF1ZXN0EgwKBHBhdGgYASABKAkSEwoLbWF4X3Jlc3VsdHMYAiABKA0iKAoKRnNEaXJFbnRyeRIMCgRwYXRoGAEgASgJEgwKBG5hbWUYAiABKAkiaAoSRnNMaXN0RGlyc1Jlc3BvbnNlEgwKBHBhdGgYASABKAkSMQoHZW50cmllcxgCIAMoCzIgLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzRGlyRW50cnkSEQoJdHJ1bmNhdGVkGAMgASgIIjwKHEZzQ3JlYXRlUHJvamVjdEZvbGRlclJlcXVlc3QSDgoGcGFyZW50GAEgASgJEgwKBG5hbWUYAiABKAkiLQodRnNDcmVhdGVQcm9qZWN0Rm9sZGVyUmVzcG9uc2USDAoEcGF0aBgBIAEoCSKMAQoNRnNMaXN0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkSDQoFZGVwdGgYAyABKA0SDgoGb2Zmc2V0GAQgASgNEhMKC21heF9yZXN1bHRzGAUgASgNEhgKC3Nob3dfaGlkZGVuGAYgASgISACIAQFCDgoMX3Nob3dfaGlkZGVuIjkKC0ZzTGlzdEVudHJ5EgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCRIOCgZoaWRkZW4YAyABKAgiuQEKDkZzTGlzdFJlc3BvbnNlEgwKBHBhdGgYASABKAkSEAoIaGFzX3BhdGgYAiABKAgSDQoFZGVwdGgYAyABKA0SDgoGb2Zmc2V0GAQgASgNEhMKC21heF9yZXN1bHRzGAUgASgNEg0KBXRvdGFsGAYgASgNEhAKCGhhc19tb3JlGAcgASgIEjIKB2VudHJpZXMYCCADKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3RFbnRyeSI6ChlGc1JlYWRFZGl0YWJsZVRleHRSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSDAoEcGF0aBgCIAEoCSKMAQoaRnNSZWFkRWRpdGFibGVUZXh0UmVzcG9uc2USDAoEcGF0aBgBIAEoCRIPCgdjb250ZW50GAIgASgJEhAKCG10aW1lX21zGAMgASgEEhQKDGNvbnRlbnRfaGFzaBgEIAEoCRISCgpzaXplX2J5dGVzGAUgASgEEhMKC3RvdGFsX2xpbmVzGAYgASgEIjwKG0ZzUmVhZFdvcmtzcGFjZUltYWdlUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkiiQEKHEZzUmVhZFdvcmtzcGFjZUltYWdlUmVzcG9uc2USDAoEcGF0aBgBIAEoCRIRCgltaW1lX3R5cGUYAiABKAkSDAoEZGF0YRgDIAEoCRISCgpzaXplX2J5dGVzGAQgASgEEhAKCG10aW1lX21zGAUgASgEEhQKDGNvbnRlbnRfaGFzaBgGIAEoCSLbAQoTQ2hhdEZpbGVPcGVuUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHd29ya2RpchgCIAEoCRIMCgRwYXRoGAMgASgJEg4KBnNvdXJjZRgEIAEoCRIRCgRsaW5lGAUgASgNSACIAQESFQoIZW5kX2xpbmUYBiABKA1IAYgBARITCgZjb2x1bW4YByABKA1IAogBARIcChRvcGVuX2luX2ZpbGVfbWFuYWdlchgIIAEoCEIHCgVfbGluZUILCglfZW5kX2xpbmVCCQoHX2NvbHVtbiLOAQoUQ2hhdEZpbGVPcGVuUmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEgwKBGtpbmQYAiABKAkSDwoHd29ya2RpchgDIAEoCRIMCgRwYXRoGAQgASgJEhEKBGxpbmUYBSABKA1IAIgBARIVCghlbmRfbGluZRgGIAEoDUgBiAEBEhMKBmNvbHVtbhgHIAEoDUgCiAEBEhkKEW91dHNpZGVfd29ya3NwYWNlGAggASgIQgcKBV9saW5lQgsKCV9lbmRfbGluZUIJCgdfY29sdW1uIs4BChJGc1dyaXRlVGV4dFJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIMCgRwYXRoGAIgASgJEg8KB2NvbnRlbnQYAyABKAkSDAoEbW9kZRgEIAEoCRIZChFleHBlY3RlZF9tdGltZV9tcxgFIAEoBBIdChVleHBlY3RlZF9jb250ZW50X2hhc2gYBiABKAkSHQoVaGFzX2V4cGVjdGVkX210aW1lX21zGAcgASgIEiEKGWhhc19leHBlY3RlZF9jb250ZW50X2hhc2gYCCABKAginQEKE0ZzV3JpdGVUZXh0UmVzcG9uc2USDAoEcGF0aBgBIAEoCRIMCgRtb2RlGAIgASgJEhYKDmV4aXN0ZWRfYmVmb3JlGAMgASgIEhUKDWJ5dGVzX3dyaXR0ZW4YBCABKAQSEAoIbXRpbWVfbXMYBSABKAQSFAoMY29udGVudF9oYXNoGAYgASgJEhMKC3RvdGFsX2xpbmVzGAcgASgEIjMKEkZzQ3JlYXRlRGlyUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkiMQoTRnNDcmVhdGVEaXJSZXNwb25zZRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkiRgoPRnNSZW5hbWVSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSEQoJZnJvbV9wYXRoGAIgASgJEg8KB3RvX3BhdGgYAyABKAkiQQoQRnNSZW5hbWVSZXNwb25zZRIRCglmcm9tX3BhdGgYASABKAkSDAoEcGF0aBgCIAEoCRIMCgRraW5kGAMgASgJIjAKD0ZzRGVsZXRlUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkiLgoQRnNEZWxldGVSZXNwb25zZRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkiIAoLUGluZ1JlcXVlc3QSEQoJdGltZXN0YW1wGAEgASgDIiEKDFBvbmdSZXNwb25zZRIRCgl0aW1lc3RhbXAYASABKAMiLgoNRXJyb3JSZXNwb25zZRIMCgRjb2RlGAEgASgFEg8KB21lc3NhZ2UYAiABKAkiMwoUUHJvdmlkZXJDdXN0b21IZWFkZXISDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSJUChVQcm92aWRlckN1c3RvbUhlYWRlcnMSOwoHaGVhZGVycxgBIAMoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLlByb3ZpZGVyQ3VzdG9tSGVhZGVyIpsCChVQcm92aWRlck1vZGVsc1JlcXVlc3QSFQoNcHJvdmlkZXJfdHlwZRgBIAEoCRIQCghiYXNlX3VybBgCIAEoCRIPCgdhcGlfa2V5GAMgASgJEhgKEHVzZV9zeXN0ZW1fcHJveHkYBCABKAgSEgoKbW9kZWxzX3VybBgFIAEoCRITCgtwcm92aWRlcl9pZBgGIAEoCRIYCgtpc19mdWxsX3VybBgHIAEoCEgAiAEBEkMKDmN1c3RvbV9oZWFkZXJzGAggASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJDdXN0b21IZWFkZXJzEhYKDnJlcXVlc3RfZm9ybWF0GAkgASgJQg4KDF9pc19mdWxsX3VybCItChZQcm92aWRlck1vZGVsc1Jlc3BvbnNlEhMKC21vZGVsc19qc29uGAEgASgJIlEKFFByb3ZpZGVyVXNhZ2VSZXF1ZXN0EhMKC3Byb3ZpZGVyX2lkGAEgASgJEg8KB3JlZnJlc2gYAiABKAgSEwoLY29uZmlnX2pzb24YAyABKAkiLAoVUHJvdmlkZXJVc2FnZVJlc3BvbnNlEhMKC3Jlc3VsdF9qc29uGAEgASgJIogBChBDaGF0SW5ncmVzc0JhdGNoEg4KBnJ1bl9pZBgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSEQoJZmlyc3Rfc2VxGAMgASgEEjgKB3JlY29yZHMYBCADKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc1JlY29yZCKaAgoRQ2hhdEluZ3Jlc3NSZWNvcmQSNwoFZGVsdGEYASABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc0RlbHRhSAASQQoKY2hlY2twb2ludBgCIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzQ2hlY2twb2ludEgAEj0KCHRlcm1pbmFsGAMgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NUZXJtaW5hbEgAEj8KCWhlYXJ0YmVhdBgEIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzSGVhcnRiZWF0SABCCQoHcGF5bG9hZCI5ChBDaGF0SW5ncmVzc0RlbHRhEhIKCmV2ZW50X2pzb24YASABKAkSEQoJd29ya2VyX2lkGAIgASgJIioKFENoYXRJbmdyZXNzSGVhcnRiZWF0EhIKCnVwZGF0ZWRfYXQYASABKAMixAEKFUNoYXRJbmdyZXNzQ2hlY2twb2ludBIaChJjb3ZlcnNfdGhyb3VnaF9zZXEYASABKAQSEAoIcmV2aXNpb24YAiABKAQSHQoVY29tcHJlc3NlZF9wcm9qZWN0aW9uGAMgASgMEhoKEnVuY29tcHJlc3NlZF9ieXRlcxgEIAEoBBIOCgZzaGEyNTYYBSABKAkSGAoQY29udGVudF9jb21wbGV0ZRgGIAEoCBIYChBoaXN0b3J5X3JlcXVpcmVkGAcgASgIIvwBChNDaGF0SW5ncmVzc1Rlcm1pbmFsEhoKEmNvdmVyc190aHJvdWdoX3NlcRgBIAEoBBIQCghyZXZpc2lvbhgCIAEoBBIdChVjb21wcmVzc2VkX3Byb2plY3Rpb24YAyABKAwSGgoSdW5jb21wcmVzc2VkX2J5dGVzGAQgASgEEg4KBnNoYTI1NhgFIAEoCRIYChBjb250ZW50X2NvbXBsZXRlGAYgASgIEhgKEGhpc3RvcnlfcmVxdWlyZWQYByABKAgSDQoFc3RhdGUYCCABKAkSEgoKZXJyb3JfY29kZRgJIAEoCRIVCg1lcnJvcl9tZXNzYWdlGAogASgJIk0KEUNoYXRJbmdyZXNzUmVzdW1lEjgKBHJ1bnMYASADKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc1J1blJlc3VtZSLVAQoUQ2hhdEluZ3Jlc3NSdW5SZXN1bWUSDgoGcnVuX2lkGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRIXCg9yZXBsYXlfZnJvbV9zZXEYAyABKAQSGgoScmVwbGF5X3Rocm91Z2hfc2VxGAQgASgEEhAKCG5leHRfc2VxGAUgASgEEh0KFWxhdGVzdF9jaGVja3BvaW50X3NlcRgGIAEoBBIUCgx0ZXJtaW5hbF9zZXEYByABKAQSGAoQdGVybWluYWxfcGVuZGluZxgIIAEoCCLOAQoTQ2hhdEluZ3Jlc3NGcmFnbWVudBIOCgZydW5faWQYASABKAkSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEhIKCnNvdXJjZV9zZXEYAyABKAQSFgoOZnJhZ21lbnRfaW5kZXgYBCABKA0SFgoOZnJhZ21lbnRfY291bnQYBSABKA0SHAoUZW5jb2RlZF9yZWNvcmRfY2h1bmsYBiABKAwSHAoUZW5jb2RlZF9yZWNvcmRfYnl0ZXMYByABKAQSDgoGc2hhMjU2GAggASgJItwCCg5DaGF0SW5ncmVzc0FjaxIOCgZydW5faWQYASABKAkSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEhkKEWNvbW1pdHRlZF90aHJvdWdoGAMgASgEEhUKDWV4cGVjdGVkX25leHQYBCABKAQSOwoGYWN0aW9uGAUgASgOMisubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NBY2suQWN0aW9uEhoKEnRlcm1pbmFsX2NvbW1pdHRlZBgGIAEoCBISCgplcnJvcl9jb2RlGAcgASgJEhUKDWVycm9yX21lc3NhZ2UYCCABKAkiawoGQWN0aW9uEhYKEkFDVElPTl9VTlNQRUNJRklFRBAAEgwKCENPTlRJTlVFEAESGAoUUkVQTEFZX0ZST01fRVhQRUNURUQQAhITCg9TRU5EX0NIRUNLUE9JTlQQAxIMCghSRUpFQ1RFRBAEIjwKFEhpc3RvcnlTZXRDd2RSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRILCgNjd2QYAiABKAkiWAoVSGlzdG9yeVNldEN3ZFJlc3BvbnNlEj8KDGNvbnZlcnNhdGlvbhgBIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNvbnZlcnNhdGlvblN1bW1hcnkiZgoXV29ya3NwYWNlUm9vdEdyYW50RHJhZnQSDwoCaWQYASABKAlIAIgBARINCgVhbGlhcxgCIAEoCRIUCgxkaXNwbGF5X3BhdGgYAyABKAkSDgoGYWNjZXNzGAQgASgJQgUKA19pZCLSAQoSV29ya3NwYWNlUm9vdEdyYW50EgoKAmlkGAEgASgJEhIKCnByb2plY3RfaWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRINCgVhbGlhcxgEIAEoCRIUCgxkaXNwbGF5X3BhdGgYBSABKAkSFgoOY2Fub25pY2FsX3BhdGgYBiABKAkSDgoGYWNjZXNzGAcgASgJEg0KBXN0YXRlGAggASgJEhIKCmNyZWF0ZWRfYXQYCSABKAMSEgoKdXBkYXRlZF9hdBgKIAEoAyKVAQoaV29ya3NwYWNlUm9vdEdyYW50c1JlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhIKCnByb2plY3RfaWQYAiABKAkSFAoMcHJvamVjdF9wYXRoGAMgASgJEj0KBmdyYW50cxgEIAMoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLldvcmtzcGFjZVJvb3RHcmFudERyYWZ0IlcKG1dvcmtzcGFjZVJvb3RHcmFudHNSZXNwb25zZRI4CgZncmFudHMYASADKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Xb3Jrc3BhY2VSb290R3JhbnQiPAoXQ2hlY2twb2ludEV4cGVjdGVkRW50cnkSCwoDa2V5GAEgASgJEhQKDGN1cnJlbnRfaGFzaBgCIAEoCSKpAQoRQ2hlY2twb2ludFJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRIQCgh0dXJuX3NlcRgDIAEoBBIYChBhdXRob3JpemVkX3Jvb3RzGAQgAygJEj8KCGV4cGVjdGVkGAUgAygLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hlY2twb2ludEV4cGVjdGVkRW50cnkiOQoSQ2hlY2twb2ludFJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRITCgtyZXN1bHRfanNvbhgCIAEoCSJXChlDaGF0Q29udmVyc2F0aW9uUmVmZXJlbmNlEgoKAmlkGAEgASgJEg0KBXRpdGxlGAIgASgJEgsKA2N3ZBgDIAEoCRISCgp1cGRhdGVkX2F0GAQgASgDIjUKD1BsYW5uaW5nUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSEgoKaW5wdXRfanNvbhgCIAEoCSInChBQbGFubmluZ1Jlc3BvbnNlEhMKC3Jlc3VsdF9qc29uGAEgASgJIh4KD1BsYW5uaW5nQ2hhbmdlZBILCgNzZXEYASABKAQq5QEKGEltcG9ydERpcmVjdG9yeU9wZXJhdGlvbhIqCiZJTVBPUlRfRElSRUNUT1JZX09QRVJBVElPTl9VTlNQRUNJRklFRBAAEiQKIElNUE9SVF9ESVJFQ1RPUllfT1BFUkFUSU9OX1NUQVJUEAESKgomSU1QT1JUX0RJUkVDVE9SWV9PUEVSQVRJT05fV1JJVEVfQ0hVTksQAhIlCiFJTVBPUlRfRElSRUNUT1JZX09QRVJBVElPTl9DT01NSVQQAxIkCiBJTVBPUlRfRElSRUNUT1JZX09QRVJBVElPTl9BQk9SVBAEKsYECg9UdW5uZWxGcmFtZUtpbmQSIQodVFVOTkVMX0ZSQU1FX0tJTkRfVU5TUEVDSUZJRUQQABIoCiRUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFUVVFU1RfU1RBUlQQARInCiNUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFUVVFU1RfQk9EWRACEiYKIlRVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVRVUVTVF9FTkQQAxIpCiVUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFU1BPTlNFX1NUQVJUEAQSKAokVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVNQT05TRV9CT0RZEAUSJwojVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVNQT05TRV9FTkQQBhIdChlUVU5ORUxfRlJBTUVfS0lORF9XU19ESUFMEAcSIAocVFVOTkVMX0ZSQU1FX0tJTkRfV1NfRElBTF9PSxAIEiMKH1RVTk5FTF9GUkFNRV9LSU5EX1dTX0RJQUxfRVJST1IQCRIeChpUVU5ORUxfRlJBTUVfS0lORF9XU19GUkFNRRAKEh4KGlRVTk5FTF9GUkFNRV9LSU5EX1dTX0NMT1NFEAsSGwoXVFVOTkVMX0ZSQU1FX0tJTkRfRVJST1IQDBIcChhUVU5ORUxfRlJBTUVfS0lORF9DQU5DRUwQDRIaChZUVU5ORUxfRlJBTUVfS0lORF9QSU5HEA4SGgoWVFVOTkVMX0ZSQU1FX0tJTkRfUE9ORxAPKoEBChNUdW5uZWxXc01lc3NhZ2VUeXBlEiYKIlRVTk5FTF9XU19NRVNTQUdFX1RZUEVfVU5TUEVDSUZJRUQQABIfChtUVU5ORUxfV1NfTUVTU0FHRV9UWVBFX1RFWFQQARIhCh1UVU5ORUxfV1NfTUVTU0FHRV9UWVBFX0JJTkFSWRACQkBaPmdpdGh1Yi5jb20vbGl2ZWFnZW50L2FnZW50LWdhdGV3YXkvaW50ZXJuYWwvcHJvdG8vdjI7Z2F0ZXdheXYyYgZwcm90bzM"); /** * @generated from message liveagent.gateway.v2.GatewayEnvelope @@ -419,6 +419,12 @@ export type GatewayEnvelope = Message<"liveagent.gateway.v2.GatewayEnvelope"> & */ value: HistoryResyncRequest; case: "historyResync"; + } | { + /** + * @generated from field: liveagent.gateway.v2.PermissionRulesRequest permission_rules = 111; + */ + value: PermissionRulesRequest; + case: "permissionRules"; } | { case: undefined; value?: undefined }; }; @@ -932,6 +938,12 @@ export type AgentEnvelope = Message<"liveagent.gateway.v2.AgentEnvelope"> & { */ value: HistorySyncSnapshot; case: "historySyncSnapshot"; + } | { + /** + * @generated from field: liveagent.gateway.v2.PermissionRulesResponse permission_rules_resp = 119; + */ + value: PermissionRulesResponse; + case: "permissionRulesResp"; } | { case: undefined; value?: undefined }; }; @@ -5871,6 +5883,55 @@ export type ProxyDetectResponse = Message<"liveagent.gateway.v2.ProxyDetectRespo export const ProxyDetectResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_proto_v2_gateway, 140); +/** + * 设置 → 权限:查看、添加、删除 K-brain 记住的权限规则(/v1/permissions/rules)。 + * 桌面端或直连 K-brain 的网关只转发设置页需要的字段,会话规则不经这里。 + * + * @generated from message liveagent.gateway.v2.PermissionRulesRequest + */ +export type PermissionRulesRequest = Message<"liveagent.gateway.v2.PermissionRulesRequest"> & { + /** + * "list" | "add" | "delete"。 + * + * @generated from field: string action = 1; + */ + action: string; + + /** + * list: {"workspace"?};add: {"rule","effect":"allow"|"deny","scope":"project"|"user","workspace"?}; + * delete: {"id","scope":"project"|"user","workspace"?}。project 作用域必须带 workspace。 + * + * @generated from field: string input_json = 2; + */ + inputJson: string; +}; + +/** + * Describes the message liveagent.gateway.v2.PermissionRulesRequest. + * Use `create(PermissionRulesRequestSchema)` to create a new message. + */ +export const PermissionRulesRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_proto_v2_gateway, 141); + +/** + * @generated from message liveagent.gateway.v2.PermissionRulesResponse + */ +export type PermissionRulesResponse = Message<"liveagent.gateway.v2.PermissionRulesResponse"> & { + /** + * K-brain 的应答 JSON 原样返回:list 为 {project_root?, rules[]},add 为新规则,delete 为 {ok}。 + * + * @generated from field: string result_json = 1; + */ + resultJson: string; +}; + +/** + * Describes the message liveagent.gateway.v2.PermissionRulesResponse. + * Use `create(PermissionRulesResponseSchema)` to create a new message. + */ +export const PermissionRulesResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_proto_v2_gateway, 142); + /** * @generated from message liveagent.gateway.v2.FsRoot */ @@ -5901,7 +5962,7 @@ export type FsRoot = Message<"liveagent.gateway.v2.FsRoot"> & { * Use `create(FsRootSchema)` to create a new message. */ export const FsRootSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 141); + messageDesc(file_proto_v2_gateway, 143); /** * @generated from message liveagent.gateway.v2.FsRootsRequest @@ -5914,7 +5975,7 @@ export type FsRootsRequest = Message<"liveagent.gateway.v2.FsRootsRequest"> & { * Use `create(FsRootsRequestSchema)` to create a new message. */ export const FsRootsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 142); + messageDesc(file_proto_v2_gateway, 144); /** * @generated from message liveagent.gateway.v2.FsRootsResponse @@ -5931,7 +5992,7 @@ export type FsRootsResponse = Message<"liveagent.gateway.v2.FsRootsResponse"> & * Use `create(FsRootsResponseSchema)` to create a new message. */ export const FsRootsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 143); + messageDesc(file_proto_v2_gateway, 145); /** * @generated from message liveagent.gateway.v2.FsListDirsRequest @@ -5953,7 +6014,7 @@ export type FsListDirsRequest = Message<"liveagent.gateway.v2.FsListDirsRequest" * Use `create(FsListDirsRequestSchema)` to create a new message. */ export const FsListDirsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 144); + messageDesc(file_proto_v2_gateway, 146); /** * @generated from message liveagent.gateway.v2.FsDirEntry @@ -5975,7 +6036,7 @@ export type FsDirEntry = Message<"liveagent.gateway.v2.FsDirEntry"> & { * Use `create(FsDirEntrySchema)` to create a new message. */ export const FsDirEntrySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 145); + messageDesc(file_proto_v2_gateway, 147); /** * @generated from message liveagent.gateway.v2.FsListDirsResponse @@ -6002,7 +6063,7 @@ export type FsListDirsResponse = Message<"liveagent.gateway.v2.FsListDirsRespons * Use `create(FsListDirsResponseSchema)` to create a new message. */ export const FsListDirsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 146); + messageDesc(file_proto_v2_gateway, 148); /** * @generated from message liveagent.gateway.v2.FsCreateProjectFolderRequest @@ -6024,7 +6085,7 @@ export type FsCreateProjectFolderRequest = Message<"liveagent.gateway.v2.FsCreat * Use `create(FsCreateProjectFolderRequestSchema)` to create a new message. */ export const FsCreateProjectFolderRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 147); + messageDesc(file_proto_v2_gateway, 149); /** * @generated from message liveagent.gateway.v2.FsCreateProjectFolderResponse @@ -6041,7 +6102,7 @@ export type FsCreateProjectFolderResponse = Message<"liveagent.gateway.v2.FsCrea * Use `create(FsCreateProjectFolderResponseSchema)` to create a new message. */ export const FsCreateProjectFolderResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 148); + messageDesc(file_proto_v2_gateway, 150); /** * @generated from message liveagent.gateway.v2.FsListRequest @@ -6083,7 +6144,7 @@ export type FsListRequest = Message<"liveagent.gateway.v2.FsListRequest"> & { * Use `create(FsListRequestSchema)` to create a new message. */ export const FsListRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 149); + messageDesc(file_proto_v2_gateway, 151); /** * @generated from message liveagent.gateway.v2.FsListEntry @@ -6110,7 +6171,7 @@ export type FsListEntry = Message<"liveagent.gateway.v2.FsListEntry"> & { * Use `create(FsListEntrySchema)` to create a new message. */ export const FsListEntrySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 150); + messageDesc(file_proto_v2_gateway, 152); /** * @generated from message liveagent.gateway.v2.FsListResponse @@ -6162,7 +6223,7 @@ export type FsListResponse = Message<"liveagent.gateway.v2.FsListResponse"> & { * Use `create(FsListResponseSchema)` to create a new message. */ export const FsListResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 151); + messageDesc(file_proto_v2_gateway, 153); /** * @generated from message liveagent.gateway.v2.FsReadEditableTextRequest @@ -6184,7 +6245,7 @@ export type FsReadEditableTextRequest = Message<"liveagent.gateway.v2.FsReadEdit * Use `create(FsReadEditableTextRequestSchema)` to create a new message. */ export const FsReadEditableTextRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 152); + messageDesc(file_proto_v2_gateway, 154); /** * @generated from message liveagent.gateway.v2.FsReadEditableTextResponse @@ -6226,7 +6287,7 @@ export type FsReadEditableTextResponse = Message<"liveagent.gateway.v2.FsReadEdi * Use `create(FsReadEditableTextResponseSchema)` to create a new message. */ export const FsReadEditableTextResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 153); + messageDesc(file_proto_v2_gateway, 155); /** * @generated from message liveagent.gateway.v2.FsReadWorkspaceImageRequest @@ -6248,7 +6309,7 @@ export type FsReadWorkspaceImageRequest = Message<"liveagent.gateway.v2.FsReadWo * Use `create(FsReadWorkspaceImageRequestSchema)` to create a new message. */ export const FsReadWorkspaceImageRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 154); + messageDesc(file_proto_v2_gateway, 156); /** * @generated from message liveagent.gateway.v2.FsReadWorkspaceImageResponse @@ -6290,7 +6351,7 @@ export type FsReadWorkspaceImageResponse = Message<"liveagent.gateway.v2.FsReadW * Use `create(FsReadWorkspaceImageResponseSchema)` to create a new message. */ export const FsReadWorkspaceImageResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 155); + messageDesc(file_proto_v2_gateway, 157); /** * @generated from message liveagent.gateway.v2.ChatFileOpenRequest @@ -6342,7 +6403,7 @@ export type ChatFileOpenRequest = Message<"liveagent.gateway.v2.ChatFileOpenRequ * Use `create(ChatFileOpenRequestSchema)` to create a new message. */ export const ChatFileOpenRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 156); + messageDesc(file_proto_v2_gateway, 158); /** * @generated from message liveagent.gateway.v2.ChatFileOpenResponse @@ -6394,7 +6455,7 @@ export type ChatFileOpenResponse = Message<"liveagent.gateway.v2.ChatFileOpenRes * Use `create(ChatFileOpenResponseSchema)` to create a new message. */ export const ChatFileOpenResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 157); + messageDesc(file_proto_v2_gateway, 159); /** * @generated from message liveagent.gateway.v2.FsWriteTextRequest @@ -6446,7 +6507,7 @@ export type FsWriteTextRequest = Message<"liveagent.gateway.v2.FsWriteTextReques * Use `create(FsWriteTextRequestSchema)` to create a new message. */ export const FsWriteTextRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 158); + messageDesc(file_proto_v2_gateway, 160); /** * @generated from message liveagent.gateway.v2.FsWriteTextResponse @@ -6493,7 +6554,7 @@ export type FsWriteTextResponse = Message<"liveagent.gateway.v2.FsWriteTextRespo * Use `create(FsWriteTextResponseSchema)` to create a new message. */ export const FsWriteTextResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 159); + messageDesc(file_proto_v2_gateway, 161); /** * @generated from message liveagent.gateway.v2.FsCreateDirRequest @@ -6515,7 +6576,7 @@ export type FsCreateDirRequest = Message<"liveagent.gateway.v2.FsCreateDirReques * Use `create(FsCreateDirRequestSchema)` to create a new message. */ export const FsCreateDirRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 160); + messageDesc(file_proto_v2_gateway, 162); /** * @generated from message liveagent.gateway.v2.FsCreateDirResponse @@ -6537,7 +6598,7 @@ export type FsCreateDirResponse = Message<"liveagent.gateway.v2.FsCreateDirRespo * Use `create(FsCreateDirResponseSchema)` to create a new message. */ export const FsCreateDirResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 161); + messageDesc(file_proto_v2_gateway, 163); /** * @generated from message liveagent.gateway.v2.FsRenameRequest @@ -6564,7 +6625,7 @@ export type FsRenameRequest = Message<"liveagent.gateway.v2.FsRenameRequest"> & * Use `create(FsRenameRequestSchema)` to create a new message. */ export const FsRenameRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 162); + messageDesc(file_proto_v2_gateway, 164); /** * @generated from message liveagent.gateway.v2.FsRenameResponse @@ -6591,7 +6652,7 @@ export type FsRenameResponse = Message<"liveagent.gateway.v2.FsRenameResponse"> * Use `create(FsRenameResponseSchema)` to create a new message. */ export const FsRenameResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 163); + messageDesc(file_proto_v2_gateway, 165); /** * @generated from message liveagent.gateway.v2.FsDeleteRequest @@ -6613,7 +6674,7 @@ export type FsDeleteRequest = Message<"liveagent.gateway.v2.FsDeleteRequest"> & * Use `create(FsDeleteRequestSchema)` to create a new message. */ export const FsDeleteRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 164); + messageDesc(file_proto_v2_gateway, 166); /** * @generated from message liveagent.gateway.v2.FsDeleteResponse @@ -6635,7 +6696,7 @@ export type FsDeleteResponse = Message<"liveagent.gateway.v2.FsDeleteResponse"> * Use `create(FsDeleteResponseSchema)` to create a new message. */ export const FsDeleteResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 165); + messageDesc(file_proto_v2_gateway, 167); /** * @generated from message liveagent.gateway.v2.PingRequest @@ -6652,7 +6713,7 @@ export type PingRequest = Message<"liveagent.gateway.v2.PingRequest"> & { * Use `create(PingRequestSchema)` to create a new message. */ export const PingRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 166); + messageDesc(file_proto_v2_gateway, 168); /** * @generated from message liveagent.gateway.v2.PongResponse @@ -6669,7 +6730,7 @@ export type PongResponse = Message<"liveagent.gateway.v2.PongResponse"> & { * Use `create(PongResponseSchema)` to create a new message. */ export const PongResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 167); + messageDesc(file_proto_v2_gateway, 169); /** * @generated from message liveagent.gateway.v2.ErrorResponse @@ -6691,7 +6752,7 @@ export type ErrorResponse = Message<"liveagent.gateway.v2.ErrorResponse"> & { * Use `create(ErrorResponseSchema)` to create a new message. */ export const ErrorResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 168); + messageDesc(file_proto_v2_gateway, 170); /** * @generated from message liveagent.gateway.v2.ProviderCustomHeader @@ -6713,7 +6774,7 @@ export type ProviderCustomHeader = Message<"liveagent.gateway.v2.ProviderCustomH * Use `create(ProviderCustomHeaderSchema)` to create a new message. */ export const ProviderCustomHeaderSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 169); + messageDesc(file_proto_v2_gateway, 171); /** * 包一层 message 只为拿到字段存在性:repeated 无法区分「草稿没带」与「草稿把头 @@ -6734,7 +6795,7 @@ export type ProviderCustomHeaders = Message<"liveagent.gateway.v2.ProviderCustom * Use `create(ProviderCustomHeadersSchema)` to create a new message. */ export const ProviderCustomHeadersSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 170); + messageDesc(file_proto_v2_gateway, 172); /** * @generated from message liveagent.gateway.v2.ProviderModelsRequest @@ -6802,7 +6863,7 @@ export type ProviderModelsRequest = Message<"liveagent.gateway.v2.ProviderModels * Use `create(ProviderModelsRequestSchema)` to create a new message. */ export const ProviderModelsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 171); + messageDesc(file_proto_v2_gateway, 173); /** * @generated from message liveagent.gateway.v2.ProviderModelsResponse @@ -6819,7 +6880,7 @@ export type ProviderModelsResponse = Message<"liveagent.gateway.v2.ProviderModel * Use `create(ProviderModelsResponseSchema)` to create a new message. */ export const ProviderModelsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 172); + messageDesc(file_proto_v2_gateway, 174); /** * @generated from message liveagent.gateway.v2.ProviderUsageRequest @@ -6849,7 +6910,7 @@ export type ProviderUsageRequest = Message<"liveagent.gateway.v2.ProviderUsageRe * Use `create(ProviderUsageRequestSchema)` to create a new message. */ export const ProviderUsageRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 173); + messageDesc(file_proto_v2_gateway, 175); /** * @generated from message liveagent.gateway.v2.ProviderUsageResponse @@ -6866,7 +6927,7 @@ export type ProviderUsageResponse = Message<"liveagent.gateway.v2.ProviderUsageR * Use `create(ProviderUsageResponseSchema)` to create a new message. */ export const ProviderUsageResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 174); + messageDesc(file_proto_v2_gateway, 176); /** * ChatIngressBatch carries contiguous logical records for one run. Each record @@ -6901,7 +6962,7 @@ export type ChatIngressBatch = Message<"liveagent.gateway.v2.ChatIngressBatch"> * Use `create(ChatIngressBatchSchema)` to create a new message. */ export const ChatIngressBatchSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 175); + messageDesc(file_proto_v2_gateway, 177); /** * @generated from message liveagent.gateway.v2.ChatIngressRecord @@ -6942,7 +7003,7 @@ export type ChatIngressRecord = Message<"liveagent.gateway.v2.ChatIngressRecord" * Use `create(ChatIngressRecordSchema)` to create a new message. */ export const ChatIngressRecordSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 176); + messageDesc(file_proto_v2_gateway, 178); /** * @generated from message liveagent.gateway.v2.ChatIngressDelta @@ -6964,7 +7025,7 @@ export type ChatIngressDelta = Message<"liveagent.gateway.v2.ChatIngressDelta"> * Use `create(ChatIngressDeltaSchema)` to create a new message. */ export const ChatIngressDeltaSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 177); + messageDesc(file_proto_v2_gateway, 179); /** * @generated from message liveagent.gateway.v2.ChatIngressHeartbeat @@ -6983,7 +7044,7 @@ export type ChatIngressHeartbeat = Message<"liveagent.gateway.v2.ChatIngressHear * Use `create(ChatIngressHeartbeatSchema)` to create a new message. */ export const ChatIngressHeartbeatSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 178); + messageDesc(file_proto_v2_gateway, 180); /** * @generated from message liveagent.gateway.v2.ChatIngressCheckpoint @@ -7030,7 +7091,7 @@ export type ChatIngressCheckpoint = Message<"liveagent.gateway.v2.ChatIngressChe * Use `create(ChatIngressCheckpointSchema)` to create a new message. */ export const ChatIngressCheckpointSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 179); + messageDesc(file_proto_v2_gateway, 181); /** * @generated from message liveagent.gateway.v2.ChatIngressTerminal @@ -7092,7 +7153,7 @@ export type ChatIngressTerminal = Message<"liveagent.gateway.v2.ChatIngressTermi * Use `create(ChatIngressTerminalSchema)` to create a new message. */ export const ChatIngressTerminalSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 180); + messageDesc(file_proto_v2_gateway, 182); /** * ChatIngressResume declares the replay window retained by the desktop after a @@ -7112,7 +7173,7 @@ export type ChatIngressResume = Message<"liveagent.gateway.v2.ChatIngressResume" * Use `create(ChatIngressResumeSchema)` to create a new message. */ export const ChatIngressResumeSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 181); + messageDesc(file_proto_v2_gateway, 183); /** * @generated from message liveagent.gateway.v2.ChatIngressRunResume @@ -7164,7 +7225,7 @@ export type ChatIngressRunResume = Message<"liveagent.gateway.v2.ChatIngressRunR * Use `create(ChatIngressRunResumeSchema)` to create a new message. */ export const ChatIngressRunResumeSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 182); + messageDesc(file_proto_v2_gateway, 184); /** * ChatIngressFragment transports one encoded ChatIngressRecord that exceeds a @@ -7220,7 +7281,7 @@ export type ChatIngressFragment = Message<"liveagent.gateway.v2.ChatIngressFragm * Use `create(ChatIngressFragmentSchema)` to create a new message. */ export const ChatIngressFragmentSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 183); + messageDesc(file_proto_v2_gateway, 185); /** * @generated from message liveagent.gateway.v2.ChatIngressAck @@ -7272,7 +7333,7 @@ export type ChatIngressAck = Message<"liveagent.gateway.v2.ChatIngressAck"> & { * Use `create(ChatIngressAckSchema)` to create a new message. */ export const ChatIngressAckSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 184); + messageDesc(file_proto_v2_gateway, 186); /** * @generated from enum liveagent.gateway.v2.ChatIngressAck.Action @@ -7308,7 +7369,7 @@ export enum ChatIngressAck_Action { * Describes the enum liveagent.gateway.v2.ChatIngressAck.Action. */ export const ChatIngressAck_ActionSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_proto_v2_gateway, 184, 0); + enumDesc(file_proto_v2_gateway, 186, 0); /** * @generated from message liveagent.gateway.v2.HistorySetCwdRequest @@ -7330,7 +7391,7 @@ export type HistorySetCwdRequest = Message<"liveagent.gateway.v2.HistorySetCwdRe * Use `create(HistorySetCwdRequestSchema)` to create a new message. */ export const HistorySetCwdRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 185); + messageDesc(file_proto_v2_gateway, 187); /** * @generated from message liveagent.gateway.v2.HistorySetCwdResponse @@ -7347,7 +7408,7 @@ export type HistorySetCwdResponse = Message<"liveagent.gateway.v2.HistorySetCwdR * Use `create(HistorySetCwdResponseSchema)` to create a new message. */ export const HistorySetCwdResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 186); + messageDesc(file_proto_v2_gateway, 188); /** * @generated from message liveagent.gateway.v2.WorkspaceRootGrantDraft @@ -7379,7 +7440,7 @@ export type WorkspaceRootGrantDraft = Message<"liveagent.gateway.v2.WorkspaceRoo * Use `create(WorkspaceRootGrantDraftSchema)` to create a new message. */ export const WorkspaceRootGrantDraftSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 187); + messageDesc(file_proto_v2_gateway, 189); /** * @generated from message liveagent.gateway.v2.WorkspaceRootGrant @@ -7441,7 +7502,7 @@ export type WorkspaceRootGrant = Message<"liveagent.gateway.v2.WorkspaceRootGran * Use `create(WorkspaceRootGrantSchema)` to create a new message. */ export const WorkspaceRootGrantSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 188); + messageDesc(file_proto_v2_gateway, 190); /** * @generated from message liveagent.gateway.v2.WorkspaceRootGrantsRequest @@ -7473,7 +7534,7 @@ export type WorkspaceRootGrantsRequest = Message<"liveagent.gateway.v2.Workspace * Use `create(WorkspaceRootGrantsRequestSchema)` to create a new message. */ export const WorkspaceRootGrantsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 189); + messageDesc(file_proto_v2_gateway, 191); /** * @generated from message liveagent.gateway.v2.WorkspaceRootGrantsResponse @@ -7490,7 +7551,7 @@ export type WorkspaceRootGrantsResponse = Message<"liveagent.gateway.v2.Workspac * Use `create(WorkspaceRootGrantsResponseSchema)` to create a new message. */ export const WorkspaceRootGrantsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 190); + messageDesc(file_proto_v2_gateway, 192); /** * @generated from message liveagent.gateway.v2.CheckpointExpectedEntry @@ -7512,7 +7573,7 @@ export type CheckpointExpectedEntry = Message<"liveagent.gateway.v2.CheckpointEx * Use `create(CheckpointExpectedEntrySchema)` to create a new message. */ export const CheckpointExpectedEntrySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 191); + messageDesc(file_proto_v2_gateway, 193); /** * @generated from message liveagent.gateway.v2.CheckpointRequest @@ -7551,7 +7612,7 @@ export type CheckpointRequest = Message<"liveagent.gateway.v2.CheckpointRequest" * Use `create(CheckpointRequestSchema)` to create a new message. */ export const CheckpointRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 192); + messageDesc(file_proto_v2_gateway, 194); /** * @generated from message liveagent.gateway.v2.CheckpointResponse @@ -7573,7 +7634,7 @@ export type CheckpointResponse = Message<"liveagent.gateway.v2.CheckpointRespons * Use `create(CheckpointResponseSchema)` to create a new message. */ export const CheckpointResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 193); + messageDesc(file_proto_v2_gateway, 195); /** * @generated from message liveagent.gateway.v2.ChatConversationReference @@ -7605,7 +7666,7 @@ export type ChatConversationReference = Message<"liveagent.gateway.v2.ChatConver * Use `create(ChatConversationReferenceSchema)` to create a new message. */ export const ChatConversationReferenceSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 194); + messageDesc(file_proto_v2_gateway, 196); /** * Planning payloads use the validated Rust/TypeScript domain contract. @@ -7629,7 +7690,7 @@ export type PlanningRequest = Message<"liveagent.gateway.v2.PlanningRequest"> & * Use `create(PlanningRequestSchema)` to create a new message. */ export const PlanningRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 195); + messageDesc(file_proto_v2_gateway, 197); /** * @generated from message liveagent.gateway.v2.PlanningResponse @@ -7646,7 +7707,7 @@ export type PlanningResponse = Message<"liveagent.gateway.v2.PlanningResponse"> * Use `create(PlanningResponseSchema)` to create a new message. */ export const PlanningResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 196); + messageDesc(file_proto_v2_gateway, 198); /** * @generated from message liveagent.gateway.v2.PlanningChanged @@ -7663,7 +7724,7 @@ export type PlanningChanged = Message<"liveagent.gateway.v2.PlanningChanged"> & * Use `create(PlanningChangedSchema)` to create a new message. */ export const PlanningChangedSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_proto_v2_gateway, 197); + messageDesc(file_proto_v2_gateway, 199); /** * @generated from enum liveagent.gateway.v2.ImportDirectoryOperation diff --git a/crates/agent-gateway/web/src/pages/settings/types.ts b/crates/agent-gateway/web/src/pages/settings/types.ts index 4fb988dda..0b774bcb8 100644 --- a/crates/agent-gateway/web/src/pages/settings/types.ts +++ b/crates/agent-gateway/web/src/pages/settings/types.ts @@ -10,6 +10,7 @@ export type SectionId = | "skills" | "mcp" | "systemTools" + | "permissions" | "stt" | "providers" | "agents" diff --git a/crates/agent-gateway/web/test/gateway-v2-adapters.test.mjs b/crates/agent-gateway/web/test/gateway-v2-adapters.test.mjs index c8a18f174..a9c4607bb 100644 --- a/crates/agent-gateway/web/test/gateway-v2-adapters.test.mjs +++ b/crates/agent-gateway/web/test/gateway-v2-adapters.test.mjs @@ -832,3 +832,51 @@ test("Planning v2 preserves CAS and taxonomy payloads and identifies the invalid const changed = decodeServerFrame(roundtrip(serverFrame({agent_id:"agent-1",planning_changed:{seq:"9"}})),{agentOnline:true}); assert.equal(changed.type,"planning.changed");assert.equal(changed.agentId,"agent-1");assert.equal(changed.payload.seq,9); }); + +test("permission rules requests carry the action and only the rule fields", () => { + const list = decodeClientFrame( + encodeRequestFrame("rules-1", "permission.rules.list", { workspace: "D:\\w", session_id: "s1" }, "agent-1"), + ); + assert.equal(list.payload.value.payload.case, "permissionRules"); + assert.equal(list.payload.value.payload.value.action, "list"); + assert.deepEqual(JSON.parse(list.payload.value.payload.value.inputJson), { workspace: "D:\\w" }); + + const add = decodeClientFrame( + encodeRequestFrame( + "rules-2", + "permission.rules.add", + { rule: " Bash(rm:*) ", effect: "deny", scope: "project", workspace: "/w", extra: 1 }, + "agent-1", + ), + ); + assert.equal(add.payload.value.payload.value.action, "add"); + assert.deepEqual(JSON.parse(add.payload.value.payload.value.inputJson), { + rule: "Bash(rm:*)", + effect: "deny", + scope: "project", + workspace: "/w", + }); + + const remove = decodeClientFrame( + encodeRequestFrame("rules-3", "permission.rules.delete", { id: "r_1", scope: "user" }, "agent-1"), + ); + assert.equal(remove.payload.value.payload.value.action, "delete"); + assert.deepEqual(JSON.parse(remove.payload.value.payload.value.inputJson), { id: "r_1", scope: "user" }); + + const rules = { project_root: "D:\\w", rules: [{ id: "r_1", rule: "Bash(ls:*)", effect: "allow", scope: "project" }] }; + const decoded = decodeServerFrame( + roundtrip( + serverFrame({ + request_id: "rules-1", + agent_id: "agent-1", + agent_response: { + request_id: "rules-1", + permission_rules_resp: { result_json: JSON.stringify(rules) }, + }, + }), + ), + { agentOnline: true }, + ); + assert.equal(decoded.kind, "response"); + assert.deepEqual(decoded.payload, rules); +}); diff --git a/crates/agent-gateway/web/test/permission-rules-settings.test.mjs b/crates/agent-gateway/web/test/permission-rules-settings.test.mjs new file mode 100644 index 000000000..38c658994 --- /dev/null +++ b/crates/agent-gateway/web/test/permission-rules-settings.test.mjs @@ -0,0 +1,52 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { createWebModuleLoader } from "../../test/helpers/load-web-module.mjs"; + +// LA-3b: the WebUI settings adapter reaches K-brain's permission rules through the Gateway +// (permission.rules.*) and hands the shared page the same shapes as the desktop adapter. +function load(client) { + const loader = createWebModuleLoader({ + rootDir: fileURLToPath(new URL("../", import.meta.url)), + mocks: { + "../lib/gatewaySocket": { getGatewayWebSocketClient: () => client }, + "../lib/storage": { loadToken: () => "fixture" }, + "../pages/settings/DevicesSection": { DevicesSection: () => null }, + "@liveagent/ui/components/IconSet": { MonitorSmartphone: () => null }, + "@liveagent/ui/lib/resourceHost": { configureResourceHostCapabilities() {}, nativeResourceHostCapabilities: {} }, + }, + }); + return loader.loadModule("src/agent-ui-adapters/settingsExtension.tsx").settingsHostAdapter.kbrain; +} + +test("the WebUI adapter lists, adds and deletes rules through the Gateway", async () => { + const calls = []; + const client = { + permissionRules: async (action, input) => { + calls.push([action, input]); + if (action === "list") return { project_root: "D:\\w", rules: [{ id: "r_1", rule: "Bash(ls:*)", effect: "allow", scope: "project", created_at: "2026-11-01T00:00:00Z", source_session: "s1" }] }; + if (action === "add") return { id: "r_2", rule: input.rule, effect: input.effect, scope: input.scope }; + return { ok: true }; + }, + }; + const kbrain = load(client); + assert.equal(typeof kbrain.listPermissionRules, "function", "the page is registered on the WebUI"); + assert.deepEqual(await kbrain.listPermissionRules({ workspace: "D:\\w" }), { + projectRoot: "D:\\w", + rules: [{ id: "r_1", rule: "Bash(ls:*)", effect: "allow", scope: "project", createdAt: "2026-11-01T00:00:00Z", sourceSession: "s1" }], + }); + assert.deepEqual(calls.at(-1), ["list", { workspace: "D:\\w" }]); + await kbrain.listPermissionRules({}); + assert.deepEqual(calls.at(-1), ["list", {}]); + assert.deepEqual(await kbrain.addPermissionRule({ rule: "Bash(rm:*)", effect: "deny", scope: "user" }), { id: "r_2", rule: "Bash(rm:*)", effect: "deny", scope: "user" }); + assert.deepEqual(calls.at(-1), ["add", { rule: "Bash(rm:*)", effect: "deny", scope: "user" }]); + await kbrain.deletePermissionRule({ id: "r_1", scope: "project", workspace: "D:\\w" }); + assert.deepEqual(calls.at(-1), ["delete", { id: "r_1", scope: "project", workspace: "D:\\w" }]); +}); + +test("a refusal from K-brain or an old desktop reaches the page as its message", async () => { + const kbrain = load({ + permissionRules: async () => { throw new Error("desktop app does not support permission rules; update LiveAgent on the desktop"); }, + }); + await assert.rejects(kbrain.listPermissionRules({}), /update LiveAgent on the desktop/); +}); diff --git a/crates/agent-gateway/web/test/review-la3b-permission-rules-r2.test.mjs b/crates/agent-gateway/web/test/review-la3b-permission-rules-r2.test.mjs new file mode 100644 index 000000000..25d0b8165 --- /dev/null +++ b/crates/agent-gateway/web/test/review-la3b-permission-rules-r2.test.mjs @@ -0,0 +1,148 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { createWebDomTestEnv } from "../../test/helpers/dom-test-env.mjs"; + +// LA-3b review, round 2: the failed-mutation re-read in PermissionRulesSection.run(), +// through the real WebUI adapter, rpc client and v2 encoder; the socket is an in-memory +// K-brain rules store. Assertions compare strings/booleans, never DOM nodes. + +const webRoot = fileURLToPath(new URL("../", import.meta.url)); +const uiPath = (relative) => fileURLToPath(new URL(`../../../agent-ui/src/${relative}`, import.meta.url)); +const DISCONNECTED = "Gateway WebSocket disconnected (code=1006)"; + +async function page(t, answer) { + let client; + const env = await createWebDomTestEnv({ + rootDir: webRoot, + mocks: { + "@liveagent/ui/components/IconSet": new Proxy({}, { get: () => () => null }), + "@liveagent/ui/components/ui/button": { + Button: ({ children, size, variant, ...props }) => env.React.createElement("button", props, children), + }, + "@liveagent/ui/components/settings/SettingsToggleGroup": { + SettingsToggleGroup: ({ children }) => env.React.createElement("div", null, children), + SettingsToggleGroupItem: ({ children, value, disabled }) => + env.React.createElement("button", { disabled, "data-toggle": value }, children), + }, + [uiPath("components/ui/select.tsx")]: { + Select: ({ children }) => env.React.createElement("select", null, children), + SelectItem: ({ value, children }) => env.React.createElement("option", { value }, children), + SelectValue: () => null, + }, + [uiPath("components/settings/SettingsSelect.tsx")]: { + SettingsSelectTrigger: () => null, + SettingsSelectContent: ({ children }) => env.React.createElement(env.React.Fragment, null, children), + }, + [uiPath("components/ui/confirm-action-popover.tsx")]: { + ConfirmDeletePopover: ({ children, onConfirm }) => children(onConfirm), + }, + "../lib/gatewaySocket": { getGatewayWebSocketClient: () => client }, + "../lib/storage": { loadToken: () => "fixture" }, + "../pages/settings/DevicesSection": { DevicesSection: () => null }, + "@liveagent/ui/lib/resourceHost": { configureResourceHostCapabilities() {}, kbrainResourceHostCapabilities: {} }, + }, + }); + const rpc = env.loadModule("src/lib/gatewaySocketRpc.ts"); + const adapters = env.loadModule("src/lib/gatewaySocketV2/adapters.ts"); + const pb = env.loadModule("@bufbuild/protobuf"); + const v2 = env.loadModule("src/lib/proto/gen/proto/v2/gateway_ws_pb.ts"); + const sent = []; + client = Object.create(rpc.GatewayWebSocketRpcClient.prototype); + client.disposed = false; + client.recoverTransport = async () => {}; + client.request = async (type, payload) => { + const frame = pb.fromBinary(v2.WebClientFrameSchema, adapters.encodeRequestFrame("r", type, payload, "agent-1")); + const input = JSON.parse(frame.payload.value.payload.value.inputJson); + sent.push(type.slice("permission.rules.".length)); + return answer(type.slice("permission.rules.".length), input); + }; + const kbrain = env.loadModule("src/agent-ui-adapters/settingsExtension.tsx").settingsHostAdapter.kbrain; + const { PermissionRulesSection } = env.loadModule("@liveagent/ui/pages/settings/PermissionRulesSection.tsx"); + const host = document.body.appendChild(document.createElement("div")); + const root = env.createRoot(host); + t.after(async () => { + await env.act(async () => root.unmount()); + host.remove(); + env.cleanup(); + }); + const settle = () => env.act(async () => { for (let i = 0; i < 10; i++) await Promise.resolve(); }); + const settings = { system: { workspaceProjects: [], hiddenWorkspaceProjectPaths: [], archivedWorkspaceProjectPaths: [] } }; + await env.act(async () => root.render(env.React.createElement(PermissionRulesSection, { kbrain, settings }))); + await settle(); + return { + sent, + rows: () => [...host.querySelectorAll('[data-permission-rules="user"] [data-permission-rule]')].map((row) => row.getAttribute("data-permission-rule")), + alerts: () => [...host.querySelectorAll('[role="alert"]')].map((node) => node.textContent), + draft: () => host.querySelector("input[data-permission-rule-input]").value, + busy: () => host.querySelector('[data-testid="permission-rules-settings"]')?.getAttribute("aria-busy") ?? "", + async deleteFirst() { + await env.act(async () => host.querySelector('[data-permission-rules="user"] button[data-delete-rule]').click()); + await settle(); + }, + async add(rule) { + await env.act(async () => { + const input = host.querySelector("input[data-permission-rule-input]"); + Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set.call(input, rule); + input.dispatchEvent(new window.Event("input", { bubbles: true })); + }); + const button = [...host.querySelectorAll("button")].find((b) => /^(添加|Add)$/.test(b.textContent.trim())); + await env.act(async () => button.click()); + await settle(); + }, + }; +} + +test("review LA-3b r2: a lost delete answer re-reads the list once and keeps the disconnect message", async (t) => { + const store = [ + { id: "u1", rule: "Bash(npm test:*)", effect: "allow", scope: "user" }, + { id: "u2", rule: "Bash(rm:*)", effect: "deny", scope: "user" }, + ]; + const view = await page(t, (action, input) => { + if (action === "list") return { rules: store.map((rule) => ({ ...rule })) }; + if (action === "delete") { + store.splice(store.findIndex((rule) => rule.id === input.id), 1); + throw new Error(DISCONNECTED); + } + throw new Error(`unexpected ${action}`); + }); + assert.deepEqual(view.rows(), ["Bash(npm test:*)", "Bash(rm:*)"]); + await view.deleteFirst(); + assert.deepEqual( + { rows: view.rows(), alerts: view.alerts(), sent: view.sent, busy: view.busy() }, + { rows: ["Bash(rm:*)"], alerts: [DISCONNECTED], sent: ["list", "delete", "list"], busy: "false" }, + ); +}); + +test("review LA-3b r2: a refused add keeps the draft and the reason, and the other rules stay listed", async (t) => { + const store = [{ id: "u1", rule: "Bash(npm test:*)", effect: "allow", scope: "user" }]; + const reason = 'permission rule "Bash(": missing closing parenthesis (HTTP 400 Bad Request)'; + const view = await page(t, (action) => { + if (action === "list") return { rules: store.map((rule) => ({ ...rule })) }; + if (action === "add") throw new Error(reason); + throw new Error(`unexpected ${action}`); + }); + await view.add("Bash("); + assert.deepEqual( + { rows: view.rows(), alerts: view.alerts(), draft: view.draft(), sent: view.sent }, + { rows: ["Bash(npm test:*)"], alerts: [reason], draft: "Bash(", sent: ["list", "add", "list"] }, + ); +}); + +// The desktop goes away mid-delete: the change fails and so does the re-read. Both +// reasons are on screen; the rules from before are not shown as if still current. +test("review LA-3b r2: when the re-read after a failed change also fails, both reasons are shown", async (t) => { + let online = true; + const view = await page(t, (action) => { + if (!online) throw new Error("agent offline"); + if (action === "list") return { rules: [{ id: "u1", rule: "Bash(npm test:*)", effect: "allow", scope: "user" }] }; + online = false; + throw new Error(DISCONNECTED); + }); + await view.deleteFirst(); + const alerts = view.alerts(); + assert.deepEqual( + { loadError: alerts.includes("agent offline"), actionError: alerts.includes(DISCONNECTED), rows: view.rows(), busy: view.busy() }, + { loadError: true, actionError: true, rows: [], busy: "false" }, + ); +}); diff --git a/crates/agent-gateway/web/test/review-la3b-permission-rules.test.mjs b/crates/agent-gateway/web/test/review-la3b-permission-rules.test.mjs new file mode 100644 index 000000000..0cdb488d5 --- /dev/null +++ b/crates/agent-gateway/web/test/review-la3b-permission-rules.test.mjs @@ -0,0 +1,183 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { createWebDomTestEnv } from "../../test/helpers/dom-test-env.mjs"; +import { createWebModuleLoader } from "../../test/helpers/load-web-module.mjs"; + +// LA-3b independent review. Real modules: GatewayWebSocketRpcClient.permissionRules and the +// transport's requestWithRecovery, the v2 frame encoder, the WebUI settings adapter and the +// shared PermissionRulesSection. Only the socket is replaced: `request` is answered by an +// in-memory K-brain rules store. Assertions compare strings/booleans, never DOM nodes. + +const webRoot = fileURLToPath(new URL("../", import.meta.url)); +const uiPath = (relative) => fileURLToPath(new URL(`../../../agent-ui/src/${relative}`, import.meta.url)); +const DISCONNECTED = "Gateway WebSocket disconnected (code=1006)"; + +// Encode with the real v2 adapter, decode the bytes with the generated schema. +function encodedPermissionRules(loadModule, type, payload) { + const adapters = loadModule("src/lib/gatewaySocketV2/adapters.ts"); + const pb = loadModule("@bufbuild/protobuf"); + const v2 = loadModule("src/lib/proto/gen/proto/v2/gateway_ws_pb.ts"); + const frame = pb.fromBinary(v2.WebClientFrameSchema, adapters.encodeRequestFrame("r", type, payload, "agent-1")); + const request = frame.payload.value.payload; + assert.equal(request.case, "permissionRules"); + return [request.value.action, JSON.parse(request.value.inputJson)]; +} + +function rpcModule() { + return createWebModuleLoader({ rootDir: webRoot }).loadModule("src/lib/gatewaySocketRpc.ts"); +} + +// A client on the real prototype; `request` is the socket. The transport's real +// requestWithRecovery decides whether a failed request is sent again. +function fixtureClient(rpc, answer) { + const client = Object.create(rpc.GatewayWebSocketRpcClient.prototype); + client.disposed = false; + client.sent = []; + client.recoverTransport = async () => {}; + client.request = async (type, payload) => { + client.sent.push([type, payload]); + return answer(type, payload); + }; + return client; +} + +test("review LA-3b: only list is sent again after a dropped socket; add and delete are sent once", async () => { + const rpc = rpcModule(); + let drop = true; + const client = fixtureClient(rpc, (type) => { + if (drop) { + drop = false; + throw new Error(DISCONNECTED); + } + return type === "permission.rules.list" ? { rules: [] } : { ok: true }; + }); + assert.deepEqual(await client.permissionRules("list", {}), { rules: [] }); + assert.deepEqual(client.sent.map(([type]) => type), ["permission.rules.list", "permission.rules.list"]); + + for (const action of ["add", "delete"]) { + client.sent.length = 0; + drop = true; + await assert.rejects(client.permissionRules(action, { id: "r_1", scope: "user" }), /disconnected/); + assert.deepEqual(client.sent.map(([type]) => type), [`permission.rules.${action}`], `${action} is not replayed`); + } +}); + +test("review LA-3b: the WebUI adapter's fields survive the real frame encoder unchanged", async () => { + const rpc = rpcModule(); + const client = fixtureClient(rpc, (type) => (type === "permission.rules.list" ? { rules: [] } : { id: "r_2", rule: "x", effect: "deny", scope: "project" })); + const loader = createWebModuleLoader({ + rootDir: webRoot, + mocks: { + "../lib/gatewaySocket": { getGatewayWebSocketClient: () => client }, + "../lib/storage": { loadToken: () => "fixture" }, + "../pages/settings/DevicesSection": { DevicesSection: () => null }, + "@liveagent/ui/components/IconSet": { MonitorSmartphone: () => null }, + "@liveagent/ui/lib/resourceHost": { configureResourceHostCapabilities() {}, kbrainResourceHostCapabilities: {} }, + }, + }); + const kbrain = loader.loadModule("src/agent-ui-adapters/settingsExtension.tsx").settingsHostAdapter.kbrain; + const workspace = "D:\\项目\\a b+c%20#?&"; + await kbrain.addPermissionRule({ rule: "Bash(python -c 'print(1)':*)", effect: "deny", scope: "project", workspace }); + await kbrain.deletePermissionRule({ id: "r_2", scope: "project", workspace }); + await kbrain.listPermissionRules({ workspace }); + const decoded = client.sent.map(([type, payload]) => encodedPermissionRules(loader.loadModule, type, payload)); + assert.deepEqual(decoded, [ + ["add", { workspace, rule: "Bash(python -c 'print(1)':*)", effect: "deny", scope: "project" }], + ["delete", { workspace, scope: "project", id: "r_2" }], + ["list", { workspace }], + ]); +}); + +// The socket drops after the desktop has carried out the delete: K-brain no longer has +// the rule but the page never hears back. A second delete is refused with 404. +test("review LA-3b: after a delete whose answer was lost, the page still lists the deleted rule", async () => { + const env = await createWebDomTestEnv({ + rootDir: webRoot, + mocks: { + "@liveagent/ui/components/IconSet": new Proxy({}, { get: () => () => null }), + "@liveagent/ui/components/ui/button": { + Button: ({ children, size, variant, ...props }) => env.React.createElement("button", props, children), + }, + "@liveagent/ui/components/settings/SettingsToggleGroup": { + SettingsToggleGroup: ({ children }) => env.React.createElement("div", null, children), + SettingsToggleGroupItem: ({ children, value, disabled }) => + env.React.createElement("button", { disabled, "data-toggle": value }, children), + }, + [uiPath("components/ui/select.tsx")]: { + Select: ({ children }) => env.React.createElement("select", null, children), + SelectItem: ({ value, children }) => env.React.createElement("option", { value }, children), + SelectValue: () => null, + }, + [uiPath("components/settings/SettingsSelect.tsx")]: { + SettingsSelectTrigger: () => null, + SettingsSelectContent: ({ children }) => env.React.createElement(env.React.Fragment, null, children), + }, + [uiPath("components/ui/confirm-action-popover.tsx")]: { + ConfirmDeletePopover: ({ children, onConfirm }) => children(onConfirm), + }, + "../lib/gatewaySocket": { getGatewayWebSocketClient: () => client }, + "../lib/storage": { loadToken: () => "fixture" }, + "../pages/settings/DevicesSection": { DevicesSection: () => null }, + "@liveagent/ui/lib/resourceHost": { configureResourceHostCapabilities() {}, kbrainResourceHostCapabilities: {} }, + }, + }); + // In-memory K-brain user rules; the desktop answers like gateway_bridge.rs does. + const store = [{ id: "u1", rule: "Bash(npm test:*)", effect: "allow", scope: "user" }]; + let dropAfterDelete = true; + const rpc = env.loadModule("src/lib/gatewaySocketRpc.ts"); + const client = fixtureClient(rpc, (type, payload) => { + const [, input] = encodedPermissionRules(env.loadModule, type, payload); + if (type === "permission.rules.list") return { rules: store.map((rule) => ({ ...rule })) }; + if (type === "permission.rules.delete") { + const index = store.findIndex((rule) => rule.id === input.id); + if (index < 0) throw new Error("permission rule not found (HTTP 404 Not Found)"); + store.splice(index, 1); + if (dropAfterDelete) { + dropAfterDelete = false; + throw new Error(DISCONNECTED); + } + return { ok: true }; + } + throw new Error(`unexpected ${type}`); + }); + const kbrain = env.loadModule("src/agent-ui-adapters/settingsExtension.tsx").settingsHostAdapter.kbrain; + const { PermissionRulesSection } = env.loadModule("@liveagent/ui/pages/settings/PermissionRulesSection.tsx"); + const host = document.body.appendChild(document.createElement("div")); + const root = env.createRoot(host); + const settle = () => env.act(async () => { for (let i = 0; i < 8; i++) await Promise.resolve(); }); + const rows = () => [...host.querySelectorAll('[data-permission-rules="user"] [data-permission-rule]')].map((row) => row.getAttribute("data-permission-rule")); + const deleteFirst = async () => { + // Once the page re-reads the list after the lost answer there is nothing left to retry. + const button = host.querySelector('[data-permission-rules="user"] button[data-delete-rule]'); + if (!button) return; + await env.act(async () => button.click()); + await settle(); + }; + const alertText = () => [...host.querySelectorAll('[role="alert"]')].map((node) => node.textContent).join(" | "); + try { + const settings = { system: { workspaceProjects: [], hiddenWorkspaceProjectPaths: [], archivedWorkspaceProjectPaths: [] } }; + await env.act(async () => root.render(env.React.createElement(PermissionRulesSection, { kbrain, settings }))); + await settle(); + assert.deepEqual(rows(), ["Bash(npm test:*)"]); + + await deleteFirst(); + assert.equal(store.length, 0, "K-brain did delete the rule"); + const firstError = alertText(); + const afterLostAnswer = rows(); + + await deleteFirst(); + const secondError = alertText(); + const afterRetry = rows(); + + assert.deepEqual( + { afterLostAnswer, afterRetry, firstError, secondError }, + { afterLostAnswer: [], afterRetry: [], firstError, secondError }, + "a failed mutation must re-read the list: the deleted rule stays on screen and every retry fails with 404", + ); + } finally { + await env.act(async () => root.unmount()); + host.remove(); + env.cleanup(); + } +}); diff --git a/crates/agent-gui/src-tauri/src/services/gateway/envelope_handler.rs b/crates/agent-gui/src-tauri/src/services/gateway/envelope_handler.rs index d9bc87e91..bbc5c484f 100644 --- a/crates/agent-gui/src-tauri/src/services/gateway/envelope_handler.rs +++ b/crates/agent-gui/src-tauri/src/services/gateway/envelope_handler.rs @@ -1255,6 +1255,34 @@ impl GatewayController { Err(error) => self.send_error_response(request_id, 500, error).await, } } + Some(proto::gateway_envelope::Payload::PermissionRules(request)) => { + // 读写 K-brain 规则文件,spawn 出去,别占单线程调度循环。 + let this = Arc::clone(self); + tauri::async_runtime::spawn(async move { + let result = match gateway_bridge::handle_permission_rules( + this.app_handle.clone(), + request, + ) + .await + { + Ok(response) => { + this.send_agent_envelope(proto::AgentEnvelope { + request_id: request_id.clone(), + timestamp: now_unix_seconds(), + payload: Some(proto::agent_envelope::Payload::PermissionRulesResp( + response, + )), + }) + .await + } + Err(error) => this.send_error_response(request_id, 400, error).await, + }; + if let Err(error) = result { + eprintln!("handle permission rules failed: {error}"); + } + }); + Ok(()) + } Some(proto::gateway_envelope::Payload::ProxyDetect(request)) => { // 检测要等握手超时(约 1.5s),测速最长 10s;spawn 出去,别让 Ping、 // 聊天探活(ChatPrepareTimeout 2s)在单线程调度循环里排队。 diff --git a/crates/agent-gui/src-tauri/src/services/gateway/ws_transport.rs b/crates/agent-gui/src-tauri/src/services/gateway/ws_transport.rs index 9a16bb895..3d44cb68d 100644 --- a/crates/agent-gui/src-tauri/src/services/gateway/ws_transport.rs +++ b/crates/agent-gui/src-tauri/src/services/gateway/ws_transport.rs @@ -31,6 +31,8 @@ pub(crate) const CONVERSATION_REFERENCES_V1_CAPABILITY: &str = "CONVERSATION_REF pub(crate) const COMPOSER_COMMANDS_V1_CAPABILITY: &str = "COMPOSER_COMMANDS_V1"; /// 应用代理设置的代理服务检测与测速(proxy_detect 请求)。 pub(crate) const PROXY_DETECT_V1_CAPABILITY: &str = "PROXY_DETECT_V1"; +/// 设置 → 权限:查看、添加、删除 K-brain 记住的权限规则(permission_rules 请求)。 +pub(crate) const PERMISSION_RULES_V1_CAPABILITY: &str = "PERMISSION_RULES_V1"; /// 桌面端主链路路径。 pub(crate) const GATEWAY_WS_AGENT_PATH: &str = "/ws/v2/agent"; /// 终端数据面路径。 @@ -92,6 +94,7 @@ pub(crate) fn build_client_hello( "planning_v1".to_string(), COMPOSER_COMMANDS_V1_CAPABILITY.to_string(), PROXY_DETECT_V1_CAPABILITY.to_string(), + PERMISSION_RULES_V1_CAPABILITY.to_string(), ], } } @@ -440,6 +443,7 @@ mod tests { "planning_v1", COMPOSER_COMMANDS_V1_CAPABILITY, PROXY_DETECT_V1_CAPABILITY, + PERMISSION_RULES_V1_CAPABILITY, ] ); diff --git a/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs b/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs index 698e4ba97..ddd440bbe 100644 --- a/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs +++ b/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs @@ -1641,6 +1641,118 @@ async fn kbrain_memory_manage( Ok(proto::MemoryManageResponse { result_json }) } +/// 设置 → 权限(WebUI 经网关):查看、添加、删除 K-brain 记住的权限规则。 +pub async fn handle_permission_rules( + app: AppHandle, + request: proto::PermissionRulesRequest, +) -> Result { + let connection = ensure_kbrain_connection(&app).await?; + kbrain_permission_rules(&connection, request).await +} + +fn invalid_permission_rules(reason: &str) -> String { + format!("invalid permission rules request: {reason}") +} + +/// 设置页只会发 workspace/rule/effect/scope/id;其余字段(session_id 等)不转发, +/// 会话规则不经这里。K-brain 的应答 JSON 原样返回。 +async fn kbrain_permission_rules( + connection: &KBrainBackendConnection, + request: proto::PermissionRulesRequest, +) -> Result { + let raw = request.input_json.trim(); + let input = if raw.is_empty() { + serde_json::Map::new() + } else { + match serde_json::from_str::(raw) { + Ok(Value::Object(object)) => object, + _ => return Err(invalid_permission_rules("input must be a JSON object")), + } + }; + let field = |key: &str| { + input + .get(key) + .and_then(Value::as_str) + .unwrap_or_default() + .trim() + .to_string() + }; + let workspace = field("workspace"); + let scope = field("scope"); + let check_scope = || match scope.as_str() { + "user" => Ok(()), + "project" if !workspace.is_empty() => Ok(()), + "project" => Err(invalid_permission_rules("project scope needs a workspace")), + _ => Err(invalid_permission_rules("scope must be project or user")), + }; + let (method, path, body) = match request.action.trim() { + "list" => { + let query = if workspace.is_empty() { + String::new() + } else { + format!("?workspace={}", urlencoding::encode(&workspace)) + }; + ( + reqwest::Method::GET, + format!("/v1/permissions/rules{query}"), + None, + ) + } + "add" => { + let rule = field("rule"); + if rule.is_empty() { + return Err(invalid_permission_rules("rule is required")); + } + let effect = field("effect"); + if effect != "allow" && effect != "deny" { + return Err(invalid_permission_rules("effect must be allow or deny")); + } + check_scope()?; + let mut create = json!({ "rule": rule, "effect": effect, "scope": scope }); + if scope == "project" { + create["workspace"] = json!(workspace); + } + ( + reqwest::Method::POST, + "/v1/permissions/rules".to_string(), + Some(create), + ) + } + "delete" => { + let id = field("id"); + if id.is_empty() { + return Err(invalid_permission_rules("id is required")); + } + check_scope()?; + let mut query = format!("?scope={}", urlencoding::encode(&scope)); + if scope == "project" { + query.push_str(&format!("&workspace={}", urlencoding::encode(&workspace))); + } + ( + reqwest::Method::DELETE, + format!("/v1/permissions/rules/{}{query}", urlencoding::encode(&id)), + None, + ) + } + _ => { + return Err(invalid_permission_rules( + "action must be list, add or delete", + )) + } + }; + let result = kbrain_json_request( + connection, + method, + &path, + body, + KBRAIN_COMMANDS_REQUEST_TIMEOUT, + ) + .await?; + let result_json = serde_json::to_string(&result) + .map_err(|e| format!("serialize permission rules result failed: {e}"))?; + Ok(proto::PermissionRulesResponse { result_json }) +} + /// `memory.manage` 里与记忆库无关的会话历史命令,仍由桌面端本地处理。 fn handle_history_manage_sync( request: proto::MemoryManageRequest, @@ -2184,10 +2296,11 @@ mod tests { use super::{ decode_kbrain_terminal_response, flatten_history_messages_json, flatten_history_messages_json_window, is_builtin_share_tool_name, is_kbrain_memory_command, - kbrain_memory_manage, kbrain_provider_models, kbrain_slash_commands, - kbrain_terminal_request, parse_occurrence_query, parse_runs_limit, prompt_workdir_query, - provider_models_request_config, proto, redact_builtin_tool_content_json, - sanitize_provider_summaries, KBrainBackendConnection, KBRAIN_MEMORY_COMMANDS, + kbrain_memory_manage, kbrain_permission_rules, kbrain_provider_models, + kbrain_slash_commands, kbrain_terminal_request, parse_occurrence_query, parse_runs_limit, + prompt_workdir_query, proto, provider_models_request_config, + redact_builtin_tool_content_json, sanitize_provider_summaries, KBrainBackendConnection, + KBRAIN_MEMORY_COMMANDS, }; use crate::commands::chat_history::{ self, history_message_content_hash, ChatHistoryMessageRef, ChatHistorySegmentRecord, @@ -2326,6 +2439,133 @@ mod tests { assert!(error.contains("invalid memory_read args JSON"), "{error}"); } + /// 设置 → 权限(WebUI 经网关):list/add/delete 映射到 K-brain 规则接口,只带设置页 + /// 需要的字段;会话规则与格式不对的请求不发给 K-brain。 + #[tokio::test] + async fn kbrain_permission_rules_maps_actions_and_rejects_bad_input() { + async fn one( + status: &'static str, + answer: &'static str, + action: &str, + input: &str, + ) -> ( + String, + Option, + Result, + ) { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind local K-brain fixture"); + let address = listener.local_addr().expect("fixture address"); + let server = tokio::spawn(serve_one_prompt_request(listener, status, answer)); + let result = kbrain_permission_rules( + &prompt_fixture_connection(address), + proto::PermissionRulesRequest { + action: action.to_string(), + input_json: input.to_string(), + }, + ) + .await; + let (headers, body) = server.await.expect("fixture task"); + (headers, body, result) + } + + let (headers, body, result) = one( + "200 OK", + r#"{"project_root":"D:\\w","rules":[]}"#, + "list", + r#"{"workspace":"D:\\w","session_id":"s1"}"#, + ) + .await; + assert!( + headers.starts_with("GET /v1/permissions/rules?workspace=D%3A%5Cw "), + "{headers}" + ); + assert_eq!(body, None); + assert_eq!( + serde_json::from_str::(&result.expect("list").result_json).expect("JSON"), + json!({"project_root": "D:\\w", "rules": []}) + ); + + let (headers, body, result) = one( + "201 Created", + r#"{"id":"r_9","rule":"Bash(rm:*)","effect":"deny","scope":"project"}"#, + "add", + r#"{"rule":"Bash(rm:*)","effect":"deny","scope":"project","workspace":"/w","extra":1}"#, + ) + .await; + assert!( + headers.starts_with("POST /v1/permissions/rules "), + "{headers}" + ); + assert_eq!( + body, + Some( + json!({"rule": "Bash(rm:*)", "effect": "deny", "scope": "project", "workspace": "/w"}) + ) + ); + assert!(result.expect("add").result_json.contains("r_9")); + + let (headers, _, result) = one( + "200 OK", + r#"{"ok":true}"#, + "delete", + r#"{"id":"r 1/x","scope":"user"}"#, + ) + .await; + assert!( + headers.starts_with("DELETE /v1/permissions/rules/r%201%2Fx?scope=user "), + "{headers}" + ); + result.expect("delete"); + + let (_, _, result) = one( + "400 Bad Request", + r#"{"error":"permission rule \"Bash(\": missing closing parenthesis"}"#, + "add", + r#"{"rule":"Bash(","effect":"deny","scope":"user"}"#, + ) + .await; + let error = result.expect_err("refused rule"); + assert!(error.contains("missing closing parenthesis"), "{error}"); + + // 本地拒绝:不连 K-brain(地址是关着的端口,连了就会报连接错误而不是校验错误)。 + let closed = prompt_fixture_connection("127.0.0.1:9".parse().expect("addr")); + for (action, input) in [ + ( + "add", + r#"{"rule":"Bash(rm:*)","effect":"deny","scope":"session"}"#, + ), + ( + "add", + r#"{"rule":"Bash(rm:*)","effect":"ask","scope":"user"}"#, + ), + ("add", r#"{"rule":" ","effect":"deny","scope":"user"}"#), + ( + "add", + r#"{"rule":"Bash(rm:*)","effect":"deny","scope":"project"}"#, + ), + ("delete", r#"{"scope":"user"}"#), + ("delete", r#"{"id":"r_1","scope":"session"}"#), + ("rename", "{}"), + ("add", "[1]"), + ] { + let error = kbrain_permission_rules( + &closed, + proto::PermissionRulesRequest { + action: action.to_string(), + input_json: input.to_string(), + }, + ) + .await + .expect_err("rejected locally"); + assert!( + error.starts_with("invalid permission rules request"), + "{action} {input}: {error}" + ); + } + } + fn prompt_fixture_connection(address: std::net::SocketAddr) -> KBrainBackendConnection { KBrainBackendConnection { base_url: format!("http://{address}"), diff --git a/crates/agent-gui/src/agent-ui-adapters/kbrainSettings.ts b/crates/agent-gui/src/agent-ui-adapters/kbrainSettings.ts index 3bb6bf117..1c44fb46d 100644 --- a/crates/agent-gui/src/agent-ui-adapters/kbrainSettings.ts +++ b/crates/agent-gui/src/agent-ui-adapters/kbrainSettings.ts @@ -3,6 +3,8 @@ import type { KBrainComputerStatus, KBrainSettingsAdapter, KBrainSettingsDocument, + PermissionRule, + PermissionRuleList, } from "@liveagent/ui/pages/settings/kbrainSettingsAdapter"; import { getKBrainRuntimeConnection } from "../lib/kbrain/runtimeConnection"; @@ -27,6 +29,48 @@ async function request(path: string, init?: RequestInit): Promise { return (await (await send(path, init)).json()) as T; } +// The rules API answers errors as {"error": "..."}; show the message, not the JSON. +async function rulesRequest(path: string, init?: RequestInit): Promise { + try { + return await request(path, init); + } catch (error) { + const text = error instanceof Error ? error.message : String(error); + let message = text; + try { + const parsed = JSON.parse(text) as { error?: unknown }; + if (typeof parsed.error === "string" && parsed.error) message = parsed.error; + } catch {} + throw new Error(message); + } +} + +type WirePermissionRule = { + id: string; + rule: string; + effect: PermissionRule["effect"]; + scope: PermissionRule["scope"]; + created_at?: string; + source_session?: string; +}; + +function permissionRuleFromWire(rule: WirePermissionRule): PermissionRule { + return { + id: rule.id, + rule: rule.rule, + effect: rule.effect, + scope: rule.scope, + ...(rule.created_at ? { createdAt: rule.created_at } : {}), + ...(rule.source_session ? { sourceSession: rule.source_session } : {}), + }; +} + +function rulesQuery(values: Record): string { + const query = new URLSearchParams(); + for (const [key, value] of Object.entries(values)) if (value) query.set(key, value); + const text = query.toString(); + return text ? `?${text}` : ""; +} + export function getKBrainHealth(): Promise { return send("/v1/health").then(readKBrainHealthResponse); } @@ -44,6 +88,28 @@ export const kbrainSettingsAdapter: KBrainSettingsAdapter = { method: "PUT", body: JSON.stringify(update), }), + listPermissionRules: async ({ workspace }) => { + const list = await rulesRequest<{ project_root?: string; rules?: WirePermissionRule[] }>( + `/v1/permissions/rules${rulesQuery({ workspace })}`, + ); + return { + ...(list.project_root ? { projectRoot: list.project_root } : {}), + rules: (list.rules ?? []).map(permissionRuleFromWire), + } satisfies PermissionRuleList; + }, + addPermissionRule: async (input) => + permissionRuleFromWire( + await rulesRequest("/v1/permissions/rules", { + method: "POST", + body: JSON.stringify(input), + }), + ), + deletePermissionRule: async ({ id, scope, workspace }) => { + await rulesRequest( + `/v1/permissions/rules/${encodeURIComponent(id)}${rulesQuery({ scope, workspace })}`, + { method: "DELETE" }, + ); + }, }; export const settingsHostAdapter = { diff --git a/crates/agent-gui/src/pages/settings/types.ts b/crates/agent-gui/src/pages/settings/types.ts index abc4061e1..a89c9fa83 100644 --- a/crates/agent-gui/src/pages/settings/types.ts +++ b/crates/agent-gui/src/pages/settings/types.ts @@ -13,6 +13,7 @@ export type SectionId = | "skills" | "mcp" | "systemTools" + | "permissions" | "stt" | "providers" | "agents" diff --git a/crates/agent-gui/test/settings/permission-rules-registration.test.mjs b/crates/agent-gui/test/settings/permission-rules-registration.test.mjs new file mode 100644 index 000000000..89c749bc7 --- /dev/null +++ b/crates/agent-gui/test/settings/permission-rules-registration.test.mjs @@ -0,0 +1,82 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { createDomTestEnv } from "../helpers/dom-test-env.mjs"; +import { createTsModuleLoader } from "../helpers/load-ts-module.mjs"; + +// LA-3a: Settings lists 权限 only when the host can reach the K-brain rules API (the desktop +// adapter today; the WebUI gets it in LA-3b). Both locales carry the page's text. + +const settings = { + stt: { provider: "tencent_cloud" }, + system: { workspaceProjects: [], hiddenWorkspaceProjectPaths: [], archivedWorkspaceProjectPaths: [] }, + customProviders: [], + customSettings: { fontScale: { sidebar: 1, chat: 1, rightDock: 1, settings: 1 } }, +}; + +async function sectionIds(kbrain) { + const shared = (name) => fileURLToPath(new URL(`../../../agent-ui/src/pages/settings/${name}`, import.meta.url)); + let sections = []; + const env = await createDomTestEnv({ + mocks: { + [shared("SettingsShell.tsx")]: { SettingsShell: ({ registry }) => { sections = registry.settingsSections; return null; } }, + [shared("PermissionRulesSection.tsx")]: { PermissionRulesSection: () => null }, + [shared("ProvidersSection.tsx")]: { ProvidersSection: () => null }, + [shared("SystemSettingsForm.tsx")]: { SystemSettingsForm: () => null }, + [shared("AgentsSection.tsx")]: { AgentsSection: () => null }, + [shared("ResourceHubSection.tsx")]: { ResourceHubSection: () => null }, + [shared("SystemToolsSection.tsx")]: { SystemToolsSection: () => null }, + [shared("CuaDriverSection.tsx")]: { CuaDriverSection: () => null }, + [shared("RemoteSection.tsx")]: { RemoteSection: () => null }, + [shared("SshSection.tsx")]: { SshSection: () => null }, + [shared("SttSection.tsx")]: { SttSection: () => null }, + [shared("HooksSection.tsx")]: { HooksSection: () => null }, + [shared("CronSection.tsx")]: { CronSection: () => null }, + [shared("AIApprovalSection.tsx")]: { AIApprovalSection: () => null }, + [shared("CalendarSection.tsx")]: { CalendarSection: () => null }, + [shared("memory/MemoryPanel.tsx")]: { MemoryPanel: () => null }, + "@liveagent/ui/i18n/index": { useLocale: () => ({ t: (key) => key }) }, + "@liveagent/adapters/settingsExtension": { + createSettingsExtension: () => ({ surface: "desktop", iconClassName: "", slots: {}, sections: [] }), + }, + "@liveagent/adapters/providerSettings": { ProviderSettingsExtension: () => null, ProviderCopyConfigButton: () => null }, + "@liveagent/ui/components/IconSet": new Proxy({}, { get: () => () => null }), + "@liveagent/ui/components/ui/button": { Button: (props) => env.React.createElement("button", props) }, + "@liveagent/ui/components/ui/input": { Input: (props) => env.React.createElement("input", props) }, + }, + }); + const { SettingsPage } = env.loadModule("@liveagent/ui/pages/settings/SettingsPage.tsx"); + const host = document.body.appendChild(document.createElement("div")); + const root = env.createRoot(host); + try { + await env.act(async () => root.render(env.React.createElement(SettingsPage, { + settings, setSettings: () => {}, saveState: { status: "saved" }, onBack: () => {}, + initialSection: "system", sttSettingsService: {}, + settingsHost: { isKbrain: true, kbrain }, + }))); + return sections.map((section) => `${section.id}:${section.groupKey}:${section.labelKey}`); + } finally { + await env.act(async () => root.unmount()); + host.remove(); + env.cleanup(); + } +} + +const base = { isKbrain: true, getConnection: () => null, getSettings: async () => ({}), updateSettings: async () => ({}) }; + +test("the permissions page is listed only for hosts with the rules API", async () => { + const withRules = await sectionIds({ ...base, listPermissionRules: async () => ({ rules: [] }), addPermissionRule: async () => ({}), deletePermissionRule: async () => {} }); + assert.equal(withRules.includes("permissions:settings.groupIntelligence:settings.navPermissions"), true); + const without = await sectionIds(base); + assert.equal(without.some((id) => id.startsWith("permissions:")), false); +}); + +test("both locales carry the same permission page keys", () => { + const loader = createTsModuleLoader(); + const zh = loader.loadModule("../agent-ui/src/i18n/translations/zhCNSettings.ts").ZH_CN_SETTINGS_TRANSLATIONS; + const en = loader.loadModule("../agent-ui/src/i18n/translations/enUSSettings.ts").EN_US_SETTINGS_TRANSLATIONS; + const keys = (table) => Object.keys(table).filter((key) => key === "settings.navPermissions" || key.startsWith("settings.permissions.")).sort(); + assert.deepEqual(keys(zh), keys(en)); + assert.equal(keys(zh).length >= 20, true); + assert.equal(zh["settings.navPermissions"], "权限"); +}); diff --git a/crates/agent-gui/test/settings/permission-rules.test.mjs b/crates/agent-gui/test/settings/permission-rules.test.mjs new file mode 100644 index 000000000..bab4ac0ce --- /dev/null +++ b/crates/agent-gui/test/settings/permission-rules.test.mjs @@ -0,0 +1,224 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { createDomTestEnv } from "../helpers/dom-test-env.mjs"; +import { createTsModuleLoader } from "../helpers/load-ts-module.mjs"; + +// LA-3a: Settings → 权限 lists the rules "allow and remember" stored (all projects and the +// chosen project), deletes them, and adds rules by hand. Assertions compare strings and +// booleans only: a failing assert on a DOM node would print the whole jsdom tree. + +const uiPath = (relative) => + fileURLToPath(new URL(`../../../agent-ui/src/${relative}`, import.meta.url)); + +const projects = [ + { id: "a", name: "LiveAgent", path: "D:\\work\\LiveAgent" }, + { id: "b", name: "K-brain", path: "D:\\work\\K-brain" }, + { id: "c", name: "Hidden", path: "D:\\work\\Hidden" }, +]; + +function settingsWith(list = projects) { + return { + system: { + workspaceProjects: list, + hiddenWorkspaceProjectPaths: ["D:\\work\\Hidden"], + archivedWorkspaceProjectPaths: [], + }, + }; +} + +const userRule = { id: "u1", rule: "Bash(npm test:*)", effect: "allow", scope: "user", createdAt: "2026-11-01T08:00:00Z", sourceSession: "cd5cf421aa" }; +const projectRule = { id: "p1", rule: "Bash(rm:*)", effect: "deny", scope: "project", createdAt: "2026-11-01T09:00:00Z" }; + +async function fixture(task, options = {}) { + const env = await createDomTestEnv({ mocks: { + "@liveagent/ui/components/IconSet": new Proxy({}, { get: () => () => null }), + "@liveagent/ui/components/ui/button": { Button: ({ children, size, variant, ...props }) => env.React.createElement("button", props, children) }, + "@liveagent/ui/components/settings/SettingsToggleGroup": { + SettingsToggleGroup: ({ children, value, onValueChange, disabled, ...props }) => env.React.createElement("div", props, + env.React.Children.map(children, (child) => env.React.cloneElement(child, { disabled: disabled || child.props.disabled, pressed: value.includes(child.props.value), onSelect: () => onValueChange([child.props.value]) }))), + SettingsToggleGroupItem: ({ children, value, pressed, onSelect, disabled, ...props }) => env.React.createElement("button", { ...props, disabled, "data-toggle": value, "aria-pressed": pressed, onClick: onSelect }, children), + }, + [uiPath("components/ui/select.tsx")]: { + Select: ({ value, onValueChange, disabled, children }) => env.React.createElement("select", { value, disabled, "data-project-select": "", onChange: (event) => onValueChange(event.target.value) }, children), + SelectItem: ({ value, children }) => env.React.createElement("option", { value }, children), + SelectValue: () => null, + }, + [uiPath("components/settings/SettingsSelect.tsx")]: { + SettingsSelectTrigger: () => null, + SettingsSelectContent: ({ children }) => env.React.createElement(env.React.Fragment, null, children), + }, + [uiPath("components/ui/confirm-action-popover.tsx")]: { + ConfirmDeletePopover: ({ children, onConfirm }) => children(onConfirm), + }, + } }); + const { PermissionRulesSection } = env.loadModule("@liveagent/ui/pages/settings/PermissionRulesSection.tsx"); + const calls = []; + let rules = { user: [userRule], project: { "D:\\work\\LiveAgent": [projectRule] } }; + let list = async ({ workspace }) => ({ + ...(workspace ? { projectRoot: workspace } : {}), + rules: [...rules.user, ...(workspace ? rules.project[workspace] ?? [] : [])], + }); + let add = async (input) => ({ id: `n${calls.length}`, rule: input.rule, effect: input.effect, scope: input.scope }); + const kbrain = { + getConnection: () => ({ baseUrl: "http://fixture", token: "t" }), + ...(options.capabilities ? { getCapabilities: async () => options.capabilities } : {}), + listPermissionRules: (query) => { calls.push(["list", query]); return list(query); }, + addPermissionRule: (input) => { calls.push(["add", input]); return add(input); }, + deletePermissionRule: async (ref) => { calls.push(["delete", ref]); }, + }; + const host = document.body.appendChild(document.createElement("div")); + const root = env.createRoot(host); + const settle = () => env.act(async () => { for (let i = 0; i < 5; i++) await Promise.resolve(); }); + const click = async (element) => { await env.act(async () => { element.click(); }); await settle(); }; + const button = (text) => [...host.querySelectorAll("button")].find((b) => b.textContent.trim() === text); + const rowsOf = (scope) => [...host.querySelectorAll(`[data-permission-rules="${scope}"] [data-permission-rule]`)].map((row) => row.getAttribute("data-permission-rule")); + const type = (value) => env.act(async () => { + const input = host.querySelector("input[data-permission-rule-input]"); + Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set.call(input, value); + input.dispatchEvent(new window.Event("input", { bubbles: true })); + }); + const pickProject = async (path) => { + await env.act(async () => { + const select = host.querySelector("select[data-project-select]"); + select.value = path; + select.dispatchEvent(new window.Event("change", { bubbles: true })); + }); + await settle(); + }; + try { + await env.act(async () => root.render(env.React.createElement(PermissionRulesSection, { kbrain, settings: options.settings ?? settingsWith() }))); + await settle(); + await task({ env, host, calls, button, click, rowsOf, type, pickProject, settle, + setRules: (next) => { rules = next; }, setList: (next) => { list = next; }, setAdd: (next) => { add = next; } }); + } finally { + await env.act(async () => root.unmount()); + host.remove(); + env.cleanup(); + } +} + +test("lists all-project rules and the first visible project's rules; switching project reloads", async () => { + await fixture(async ({ host, calls, rowsOf, pickProject }) => { + assert.deepEqual(calls[0], ["list", { workspace: "D:\\work\\LiveAgent" }]); + assert.deepEqual(rowsOf("user"), ["Bash(npm test:*)"]); + assert.deepEqual(rowsOf("project"), ["Bash(rm:*)"]); + const options = [...host.querySelectorAll("select[data-project-select] option")].map((o) => o.value); + assert.deepEqual(options, ["D:\\work\\LiveAgent", "D:\\work\\K-brain"], "hidden projects are not offered"); + assert.match(host.textContent, /允许/); + assert.match(host.textContent, /拒绝/); + assert.match(host.textContent, /cd5cf421/, "the source session is shown"); + await pickProject("D:\\work\\K-brain"); + assert.deepEqual(calls.at(-1), ["list", { workspace: "D:\\work\\K-brain" }]); + assert.deepEqual(rowsOf("project"), []); + assert.match(host.querySelector('[data-permission-rules="project"]').textContent, /还没有规则/); + }); +}); + +test("deleting a rule names its scope (and project) and reloads", async () => { + await fixture(async ({ host, calls, click, rowsOf, setRules }) => { + setRules({ user: [userRule], project: {} }); + await click(host.querySelector('[data-permission-rule="Bash(rm:*)"] button[data-delete-rule]')); + assert.deepEqual(calls.find((call) => call[0] === "delete"), ["delete", { id: "p1", scope: "project", workspace: "D:\\work\\LiveAgent" }]); + assert.deepEqual(rowsOf("project"), [], "the list is reloaded after deleting"); + await click(host.querySelector('[data-permission-rule="Bash(npm test:*)"] button[data-delete-rule]')); + assert.deepEqual(calls.filter((call) => call[0] === "delete").at(-1), ["delete", { id: "u1", scope: "user" }]); + }); +}); + +test("adds a deny rule for all projects by default, or for the chosen project", async () => { + await fixture(async ({ host, calls, button, click, type }) => { + assert.equal(button("添加").disabled, true, "nothing to add yet"); + await type("Bash(curl:*)"); + await click(button("添加")); + assert.deepEqual(calls.find((call) => call[0] === "add"), ["add", { rule: "Bash(curl:*)", effect: "deny", scope: "user" }]); + assert.equal(host.querySelector("input[data-permission-rule-input]").value, "", "the field clears after adding"); + await type("Edit(src/**)"); + await click(host.querySelector('[data-toggle="allow"]')); + await click(host.querySelector('[data-toggle="project"]')); + await click(button("添加")); + assert.deepEqual(calls.filter((call) => call[0] === "add").at(-1), ["add", { rule: "Edit(src/**)", effect: "allow", scope: "project", workspace: "D:\\work\\LiveAgent" }]); + }); +}); + +test("a rule K-brain refuses keeps the text and shows the reason", async () => { + await fixture(async ({ host, button, click, type, setAdd }) => { + setAdd(async () => { throw new Error('permission rule "Bash(": missing closing parenthesis'); }); + await type("Bash("); + await click(button("添加")); + assert.match(host.querySelector('[role="alert"]')?.textContent ?? "", /missing closing parenthesis/); + assert.equal(host.querySelector("input[data-permission-rule-input]").value, "Bash("); + }); +}); + +test("a backend without permission_rules shows an upgrade note and lists nothing", async () => { + await fixture(async ({ host, calls }) => { + assert.equal(calls.length, 0); + assert.match(host.textContent, /不支持权限规则/); + }, { capabilities: { memory_switch: 1 } }); +}); + +test("only the latest project's answer is shown when answers arrive out of order", async () => { + await fixture(async ({ calls, rowsOf, pickProject, setList, settle }) => { + let releaseSlow; + setList(async ({ workspace }) => { + if (workspace === "D:\\work\\K-brain") { + await new Promise((resolve) => { releaseSlow = resolve; }); + return { rules: [{ ...projectRule, id: "slow", rule: "Bash(slow:*)" }] }; + } + return { rules: [{ ...projectRule, id: "fast", rule: "Bash(fast:*)" }] }; + }); + await pickProject("D:\\work\\K-brain"); + await pickProject("D:\\work\\LiveAgent"); + releaseSlow(); + await settle(); + assert.deepEqual(rowsOf("project"), ["Bash(fast:*)"]); + assert.equal(calls.filter((call) => call[0] === "list").length >= 3, true); + }); +}); + +test("without projects the project list says so and project scope cannot be chosen", async () => { + await fixture(async ({ host, calls }) => { + assert.deepEqual(calls[0], ["list", {}]); + assert.match(host.querySelector('[data-permission-rules="project"]').textContent, /没有可选的项目/); + assert.equal(host.querySelector('[data-toggle="project"]').disabled, true); + }, { settings: settingsWith([]) }); +}); + +// The desktop adapter talks to K-brain directly. +test("the desktop adapter calls the rules endpoints and reports K-brain's error message", async () => { + const loader = createTsModuleLoader(); + const { setKBrainRuntimeConnection, clearKBrainRuntimeConnection } = loader.loadModule("src/lib/kbrain/runtimeConnection.ts"); + const { kbrainSettingsAdapter } = loader.loadModule("src/agent-ui-adapters/kbrainSettings.ts"); + const requests = []; + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url, init = {}) => { + requests.push([init.method ?? "GET", String(url), init.body ?? null]); + const path = new URL(String(url)).pathname; + if (init.method === "POST" && JSON.parse(init.body).rule === "Bash(") { + return new Response(JSON.stringify({ error: 'permission rule "Bash(": missing closing parenthesis' }), { status: 400 }); + } + if (init.method === "POST") return new Response(JSON.stringify({ id: "r9", rule: "Bash(curl:*)", effect: "deny", scope: "user", created_at: "2026-11-01T00:00:00Z" }), { status: 201 }); + if (init.method === "DELETE") return new Response(JSON.stringify({ ok: true }), { status: 200 }); + if (path === "/v1/permissions/rules") return new Response(JSON.stringify({ project_root: "D:\\w", rules: [{ id: "a", rule: "Bash(ls:*)", effect: "allow", scope: "project", created_at: "2026-11-01T00:00:00Z", source_session: "s1" }] }), { status: 200 }); + return new Response("{}", { status: 404 }); + }; + setKBrainRuntimeConnection({ baseUrl: "http://kb", token: "tok", protocolVersion: "kbrain.agent.v1" }); + try { + const listed = await kbrainSettingsAdapter.listPermissionRules({ workspace: "D:\\w" }); + assert.deepEqual(listed, { projectRoot: "D:\\w", rules: [{ id: "a", rule: "Bash(ls:*)", effect: "allow", scope: "project", createdAt: "2026-11-01T00:00:00Z", sourceSession: "s1" }] }); + assert.deepEqual(requests.at(-1), ["GET", "http://kb/v1/permissions/rules?workspace=D%3A%5Cw", null]); + const added = await kbrainSettingsAdapter.addPermissionRule({ rule: "Bash(curl:*)", effect: "deny", scope: "user" }); + assert.equal(added.id, "r9"); + assert.deepEqual(JSON.parse(requests.at(-1)[2]), { rule: "Bash(curl:*)", effect: "deny", scope: "user" }); + await kbrainSettingsAdapter.deletePermissionRule({ id: "a b", scope: "project", workspace: "D:\\w" }); + assert.deepEqual(requests.at(-1).slice(0, 2), ["DELETE", "http://kb/v1/permissions/rules/a%20b?scope=project&workspace=D%3A%5Cw"]); + await assert.rejects( + kbrainSettingsAdapter.addPermissionRule({ rule: "Bash(", effect: "deny", scope: "user" }), + (error) => error.message === 'permission rule "Bash(": missing closing parenthesis', + ); + } finally { + globalThis.fetch = originalFetch; + clearKBrainRuntimeConnection(); + } +}); diff --git a/crates/agent-gui/test/settings/review-la3a-permission-rules-r2.test.mjs b/crates/agent-gui/test/settings/review-la3a-permission-rules-r2.test.mjs new file mode 100644 index 000000000..7a7b698c6 --- /dev/null +++ b/crates/agent-gui/test/settings/review-la3a-permission-rules-r2.test.mjs @@ -0,0 +1,289 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { createDomTestEnv } from "../helpers/dom-test-env.mjs"; + +// LA-3a adversarial review, round 2 (after binding the list to its project, reload(target) +// and isImeKeyboardEvent). The fake K-brain below keeps rules per project the way +// K-brain does (one file per project root, random rule ids) and answers a DELETE for an +// id that is not in the named project's file with 404 {"error":"permission rule not found"} +// (internal/backend/permission_rules.go, permissions.RemoveRule). Assertions compare +// strings, numbers and booleans only: never pass a DOM node to assert. + +const uiPath = (relative) => + fileURLToPath(new URL(`../../../agent-ui/src/${relative}`, import.meta.url)); + +const LIVEAGENT = "D:\\work\\LiveAgent"; +const KBRAIN = "D:\\work\\K-brain"; +const DOCS = "D:\\work\\Docs"; + +const settings = { + system: { + workspaceProjects: [ + { id: "a", name: "LiveAgent", path: LIVEAGENT }, + { id: "b", name: "K-brain", path: KBRAIN }, + { id: "c", name: "Docs", path: DOCS }, + ], + hiddenWorkspaceProjectPaths: [], + archivedWorkspaceProjectPaths: [], + }, +}; + +function fakeKBrain() { + const store = { + user: [{ id: "r_user01", rule: "Bash(npm test:*)", effect: "allow", scope: "user" }], + project: { + [LIVEAGENT]: [{ id: "r_la0001", rule: "Bash(rm:*)", effect: "deny", scope: "project" }], + [KBRAIN]: [{ id: "r_kb0001", rule: "Bash(go test:*)", effect: "allow", scope: "project" }], + [DOCS]: [{ id: "r_dc0001", rule: "Edit(docs/**)", effect: "allow", scope: "project" }], + }, + }; + const calls = []; + const gates = { list: null, add: null, delete: null }; + const wait = (name, arg) => (gates[name] ? gates[name](arg) : Promise.resolve()); + const kbrain = { + isKbrain: true, + getConnection: () => ({ baseUrl: "http://fixture", token: "t" }), + listPermissionRules: async (query) => { + calls.push(JSON.stringify(["list", query.workspace ?? ""])); + await wait("list", query); + return { + ...(query.workspace ? { projectRoot: query.workspace } : {}), + rules: [...store.user, ...(query.workspace ? (store.project[query.workspace] ?? []) : [])], + }; + }, + addPermissionRule: async (input) => { + calls.push(JSON.stringify(["add", input])); + await wait("add", input); + const rule = { id: `r_new${calls.length}`, rule: input.rule, effect: input.effect, scope: input.scope }; + if (input.scope === "user") store.user.push(rule); + else (store.project[input.workspace] ??= []).push(rule); + return rule; + }, + deletePermissionRule: async (ref) => { + calls.push(JSON.stringify(["delete", ref])); + await wait("delete", ref); + const file = ref.scope === "user" ? store.user : (store.project[ref.workspace] ?? []); + const index = file.findIndex((rule) => rule.id === ref.id); + if (index < 0) throw new Error("permission rule not found"); + file.splice(index, 1); + }, + }; + return { kbrain, store, calls, gates }; +} + +async function fixture(task) { + const env = await createDomTestEnv({ + mocks: { + "@liveagent/ui/components/IconSet": new Proxy({}, { get: () => () => null }), + "@liveagent/ui/components/ui/button": { + Button: ({ children, size, variant, ...props }) => env.React.createElement("button", props, children), + }, + "@liveagent/ui/components/settings/SettingsToggleGroup": { + SettingsToggleGroup: ({ children, value, onValueChange, disabled, ...props }) => + env.React.createElement( + "div", + props, + env.React.Children.map(children, (child) => + env.React.cloneElement(child, { + disabled: disabled || child.props.disabled, + pressed: value.includes(child.props.value), + onSelect: () => onValueChange([child.props.value]), + }), + ), + ), + SettingsToggleGroupItem: ({ children, value, pressed, onSelect, disabled, ...props }) => + env.React.createElement("button", { ...props, disabled, "data-toggle": value, "aria-pressed": pressed, onClick: onSelect }, children), + }, + [uiPath("components/ui/select.tsx")]: { + Select: ({ value, onValueChange, disabled, children }) => + env.React.createElement("select", { value, disabled, "data-project-select": "", onChange: (event) => onValueChange(event.target.value) }, children), + SelectItem: ({ value, children }) => env.React.createElement("option", { value }, children), + SelectValue: () => null, + }, + [uiPath("components/settings/SettingsSelect.tsx")]: { + SettingsSelectTrigger: () => null, + SettingsSelectContent: ({ children }) => env.React.createElement(env.React.Fragment, null, children), + }, + [uiPath("components/ui/confirm-action-popover.tsx")]: { + ConfirmDeletePopover: ({ children, onConfirm }) => children(onConfirm), + }, + }, + }); + const { PermissionRulesSection } = env.loadModule("@liveagent/ui/pages/settings/PermissionRulesSection.tsx"); + const fake = fakeKBrain(); + const host = document.body.appendChild(document.createElement("div")); + const root = env.createRoot(host); + const settle = () => env.act(async () => { for (let i = 0; i < 8; i++) await Promise.resolve(); }); + const click = async (element) => { await env.act(async () => { element.click(); }); await settle(); }; + const button = (text) => [...host.querySelectorAll("button")].find((b) => b.textContent.trim() === text); + const rowsOf = (scope) => + [...host.querySelectorAll(`[data-permission-rules="${scope}"] [data-permission-rule]`)].map((row) => row.getAttribute("data-permission-rule")); + const projectHeaderPath = () => + [...host.querySelectorAll('[data-permission-rules="project"] p')].map((p) => p.textContent).find((text) => text.startsWith("D:\\")) ?? ""; + const alerts = () => [...host.querySelectorAll('[role="alert"]')].map((node) => node.textContent).join(" | "); + const type = (value) => env.act(async () => { + const input = host.querySelector("input[data-permission-rule-input]"); + Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set.call(input, value); + input.dispatchEvent(new window.Event("input", { bubbles: true })); + }); + const pickProject = async (path) => { + await env.act(async () => { + const select = host.querySelector("select[data-project-select]"); + select.value = path; + select.dispatchEvent(new window.Event("change", { bubbles: true })); + }); + await settle(); + }; + try { + await env.act(async () => root.render(env.React.createElement(PermissionRulesSection, { kbrain: fake.kbrain, settings }))); + await settle(); + await task({ env, host, fake, settle, click, button, rowsOf, projectHeaderPath, alerts, type, pickProject }); + } finally { + await env.act(async () => root.unmount()); + host.remove(); + env.cleanup(); + } +} + +function gate() { + let release; + const promise = new Promise((resolve) => { release = resolve; }); + return { promise, release }; +} + +// Risk 1/2: run() awaits the action and then calls the reload captured when the click +// happened. If the project changes while the action is in flight, that stale reload +// (old workspace) takes a newer ticket, drops the new project's answer and leaves the + +const deleteButtons = (host, scope) => + [...host.querySelectorAll(`[data-permission-rules="${scope}"] button[data-delete-rule]`)].length; +const rowButton = (host, rule) => + host.querySelector(`[data-permission-rule="${rule}"] button[data-delete-rule]`); +const stored = (fake, path) => (fake.store.project[path] ?? []).map((rule) => rule.rule); + +// R2-1: while the chosen project loads, its section shows no other project's rows (and no +// delete buttons); all-project rules stay; the answer then shows the chosen project. +test("r2: switching to a slow project hides the previous project's rows until it answers", async () => { + await fixture(async ({ host, fake, settle, rowsOf, projectHeaderPath, pickProject }) => { + const pending = gate(); + fake.gates.list = (query) => (query.workspace === KBRAIN ? pending.promise : Promise.resolve()); + await pickProject(KBRAIN); + assert.deepEqual(rowsOf("project"), [], "no LiveAgent rows under K-brain while it loads"); + assert.equal(deleteButtons(host, "project"), 0); + assert.deepEqual(rowsOf("user"), ["Bash(npm test:*)"], "all-project rules stay"); + pending.release(); + await settle(); + assert.equal(projectHeaderPath(), KBRAIN); + assert.deepEqual(rowsOf("project"), ["Bash(go test:*)"]); + }); +}); + +// R2-2: A -> B (slow) -> A: A's list is shown again and a delete names A. +test("r2: switching away and back before the slow answer deletes in the shown project", async () => { + await fixture(async ({ host, fake, settle, click, rowsOf, alerts, pickProject }) => { + const pending = gate(); + fake.gates.list = (query) => (query.workspace === KBRAIN ? pending.promise : Promise.resolve()); + await pickProject(KBRAIN); + await pickProject(LIVEAGENT); + assert.deepEqual(rowsOf("project"), ["Bash(rm:*)"]); + await click(rowButton(host, "Bash(rm:*)")); + pending.release(); + await settle(); + assert.equal(alerts(), ""); + assert.deepEqual(stored(fake, LIVEAGENT), [], "LiveAgent's rule is deleted"); + assert.deepEqual(rowsOf("project"), [], `slow K-brain answer must not replace LiveAgent: ${rowsOf("project").join(",")}`); + }); +}); + +// R2-3: a delete in flight while the user switches project names the rule's project and +// the reload afterwards shows the project chosen now. +test("r2: switching project while a delete is in flight deletes in the rule's project", async () => { + await fixture(async ({ host, fake, settle, click, rowsOf, projectHeaderPath, alerts, pickProject }) => { + const pending = gate(); + fake.gates.delete = () => pending.promise; + await click(rowButton(host, "Bash(rm:*)")); + await pickProject(KBRAIN); + pending.release(); + await settle(); + const deletes = fake.calls.filter((c) => c.startsWith('["delete"')); + assert.equal(deletes.at(-1), JSON.stringify(["delete", { id: "r_la0001", scope: "project", workspace: LIVEAGENT }])); + assert.equal(alerts(), ""); + assert.deepEqual(stored(fake, LIVEAGENT), []); + assert.equal(projectHeaderPath(), KBRAIN); + assert.deepEqual(rowsOf("project"), ["Bash(go test:*)"]); + }); +}); + +// R2-4: a switch during the reload that follows an add wins over that reload. +test("r2: switching project during the post-add reload shows the last chosen project", async () => { + await fixture(async ({ fake, settle, click, button, rowsOf, projectHeaderPath, type, pickProject }) => { + const pendingAdd = gate(); + const pendingReload = gate(); + let adds = 0; + fake.gates.add = () => { adds++; return pendingAdd.promise; }; + fake.gates.list = (query) => (adds > 0 && query.workspace === KBRAIN ? pendingReload.promise : Promise.resolve()); + await type("Bash(curl:*)"); + await click(button("添加")); + await pickProject(KBRAIN); + pendingAdd.release(); + await settle(); + await pickProject(DOCS); + pendingReload.release(); + await settle(); + assert.equal(projectHeaderPath(), DOCS); + assert.deepEqual(rowsOf("project"), ["Edit(docs/**)"]); + assert.deepEqual(rowsOf("user"), ["Bash(npm test:*)", "Bash(curl:*)"]); + }); +}); + +// R2-5: a project-scope add stores the rule in the project chosen when Add was pressed, +// even if the user switches while it runs. +test("r2: a project-scope add keeps the project chosen at the click", async () => { + await fixture(async ({ host, fake, settle, click, button, rowsOf, type, pickProject }) => { + const pending = gate(); + fake.gates.add = () => pending.promise; + await type("Bash(make:*)"); + await click(host.querySelector('[data-toggle="project"]')); + await click(button("添加")); + await pickProject(KBRAIN); + pending.release(); + await settle(); + assert.deepEqual(stored(fake, LIVEAGENT), ["Bash(rm:*)", "Bash(make:*)"]); + assert.deepEqual(stored(fake, KBRAIN), ["Bash(go test:*)"]); + assert.deepEqual(rowsOf("project"), ["Bash(go test:*)"]); + }); +}); + +// R2-6: a failed load for the chosen project shows no rows and no delete buttons; picking +// a project that loads recovers. +test("r2: a failed project load shows the error and no stale rows; another project recovers", async () => { + await fixture(async ({ host, fake, rowsOf, alerts, pickProject }) => { + fake.gates.list = (query) => (query.workspace === KBRAIN ? Promise.reject(new Error("project root must be absolute")) : Promise.resolve()); + await pickProject(KBRAIN); + assert.match(alerts(), /project root must be absolute/); + assert.deepEqual(rowsOf("project"), []); + assert.equal(deleteButtons(host, "project") + deleteButtons(host, "user"), 0); + await pickProject(DOCS); + assert.equal(alerts(), ""); + assert.deepEqual(rowsOf("project"), ["Edit(docs/**)"]); + }); +}); + +// R2-7: positive guard for the IME fix: isImeKeyboardEvent must not swallow a real Enter +// (keyCode 13). The keyCode 229 commit case is round 1's IME test. +test("r2: a plain Enter (keyCode 13) in the rule field still adds", async () => { + await fixture(async ({ env, host, fake, settle, type }) => { + const press = (init, keyCode) => env.act(async () => { + const input = host.querySelector("input[data-permission-rule-input]"); + const event = new window.KeyboardEvent("keydown", { bubbles: true, cancelable: true, ...init }); + Object.defineProperty(event, "keyCode", { value: keyCode }); + Object.defineProperty(event, "which", { value: keyCode }); + input.dispatchEvent(event); + }); + await type("Bash(curl:*)"); + await press({ key: "Enter" }, 13); + await settle(); + assert.deepEqual(fake.calls.filter((c) => c.startsWith('["add"')), [JSON.stringify(["add", { rule: "Bash(curl:*)", effect: "deny", scope: "user" }])]); + }); +}); diff --git a/crates/agent-gui/test/settings/review-la3a-permission-rules.test.mjs b/crates/agent-gui/test/settings/review-la3a-permission-rules.test.mjs new file mode 100644 index 000000000..753cd9542 --- /dev/null +++ b/crates/agent-gui/test/settings/review-la3a-permission-rules.test.mjs @@ -0,0 +1,244 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { createDomTestEnv } from "../helpers/dom-test-env.mjs"; + +// LA-3a adversarial review. The fake K-brain below keeps rules per project the way +// K-brain does (one file per project root, random rule ids) and answers a DELETE for an +// id that is not in the named project's file with 404 {"error":"permission rule not found"} +// (internal/backend/permission_rules.go, permissions.RemoveRule). Assertions compare +// strings, numbers and booleans only: never pass a DOM node to assert. + +const uiPath = (relative) => + fileURLToPath(new URL(`../../../agent-ui/src/${relative}`, import.meta.url)); + +const LIVEAGENT = "D:\\work\\LiveAgent"; +const KBRAIN = "D:\\work\\K-brain"; + +const settings = { + system: { + workspaceProjects: [ + { id: "a", name: "LiveAgent", path: LIVEAGENT }, + { id: "b", name: "K-brain", path: KBRAIN }, + ], + hiddenWorkspaceProjectPaths: [], + archivedWorkspaceProjectPaths: [], + }, +}; + +function fakeKBrain() { + const store = { + user: [{ id: "r_user01", rule: "Bash(npm test:*)", effect: "allow", scope: "user" }], + project: { + [LIVEAGENT]: [{ id: "r_la0001", rule: "Bash(rm:*)", effect: "deny", scope: "project" }], + [KBRAIN]: [{ id: "r_kb0001", rule: "Bash(go test:*)", effect: "allow", scope: "project" }], + }, + }; + const calls = []; + const gates = { list: null, add: null }; + const wait = (name, arg) => (gates[name] ? gates[name](arg) : Promise.resolve()); + const kbrain = { + isKbrain: true, + getConnection: () => ({ baseUrl: "http://fixture", token: "t" }), + listPermissionRules: async (query) => { + calls.push(JSON.stringify(["list", query.workspace ?? ""])); + await wait("list", query); + return { + ...(query.workspace ? { projectRoot: query.workspace } : {}), + rules: [...store.user, ...(query.workspace ? (store.project[query.workspace] ?? []) : [])], + }; + }, + addPermissionRule: async (input) => { + calls.push(JSON.stringify(["add", input])); + await wait("add", input); + const rule = { id: `r_new${calls.length}`, rule: input.rule, effect: input.effect, scope: input.scope }; + if (input.scope === "user") store.user.push(rule); + else (store.project[input.workspace] ??= []).push(rule); + return rule; + }, + deletePermissionRule: async (ref) => { + calls.push(JSON.stringify(["delete", ref])); + const file = ref.scope === "user" ? store.user : (store.project[ref.workspace] ?? []); + const index = file.findIndex((rule) => rule.id === ref.id); + if (index < 0) throw new Error("permission rule not found"); + file.splice(index, 1); + }, + }; + return { kbrain, store, calls, gates }; +} + +async function fixture(task) { + const env = await createDomTestEnv({ + mocks: { + "@liveagent/ui/components/IconSet": new Proxy({}, { get: () => () => null }), + "@liveagent/ui/components/ui/button": { + Button: ({ children, size, variant, ...props }) => env.React.createElement("button", props, children), + }, + "@liveagent/ui/components/settings/SettingsToggleGroup": { + SettingsToggleGroup: ({ children, value, onValueChange, disabled, ...props }) => + env.React.createElement( + "div", + props, + env.React.Children.map(children, (child) => + env.React.cloneElement(child, { + disabled: disabled || child.props.disabled, + pressed: value.includes(child.props.value), + onSelect: () => onValueChange([child.props.value]), + }), + ), + ), + SettingsToggleGroupItem: ({ children, value, pressed, onSelect, disabled, ...props }) => + env.React.createElement("button", { ...props, disabled, "data-toggle": value, "aria-pressed": pressed, onClick: onSelect }, children), + }, + [uiPath("components/ui/select.tsx")]: { + Select: ({ value, onValueChange, disabled, children }) => + env.React.createElement("select", { value, disabled, "data-project-select": "", onChange: (event) => onValueChange(event.target.value) }, children), + SelectItem: ({ value, children }) => env.React.createElement("option", { value }, children), + SelectValue: () => null, + }, + [uiPath("components/settings/SettingsSelect.tsx")]: { + SettingsSelectTrigger: () => null, + SettingsSelectContent: ({ children }) => env.React.createElement(env.React.Fragment, null, children), + }, + [uiPath("components/ui/confirm-action-popover.tsx")]: { + ConfirmDeletePopover: ({ children, onConfirm }) => children(onConfirm), + }, + }, + }); + const { PermissionRulesSection } = env.loadModule("@liveagent/ui/pages/settings/PermissionRulesSection.tsx"); + const fake = fakeKBrain(); + const host = document.body.appendChild(document.createElement("div")); + const root = env.createRoot(host); + const settle = () => env.act(async () => { for (let i = 0; i < 8; i++) await Promise.resolve(); }); + const click = async (element) => { await env.act(async () => { element.click(); }); await settle(); }; + const button = (text) => [...host.querySelectorAll("button")].find((b) => b.textContent.trim() === text); + const rowsOf = (scope) => + [...host.querySelectorAll(`[data-permission-rules="${scope}"] [data-permission-rule]`)].map((row) => row.getAttribute("data-permission-rule")); + const projectHeaderPath = () => + [...host.querySelectorAll('[data-permission-rules="project"] p')].map((p) => p.textContent).find((text) => text.startsWith("D:\\")) ?? ""; + const alerts = () => [...host.querySelectorAll('[role="alert"]')].map((node) => node.textContent).join(" | "); + const type = (value) => env.act(async () => { + const input = host.querySelector("input[data-permission-rule-input]"); + Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set.call(input, value); + input.dispatchEvent(new window.Event("input", { bubbles: true })); + }); + const pickProject = async (path) => { + await env.act(async () => { + const select = host.querySelector("select[data-project-select]"); + select.value = path; + select.dispatchEvent(new window.Event("change", { bubbles: true })); + }); + await settle(); + }; + try { + await env.act(async () => root.render(env.React.createElement(PermissionRulesSection, { kbrain: fake.kbrain, settings }))); + await settle(); + await task({ env, host, fake, settle, click, button, rowsOf, projectHeaderPath, alerts, type, pickProject }); + } finally { + await env.act(async () => root.unmount()); + host.remove(); + env.cleanup(); + } +} + +function gate() { + let release; + const promise = new Promise((resolve) => { release = resolve; }); + return { promise, release }; +} + +// Risk 1/2: run() awaits the action and then calls the reload captured when the click +// happened. If the project changes while the action is in flight, that stale reload +// (old workspace) takes a newer ticket, drops the new project's answer and leaves the +// old project's rules under the new project's header. +test("switching project while an add is in flight shows the new project's rules", async () => { + await fixture(async ({ fake, settle, click, button, rowsOf, projectHeaderPath, type, pickProject }) => { + assert.deepEqual(rowsOf("project"), ["Bash(rm:*)"]); + const pending = gate(); + fake.gates.add = () => pending.promise; + await type("Bash(curl:*)"); + await click(button("添加")); + await pickProject(KBRAIN); + assert.equal(projectHeaderPath(), KBRAIN, "the header follows the chosen project"); + pending.release(); + await settle(); + assert.equal(projectHeaderPath(), KBRAIN); + assert.deepEqual( + rowsOf("project"), + ["Bash(go test:*)"], + `the "${KBRAIN}" header lists another project's rules; list calls: ${fake.calls.filter((c) => c.startsWith('["list"')).join(" ")}`, + ); + }); +}); + +// Follow-on of the case above, end to end against the per-project fake: the row shown +// belongs to LiveAgent but the delete names the chosen project, so K-brain answers 404 and +// the rule the user meant to delete stays. +test("after switching project mid-add, deleting a shown project rule removes that rule", async () => { + await fixture(async ({ host, fake, settle, click, button, rowsOf, alerts, type, pickProject }) => { + const pending = gate(); + fake.gates.add = () => pending.promise; + await type("Bash(curl:*)"); + await click(button("添加")); + await pickProject(KBRAIN); + pending.release(); + await settle(); + const shown = rowsOf("project"); + const target = shown[0]; + const row = [...host.querySelectorAll('[data-permission-rules="project"] [data-permission-rule]')].find( + (node) => node.getAttribute("data-permission-rule") === target, + ); + await click(row.querySelector("button[data-delete-rule]")); + const deletes = fake.calls.filter((c) => c.startsWith('["delete"')); + assert.equal(alerts().includes("permission rule not found"), false, `delete of "${target}" failed: ${deletes.join(" ")}`); + const remaining = [...fake.store.project[LIVEAGENT], ...fake.store.project[KBRAIN]].map((rule) => rule.rule); + assert.equal(remaining.includes(target), false, `"${target}" is still stored after deleting it: ${deletes.join(" ")}`); + }); +}); + +// Risk 1: while the new project's list is loading, the old list stays on screen under +// the new header with its delete buttons enabled (only `busy` disables them); a delete +// then names the new project. +test("a project rule deleted while the next project's list is loading names its own project", async () => { + await fixture(async ({ host, fake, settle, click, rowsOf, alerts, pickProject }) => { + const pending = gate(); + fake.gates.list = (query) => (query.workspace === KBRAIN ? pending.promise : Promise.resolve()); + await pickProject(KBRAIN); + const row = host.querySelector('[data-permission-rules="project"] [data-permission-rule="Bash(rm:*)"]'); + const deletable = Boolean(row) && row.querySelector("button[data-delete-rule]")?.disabled === false; + if (deletable) { + await click(row.querySelector("button[data-delete-rule]")); + const deletes = fake.calls.filter((c) => c.startsWith('["delete"')); + assert.equal( + deletes.at(-1), + JSON.stringify(["delete", { id: "r_la0001", scope: "project", workspace: LIVEAGENT }]), + `LiveAgent's rule was deleted under another project; alerts: ${alerts()}`, + ); + } + pending.release(); + await settle(); + }); +}); + +// Risk 6: WebKit (Tauri on macOS = WKWebView, Safari) fires compositionend before the +// committing keydown, which arrives as Enter with keyCode 229 and isComposing=false. The +// rule field only checks isComposing; ToolApprovalBar and MentionComposer use +// isImeKeyboardEvent (MentionComposerInputUtils.ts), which also treats keyCode 229 as IME. +test("committing an IME candidate with Enter in the rule field does not add the rule (WebKit order)", async () => { + await fixture(async ({ env, host, fake }) => { + const input = host.querySelector("input[data-permission-rule-input]"); + await env.act(async () => { + input.dispatchEvent(new window.CompositionEvent("compositionstart", { bubbles: true })); + Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set.call(input, "Edit(文档"); + input.dispatchEvent(new window.Event("input", { bubbles: true })); + input.dispatchEvent(new window.CompositionEvent("compositionend", { bubbles: true, data: "文档" })); + const enter = new window.KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true }); + Object.defineProperty(enter, "keyCode", { value: 229 }); + Object.defineProperty(enter, "which", { value: 229 }); + input.dispatchEvent(enter); + for (let i = 0; i < 5; i++) await Promise.resolve(); + }); + const adds = fake.calls.filter((c) => c.startsWith('["add"')); + assert.deepEqual(adds, [], `the IME commit Enter added a half-typed rule: ${adds.join(" ")}`); + }); +}); diff --git a/crates/agent-ui/src/i18n/translations/enUSSettings.ts b/crates/agent-ui/src/i18n/translations/enUSSettings.ts index 1a00af291..aeb0624fd 100644 --- a/crates/agent-ui/src/i18n/translations/enUSSettings.ts +++ b/crates/agent-ui/src/i18n/translations/enUSSettings.ts @@ -70,6 +70,29 @@ export const EN_US_SETTINGS_TRANSLATIONS = { "settings.showSidebarShortcut": "Show {name} in sidebar", "settings.navSystem": "System", "settings.navSystemTools": "System Tools", + "settings.navPermissions": "Permissions", + "settings.permissions.title": "Permission rules", + "settings.permissions.description": + 'Rules saved with "Allow and remember" on an approval, and rules added here. Deny rules win over allow rules, and a deleted rule stops applying at once. Rules for a single session stay with that session and are not listed here.', + "settings.permissions.userScope": "All projects", + "settings.permissions.projectScope": "This project", + "settings.permissions.project": "Choose a project", + "settings.permissions.noProject": "No project to choose", + "settings.permissions.empty": "No rules yet", + "settings.permissions.allow": "Allow", + "settings.permissions.deny": "Deny", + "settings.permissions.addedAt": "Added {time}", + "settings.permissions.fromSession": "From session {id}", + "settings.permissions.delete": "Delete rule", + "settings.permissions.addTitle": "Add a rule", + "settings.permissions.rule": "Rule", + "settings.permissions.rulePlaceholder": "For example Bash(rm:*), Edit(src/**), Read", + "settings.permissions.effect": "Allow or deny", + "settings.permissions.scope": "Applies to", + "settings.permissions.add": "Add", + "settings.permissions.upgrade": + "This K-brain version has no permission rules. Upgrade the backend to use them.", + "settings.permissions.connectionChanged": "The K-brain connection changed. Reopen this page.", "settings.navStt": "Speech to Text", "settings.toolPermissionsOverridden": "{count} customized", "settings.toolPolicy.allow": "Allow", diff --git a/crates/agent-ui/src/i18n/translations/zhCNSettings.ts b/crates/agent-ui/src/i18n/translations/zhCNSettings.ts index dc8698a08..4fa210671 100644 --- a/crates/agent-ui/src/i18n/translations/zhCNSettings.ts +++ b/crates/agent-ui/src/i18n/translations/zhCNSettings.ts @@ -63,6 +63,28 @@ export const ZH_CN_SETTINGS_TRANSLATIONS = { "settings.showSidebarShortcut": "在侧栏显示 {name}", "settings.navSystem": "系统设置", "settings.navSystemTools": "系统工具", + "settings.navPermissions": "权限", + "settings.permissions.title": "权限规则", + "settings.permissions.description": + "审批时选「允许并记住」存下的规则,以及在这里手动添加的规则。拒绝规则优先于允许规则,删除后立即生效。本会话的规则只在那个会话里有效,不在这里列出。", + "settings.permissions.userScope": "所有项目", + "settings.permissions.projectScope": "本项目", + "settings.permissions.project": "选择项目", + "settings.permissions.noProject": "没有可选的项目", + "settings.permissions.empty": "还没有规则", + "settings.permissions.allow": "允许", + "settings.permissions.deny": "拒绝", + "settings.permissions.addedAt": "添加于 {time}", + "settings.permissions.fromSession": "来自会话 {id}", + "settings.permissions.delete": "删除规则", + "settings.permissions.addTitle": "添加规则", + "settings.permissions.rule": "规则", + "settings.permissions.rulePlaceholder": "例如 Bash(rm:*)、Edit(src/**)、Read", + "settings.permissions.effect": "允许还是拒绝", + "settings.permissions.scope": "生效范围", + "settings.permissions.add": "添加", + "settings.permissions.upgrade": "当前 K-brain 版本不支持权限规则,升级后端后可用。", + "settings.permissions.connectionChanged": "K-brain 连接已变化,请重新打开这一页。", "settings.navStt": "语音输入", "settings.toolPermissionsOverridden": "{count} 项已自定义", "settings.toolPolicy.allow": "允许", diff --git a/crates/agent-ui/src/pages/settings/PermissionRulesSection.tsx b/crates/agent-ui/src/pages/settings/PermissionRulesSection.tsx new file mode 100644 index 000000000..9954b55d0 --- /dev/null +++ b/crates/agent-ui/src/pages/settings/PermissionRulesSection.tsx @@ -0,0 +1,404 @@ +import { workspaceProjectPathKey } from "@liveagent/app/lib/settings"; +import type { SettingsSectionProps } from "@liveagent/app/pages/settings/types"; +import { Trash2 } from "@liveagent/ui/components/IconSet"; +import { SettingsSection } from "@liveagent/ui/components/settings/SettingsLayout"; +import { + SettingsToggleGroup, + SettingsToggleGroupItem, +} from "@liveagent/ui/components/settings/SettingsToggleGroup"; +import { Button } from "@liveagent/ui/components/ui/button"; +import { useLocale } from "@liveagent/ui/i18n"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { isImeKeyboardEvent } from "../../components/chat/MentionComposerInputUtils"; +import { + SettingsSelectContent, + SettingsSelectTrigger, +} from "../../components/settings/SettingsSelect"; +import { ConfirmDeletePopover } from "../../components/ui/confirm-action-popover"; +import { Select, SelectItem, SelectValue } from "../../components/ui/select"; +import type { + KBrainSettingsAdapter, + PermissionRule, + PermissionRuleEffect, + PermissionRuleInput, + PermissionRuleList, +} from "./kbrainSettingsAdapter"; + +const EFFECTS: readonly PermissionRuleEffect[] = ["deny", "allow"]; +const ADD_SCOPES: readonly PermissionRuleInput["scope"][] = ["user", "project"]; + +function connectionScope(kbrain: KBrainSettingsAdapter): string { + const connection = kbrain.getConnection(); + return JSON.stringify(connection ? [connection.baseUrl, connection.token] : null); +} + +function messageOf(reason: unknown): string { + return reason instanceof Error ? reason.message : String(reason); +} + +function formatTime(value: string | undefined): string { + if (!value) return ""; + const time = new Date(value); + return Number.isNaN(time.getTime()) + ? value + : time.toLocaleString(undefined, { dateStyle: "short", timeStyle: "short" }); +} + +/** + * Settings → 权限: the rules "allow and remember" stored, for all projects and for the + * chosen project, with delete and a form to add allow or deny rules by hand. Session rules + * stay with their session and are not listed here. + */ +export function PermissionRulesSection({ + kbrain, + settings, +}: { + kbrain: KBrainSettingsAdapter; + settings: SettingsSectionProps["settings"]; +}) { + const { t } = useLocale(); + // Archived and hidden projects are not offered, the same as in scheduled tasks. + const projects = useMemo(() => { + const excluded = new Set( + [ + ...settings.system.archivedWorkspaceProjectPaths, + ...settings.system.hiddenWorkspaceProjectPaths, + ].map(workspaceProjectPathKey), + ); + return settings.system.workspaceProjects + .filter((project) => !excluded.has(workspaceProjectPathKey(project.path))) + .map((project) => ({ path: project.path, name: project.name || project.path })); + }, [settings]); + const [chosen, setChosen] = useState(""); + const workspace = projects.some((project) => project.path === chosen) + ? chosen + : (projects[0]?.path ?? ""); + + const [supported, setSupported] = useState(kbrain.getCapabilities ? null : true); + // The list remembers the project it was loaded for: deleting a project rule names that + // project, and another project's rules are not shown while the chosen one loads. + const [loaded, setLoaded] = useState<{ workspace: string; list: PermissionRuleList } | null>( + null, + ); + const workspaceRef = useRef(workspace); + workspaceRef.current = workspace; + const [loading, setLoading] = useState(false); + const [loadError, setLoadError] = useState(""); + const [draft, setDraft] = useState(""); + const [effect, setEffect] = useState("deny"); + const [scope, setScope] = useState("user"); + const [busy, setBusy] = useState(false); + const [actionError, setActionError] = useState(""); + const epoch = useRef(0); + + useEffect(() => { + if (!kbrain.getCapabilities) return; + let active = true; + void kbrain + .getCapabilities() + .then((capabilities) => { + if (active) setSupported(Number(capabilities.permission_rules) >= 1); + }) + .catch((reason) => { + if (!active) return; + setSupported(true); + setLoadError(messageOf(reason)); + }); + return () => { + active = false; + }; + }, [kbrain]); + + // Each load gets a ticket; an answer for an older project or connection is dropped. + const reload = useCallback( + async (target: string) => { + if (!kbrain.listPermissionRules) return; + const ticket = ++epoch.current; + const connection = connectionScope(kbrain); + setLoading(true); + setLoadError(""); + try { + const next = await kbrain.listPermissionRules(target ? { workspace: target } : {}); + if (ticket !== epoch.current) return; + if (connection !== connectionScope(kbrain)) + throw new Error(t("settings.permissions.connectionChanged")); + setLoaded({ workspace: target, list: next }); + } catch (reason) { + if (ticket !== epoch.current) return; + setLoaded(null); + setLoadError(messageOf(reason)); + } finally { + if (ticket === epoch.current) setLoading(false); + } + }, + [kbrain, t], + ); + + useEffect(() => { + if (supported) void reload(workspace); + return () => { + ++epoch.current; + }; + }, [reload, supported, workspace]); + + const addScope = workspace ? scope : "user"; + + async function run(action: () => Promise) { + setBusy(true); + setActionError(""); + try { + await action(); + // The project chosen now: the user may have switched while the action ran. + await reload(workspaceRef.current); + return true; + } catch (reason) { + setActionError(messageOf(reason)); + // The change may have reached K-brain before its answer was lost (a dropped + // connection): list what is there now so a deleted rule does not linger. + await reload(workspaceRef.current); + return false; + } finally { + setBusy(false); + } + } + + async function add() { + const rule = draft.trim(); + if (!rule || busy || !kbrain.addPermissionRule) return; + const addRule = kbrain.addPermissionRule; + const input: PermissionRuleInput = { + rule, + effect, + scope: addScope, + ...(addScope === "project" ? { workspace } : {}), + }; + if (await run(() => addRule(input))) setDraft(""); + } + + function remove(rule: PermissionRule, ruleWorkspace: string) { + const deleteRule = kbrain.deletePermissionRule; + if (!deleteRule) return; + void run(() => + deleteRule({ + id: rule.id, + scope: rule.scope, + ...(rule.scope === "project" ? { workspace: ruleWorkspace } : {}), + }), + ); + } + + if (supported === null) return null; + + const list = loaded?.list ?? null; + const userRules = list?.rules.filter((rule) => rule.scope === "user") ?? []; + const projectRules = list?.rules.filter((rule) => rule.scope === "project") ?? []; + const projectLoaded = !!loaded && loaded.workspace === workspace; + const loadedWorkspace = loaded?.workspace ?? ""; + + const renderRules = (rules: PermissionRule[]) => + rules.length === 0 ? ( +

{t("settings.permissions.empty")}

+ ) : ( +
    + {rules.map((rule) => ( +
  • +
    + {rule.rule} +

    + {[ + rule.createdAt + ? t("settings.permissions.addedAt").replace( + "{time}", + formatTime(rule.createdAt), + ) + : "", + rule.sourceSession + ? t("settings.permissions.fromSession").replace( + "{id}", + rule.sourceSession.slice(0, 8), + ) + : "", + ] + .filter(Boolean) + .join(" · ")} +

    +
    + + {t(`settings.permissions.${rule.effect}`)} + + remove(rule, loadedWorkspace)}> + {(open) => ( + + )} + +
  • + ))} +
+ ); + + return ( + + {!supported ? ( +

+ {t("settings.permissions.upgrade")} +

+ ) : ( +
+ {loadError ? ( +

+ {loadError} +

+ ) : null} + +
+

+ {t("settings.permissions.userScope")} +

+ {list ? renderRules(userRules) : null} +
+ +
+
+

+ {t("settings.permissions.projectScope")} +

+ {projects.length > 0 ? ( + + ) : null} +
+ {projects.length === 0 ? ( +

+ {t("settings.permissions.noProject")} +

+ ) : projectLoaded ? ( + <> +

{workspace}

+ {renderRules(projectRules)} + + ) : null} +
+ +
+

+ {t("settings.permissions.addTitle")} +

+ setDraft(event.target.value)} + onKeyDown={(event) => { + // The Enter that commits an IME candidate (keyCode 229 in WebKit) picks + // a word; it does not add the rule. + if (event.key === "Enter" && !isImeKeyboardEvent(event)) { + event.preventDefault(); + void add(); + } + }} + className="h-8 w-full min-w-0 rounded-md border border-border bg-background px-2 font-mono text-xs text-foreground outline-none focus-visible:ring-2 focus-visible:ring-primary/35" + /> +
+ { + const next = values[0] as PermissionRuleEffect | undefined; + if (next && EFFECTS.includes(next)) setEffect(next); + }} + > + {EFFECTS.map((value) => ( + + {t(`settings.permissions.${value}`)} + + ))} + + { + const next = values[0] as PermissionRuleInput["scope"] | undefined; + if (next && ADD_SCOPES.includes(next)) setScope(next); + }} + > + {ADD_SCOPES.map((value) => ( + + {t( + value === "user" + ? "settings.permissions.userScope" + : "settings.permissions.projectScope", + )} + + ))} + + +
+ {actionError ? ( +

+ {actionError} +

+ ) : null} +
+
+ )} +
+ ); +} diff --git a/crates/agent-ui/src/pages/settings/SettingsPage.tsx b/crates/agent-ui/src/pages/settings/SettingsPage.tsx index 9a6dc136c..a6d281b60 100644 --- a/crates/agent-ui/src/pages/settings/SettingsPage.tsx +++ b/crates/agent-ui/src/pages/settings/SettingsPage.tsx @@ -13,6 +13,7 @@ import { Key, Mic, Settings2, + ShieldCheck, Wrench, Zap, } from "@liveagent/ui/components/IconSet"; @@ -24,6 +25,7 @@ import { CalendarSection } from "./CalendarSection"; import { CronSection } from "./CronSection"; import { HooksSection } from "./HooksSection"; import { MemoryPanel } from "./memory/MemoryPanel"; +import { PermissionRulesSection } from "./PermissionRulesSection"; import { ProvidersSection } from "./ProvidersSection"; import { RemoteSection } from "./RemoteSection"; import { ResourceHubSection } from "./ResourceHubSection"; @@ -177,6 +179,23 @@ export function SettingsPage(props: SettingsPageProps) { ), }, + // Only hosts that can reach the K-brain rules API get the page. + ...(props.settingsHost?.kbrain?.listPermissionRules + ? [ + { + id: "permissions", + groupKey: "settings.groupIntelligence", + groupOrder: 25, + order: 25, + labelKey: "settings.navPermissions", + icon: , + render: () => + props.settingsHost?.kbrain ? ( + + ) : null, + } satisfies SettingsSectionDefinition, + ] + : []), { id: "stt", groupKey: "settings.groupIntelligence", diff --git a/crates/agent-ui/src/pages/settings/kbrainSettingsAdapter.ts b/crates/agent-ui/src/pages/settings/kbrainSettingsAdapter.ts index af97faea8..e159fa16c 100644 --- a/crates/agent-ui/src/pages/settings/kbrainSettingsAdapter.ts +++ b/crates/agent-ui/src/pages/settings/kbrainSettingsAdapter.ts @@ -70,6 +70,31 @@ export type KBrainRuntimeConnection = { token: string; }; +export type PermissionRuleScope = "session" | "project" | "user"; +export type PermissionRuleEffect = "allow" | "deny"; + +/** One remembered rule from K-brain `/v1/permissions/rules`. */ +export type PermissionRule = { + id: string; + rule: string; + effect: PermissionRuleEffect; + scope: PermissionRuleScope; + createdAt?: string; + sourceSession?: string; +}; + +export type PermissionRuleList = { projectRoot?: string; rules: PermissionRule[] }; + +/** A rule to add; `workspace` names the project for project scope. */ +export type PermissionRuleInput = { + rule: string; + effect: PermissionRuleEffect; + scope: "project" | "user"; + workspace?: string; +}; + +export type PermissionRuleRef = { id: string; scope: PermissionRuleScope; workspace?: string }; + export type KBrainSettingsAdapter = { isKbrain: true; runtimeConnection: () => KBrainRuntimeConnection | null; @@ -81,6 +106,14 @@ export type KBrainSettingsAdapter = { getCapabilities?: () => Promise>; /** Parsed `/v1/health` handshake for diagnostics; only hosts talking to K-brain directly provide it. */ getHealth?: () => Promise; + /** + * K-brain permission rules (`capabilities.permission_rules`). Lists all-project rules and, + * with `workspace`, that project's rules. Hosts that cannot reach the rules API leave + * these out and Settings hides the page. + */ + listPermissionRules?: (query: { workspace?: string }) => Promise; + addPermissionRule?: (input: PermissionRuleInput) => Promise; + deletePermissionRule?: (ref: PermissionRuleRef) => Promise; }; export type SettingsHostAdapter = { diff --git a/docs/architecture/protocols.md b/docs/architecture/protocols.md index 52b04823d..498323f1e 100644 --- a/docs/architecture/protocols.md +++ b/docs/architecture/protocols.md @@ -203,6 +203,7 @@ Git 面板与文件树不再轮询:桌面端 `workspace_watch` 服务(notify | Skills 列表和管理 | `SkillFilesListRequest`、`SkillManageRequest`、`SkillMetadataReadRequest`、`SkillTextReadRequest` | `system_ensure_builtin_skills`、`system_manage_skill`、`system_read_skill_*`、`commands/app/system.rs`、`services/skills/*` | | Memory 管理 | `MemoryManageRequest` | `services/gateway_bridge.rs` 的 `handle_memory_manage`:记忆命令交给 K-brain `/v1/memory/manage`,`history_shared_list`、`chat_history_search` 在桌面端本地处理 | | Cron 管理 | `CronManageRequest` | `commands/automation/cron.rs`、`services/automation/*`、settings cron 表 | +| 权限规则(设置 → 权限) | `PermissionRulesRequest`(action `list`/`add`/`delete`,需桌面端声明 `PERMISSION_RULES_V1`) | `services/gateway_bridge.rs` 的 `handle_permission_rules`:只转发 workspace/rule/effect/scope/id,交给 K-brain `/v1/permissions/rules`;直连 K-brain 时由 Gateway `internal/kbrain/permission_rules.go` 转发 | ## 恢复与去重机制