diff --git a/crates/agent-gateway/internal/chatcmd/chatcmd.go b/crates/agent-gateway/internal/chatcmd/chatcmd.go index 30c93ce7a..408c5da2f 100644 --- a/crates/agent-gateway/internal/chatcmd/chatcmd.go +++ b/crates/agent-gateway/internal/chatcmd/chatcmd.go @@ -64,6 +64,7 @@ func NormalizeRequestBody(body *handler.ChatRequestBody) error { body.ClientRequestID = strings.TrimSpace(body.ClientRequestID) body.ExecutionMode = handler.NormalizeExecutionMode(body.ExecutionMode) body.Workdir = handler.NormalizeWorkdir(body.Workdir) + body.CommandSafetyMode = handler.NormalizeCommandSafetyMode(body.CommandSafetyMode) body.QueuePolicy = normalizeQueuePolicy(body.QueuePolicy) body.UploadedFiles = handler.NormalizeChatUploadedFiles(body.UploadedFiles) body.RuntimeControls = handler.NormalizeChatRuntimeControls(body.RuntimeControls) @@ -274,6 +275,7 @@ func buildUserMessageAppendedPayload( "uploaded_files": body.UploadedFiles, "execution_mode": body.ExecutionMode, "workdir": body.Workdir, + "command_safety_mode": body.CommandSafetyMode, "runtime_controls": body.RuntimeControls, "selected_model": body.SelectedModel, } @@ -324,6 +326,7 @@ func buildProtoRequest(body handler.ChatRequestBody) *gatewayv2.ChatRequest { RuntimeControls: handler.ToProtoChatRuntimeControls(body.RuntimeControls), ExecutionMode: body.ExecutionMode, Workdir: body.Workdir, + CommandSafetyMode: body.CommandSafetyMode, UploadedFiles: handler.ToProtoChatUploadedFiles(body.UploadedFiles), QueuePolicy: body.QueuePolicy, } @@ -356,6 +359,7 @@ func RequestBodyFromProto(req *gatewayv2.ChatRequest) handler.ChatRequestBody { Message: req.GetMessage(), ExecutionMode: req.GetExecutionMode(), Workdir: req.GetWorkdir(), + CommandSafetyMode: req.GetCommandSafetyMode(), QueuePolicy: req.GetQueuePolicy(), } if selected := req.GetSelectedModel(); selected != nil { diff --git a/crates/agent-gateway/internal/handler/types.go b/crates/agent-gateway/internal/handler/types.go index 84feec87f..c554458ec 100644 --- a/crates/agent-gateway/internal/handler/types.go +++ b/crates/agent-gateway/internal/handler/types.go @@ -35,6 +35,7 @@ type ChatRequestBody struct { RuntimeControls *ChatRuntimeControlsBody `json:"runtime_controls,omitempty"` ExecutionMode string `json:"execution_mode,omitempty"` Workdir string `json:"workdir,omitempty"` + CommandSafetyMode string `json:"command_safety_mode,omitempty"` UploadedFiles []ChatUploadedFileBody `json:"uploaded_files,omitempty"` QueuePolicy string `json:"queue_policy,omitempty"` } @@ -143,6 +144,17 @@ func NormalizeWorkdir(value string) string { return normalizeTrimmedText(value) } +// NormalizeCommandSafetyMode 归一化命令安全模式。仅放行四个合法值;空串或未知值 +// 归为空串,表示"远端未指定",桌面端据此回落到本地 settings.system.commandSafetyMode。 +func NormalizeCommandSafetyMode(value string) string { + switch normalizeTrimmedText(value) { + case "ask", "auto", "sandbox", "sandboxOffline": + return normalizeTrimmedText(value) + default: + return "" + } +} + func NormalizeChatUploadedFiles(input []ChatUploadedFileBody) []ChatUploadedFileBody { out := make([]ChatUploadedFileBody, 0, len(input)) seen := make(map[string]struct{}, len(input)) diff --git a/crates/agent-gateway/internal/handler/types_test.go b/crates/agent-gateway/internal/handler/types_test.go index 9527eb6b5..d8b5109b2 100644 --- a/crates/agent-gateway/internal/handler/types_test.go +++ b/crates/agent-gateway/internal/handler/types_test.go @@ -23,6 +23,28 @@ func TestNormalizeExecutionMode(t *testing.T) { } } +func TestNormalizeCommandSafetyMode(t *testing.T) { + t.Parallel() + + // 空串/未知值归为空串(表示"远端未指定"),桌面端据此回落本地设置; + // 绝不默认成某个具体模式,以免静默下调桌面端已选的更严格模式。 + cases := map[string]string{ + "": "", + "unknown": "", + " ask ": "ask", + "ask": "ask", + "auto": "auto", + "sandbox": "sandbox", + "sandboxOffline": "sandboxOffline", + } + + for input, want := range cases { + if got := NormalizeCommandSafetyMode(input); got != want { + t.Fatalf("NormalizeCommandSafetyMode(%q) = %q, want %q", input, got, want) + } + } +} + func TestNormalizeChatSelectedModelAcceptsGemini(t *testing.T) { t.Parallel() diff --git a/crates/agent-gateway/internal/proto/v2/gateway.pb.go b/crates/agent-gateway/internal/proto/v2/gateway.pb.go index 0405e9d4c..2142996da 100644 --- a/crates/agent-gateway/internal/proto/v2/gateway.pb.go +++ b/crates/agent-gateway/internal/proto/v2/gateway.pb.go @@ -6632,8 +6632,11 @@ type ChatRequest struct { ClientRequestId string `protobuf:"bytes,8,opt,name=client_request_id,json=clientRequestId,proto3" json:"client_request_id,omitempty"` RuntimeControls *ChatRuntimeControls `protobuf:"bytes,9,opt,name=runtime_controls,json=runtimeControls,proto3" json:"runtime_controls,omitempty"` QueuePolicy string `protobuf:"bytes,10,opt,name=queue_policy,json=queuePolicy,proto3" json:"queue_policy,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // 命令安全模式(ask/auto/sandbox/sandboxOffline)。远端 WebUI 直带,桌面端据此 + // 覆盖本地 settings.system.commandSafetyMode;空串表示未指定(回落本地设置)。 + CommandSafetyMode string `protobuf:"bytes,11,opt,name=command_safety_mode,json=commandSafetyMode,proto3" json:"command_safety_mode,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ChatRequest) Reset() { @@ -6729,6 +6732,13 @@ func (x *ChatRequest) GetQueuePolicy() string { return "" } +func (x *ChatRequest) GetCommandSafetyMode() string { + if x != nil { + return x.CommandSafetyMode + } + return "" +} + type ChatMessageRef struct { state protoimpl.MessageState `protogen:"open.v1"` SegmentIndex int32 `protobuf:"varint,1,opt,name=segment_index,json=segmentIndex,proto3" json:"segment_index,omitempty"` @@ -14256,7 +14266,7 @@ const file_proto_v2_gateway_proto_rawDesc = "" + "\vGitResponse\x12\x16\n" + "\x06action\x18\x01 \x01(\tR\x06action\x12\x1f\n" + "\vresult_json\x18\x02 \x01(\tR\n" + - "resultJson\"\xf2\x03\n" + + "resultJson\"\xa2\x04\n" + "\vChatRequest\x12'\n" + "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12\x18\n" + "\amessage\x18\x02 \x01(\tR\amessage\x12N\n" + @@ -14267,7 +14277,8 @@ const file_proto_v2_gateway_proto_rawDesc = "" + "\x11client_request_id\x18\b \x01(\tR\x0fclientRequestId\x12T\n" + "\x10runtime_controls\x18\t \x01(\v2).liveagent.gateway.v2.ChatRuntimeControlsR\x0fruntimeControls\x12!\n" + "\fqueue_policy\x18\n" + - " \x01(\tR\vqueuePolicyJ\x04\b\x06\x10\aR\x15selected_system_tools\"\xcf\x01\n" + + " \x01(\tR\vqueuePolicy\x12.\n" + + "\x13command_safety_mode\x18\v \x01(\tR\x11commandSafetyModeJ\x04\b\x06\x10\aR\x15selected_system_tools\"\xcf\x01\n" + "\x0eChatMessageRef\x12#\n" + "\rsegment_index\x18\x01 \x01(\x05R\fsegmentIndex\x12#\n" + "\rmessage_index\x18\x02 \x01(\x05R\fmessageIndex\x12\x1d\n" + diff --git a/crates/agent-gateway/proto/v2/gateway.proto b/crates/agent-gateway/proto/v2/gateway.proto index 96834593f..c9f766911 100644 --- a/crates/agent-gateway/proto/v2/gateway.proto +++ b/crates/agent-gateway/proto/v2/gateway.proto @@ -676,6 +676,9 @@ message ChatRequest { string client_request_id = 8; ChatRuntimeControls runtime_controls = 9; string queue_policy = 10; + // 命令安全模式(ask/auto/sandbox/sandboxOffline)。远端 WebUI 直带,桌面端据此 + // 覆盖本地 settings.system.commandSafetyMode;空串表示未指定(回落本地设置)。 + string command_safety_mode = 11; } message ChatMessageRef { diff --git a/crates/agent-gateway/web/src/agent-ui-adapters/sandboxCapability.ts b/crates/agent-gateway/web/src/agent-ui-adapters/sandboxCapability.ts new file mode 100644 index 000000000..7457b0a1e --- /dev/null +++ b/crates/agent-gateway/web/src/agent-ui-adapters/sandboxCapability.ts @@ -0,0 +1,16 @@ +export type SandboxCapability = { + supported: boolean; + mechanism: string; + platform: string; + reason?: string; +}; + +/** WebUI:沙箱在桌面端执行,浏览器的 OS 不代表执行端平台;null = 未知,显示通用文案。 */ +export function inferSandboxPlatform(): "macos" | "linux" | "windows" | null { + return null; +} + +/** WebUI:沙箱在桌面端执行,浏览器侧无从探测;null 表示能力未知(由桌面端裁决)。 */ +export function useSandboxCapability(): SandboxCapability | null { + return null; +} diff --git a/crates/agent-gateway/web/src/app/GatewayAppView.tsx b/crates/agent-gateway/web/src/app/GatewayAppView.tsx index 2a99f34a9..ef252782f 100644 --- a/crates/agent-gateway/web/src/app/GatewayAppView.tsx +++ b/crates/agent-gateway/web/src/app/GatewayAppView.tsx @@ -36,6 +36,7 @@ import { GatewayTranscript } from "@/components/GatewayTranscript"; import { getNextTheme, updateExecutionModeFromChatSelection, + updateSystem, updateWorkspaceResourceSettings, } from "@/lib/settings"; import { WorkdirPickerModal } from "@/pages/settings/WorkdirPickerModal"; @@ -681,6 +682,14 @@ export function GatewayAppView({ viewModel }: { viewModel: GatewayAppViewModel } modelOptions={modelOptions} selectedValue={selectedValue} chatRuntimeControls={chatRuntimeControlsForCurrentProvider} + commandSafetyMode={settings.system.commandSafetyMode} + onCommandSafetyModeChange={(mode) => + setSettings((prev) => + prev.system.commandSafetyMode === mode + ? prev + : updateSystem(prev, { commandSafetyMode: mode }), + ) + } reasoningOptions={chatRuntimeReasoningOptions} thinkingAlwaysOn={chatRuntimeThinkingAlwaysOn} contextUsageTokensSource={contextUsageTokensSource} diff --git a/crates/agent-gateway/web/src/app/chatEventUtils.ts b/crates/agent-gateway/web/src/app/chatEventUtils.ts index 497d0a97e..378d87d9c 100644 --- a/crates/agent-gateway/web/src/app/chatEventUtils.ts +++ b/crates/agent-gateway/web/src/app/chatEventUtils.ts @@ -103,5 +103,6 @@ export function buildGatewaySystemSettings(settings: AppSettings, workdirOverrid return { executionMode: settings.system.executionMode, workdir: workdirOverride ?? settings.system.workdir.trim(), + commandSafetyMode: settings.system.commandSafetyMode, }; } diff --git a/crates/agent-gateway/web/src/lib/gatewaySocketShared.ts b/crates/agent-gateway/web/src/lib/gatewaySocketShared.ts index 0197dd268..857d28ddc 100644 --- a/crates/agent-gateway/web/src/lib/gatewaySocketShared.ts +++ b/crates/agent-gateway/web/src/lib/gatewaySocketShared.ts @@ -67,6 +67,7 @@ export type GatewayRequestOptions = { export type GatewayChatSystemSettings = { executionMode?: string; workdir?: string; + commandSafetyMode?: string; }; export type GatewayChatCommandInput = { @@ -469,6 +470,7 @@ export function buildChatCommandPayload(input: GatewayChatCommandInput) { client_request_id: clientRequestId, execution_mode: systemSettings?.executionMode?.trim() || "text", workdir: systemSettings?.workdir?.trim() || "", + command_safety_mode: systemSettings?.commandSafetyMode?.trim() || "", uploaded_files: input.uploadedFiles?.map((file) => ({ relative_path: file.relativePath, diff --git a/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts b/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts index f16304fa6..88028f176 100644 --- a/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts +++ b/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts @@ -339,6 +339,7 @@ function buildChatCommand(body: J) { : undefined, executionMode: str(inner.execution_mode), workdir: str(inner.workdir), + commandSafetyMode: str(inner.command_safety_mode), uploadedFiles: uploadedFiles.map((file) => { const raw = rec(file); return create(ChatUploadedFileSchema, { 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 224c7631e..536763621 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("ChZwcm90by92Mi9nYXRld2F5LnByb3RvEhRsaXZlYWdlbnQuZ2F0ZXdheS52MiLZHQoPR2F0ZXdheUVudmVsb3BlEhIKCnJlcXVlc3RfaWQYASABKAkSEQoJdGltZXN0YW1wGAIgASgDEkAKDGNoYXRfY29tbWFuZBgKIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRDb21tYW5kUmVxdWVzdEgAEj4KC2Nyb25fbWFuYWdlGBQgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuQ3Jvbk1hbmFnZVJlcXVlc3RIABJACgxoaXN0b3J5X2xpc3QYHiABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5TGlzdFJlcXVlc3RIABI+CgtoaXN0b3J5X2dldBgfIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlHZXRSZXF1ZXN0SAASRAoOaGlzdG9yeV9yZW5hbWUYICABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UmVuYW1lUmVxdWVzdEgAEkQKDmhpc3RvcnlfZGVsZXRlGCEgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeURlbGV0ZVJlcXVlc3RIABJECg5oaXN0b3J5X3ByZWZpeBgiIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlQcmVmaXhSZXF1ZXN0SAASPgoLaGlzdG9yeV9waW4YIyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UGluUmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfZ2V0GCQgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlR2V0UmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfc2V0GCUgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlU2V0UmVxdWVzdEgAElEKFWhpc3Rvcnlfc2hhcmVfcmVzb2x2ZRgmIAEoCzIwLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVJlc29sdmVSZXF1ZXN0SAASSAoQaGlzdG9yeV93b3JrZGlycxgnIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlXb3JrZGlyc1JlcXVlc3RIABJCCg1wcm92aWRlcl9saXN0GCggASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJMaXN0UmVxdWVzdEgAEkAKDHNldHRpbmdzX2dldBgpIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzR2V0UmVxdWVzdEgAEkYKD3NldHRpbmdzX3VwZGF0ZRgqIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzVXBkYXRlUmVxdWVzdEgAEkcKEHNraWxsX2ZpbGVzX2xpc3QYKyABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbEZpbGVzTGlzdFJlcXVlc3RIABJNChNza2lsbF9tZXRhZGF0YV9yZWFkGCwgASgLMi4ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxNZXRhZGF0YVJlYWRSZXF1ZXN0SAASRQoPc2tpbGxfdGV4dF9yZWFkGC0gASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxUZXh0UmVhZFJlcXVlc3RIABJJChFmaWxlX21lbnRpb25fbGlzdBguIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZpbGVNZW50aW9uTGlzdFJlcXVlc3RIABJRChV1cGxvYWRfcmVhZGFibGVfZmlsZXMYLyABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRSZWFkYWJsZUZpbGVzUmVxdWVzdEgAEjgKCGZzX3Jvb3RzGDAgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSb290c1JlcXVlc3RIABI/Cgxmc19saXN0X2RpcnMYMSABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3REaXJzUmVxdWVzdEgAEjEKBHBpbmcYMiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5QaW5nUmVxdWVzdEgAElMKFnVwbG9hZGVkX2ltYWdlX3ByZXZpZXcYMyABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRlZEltYWdlUHJldmlld1JlcXVlc3RIABJCCg1tZW1vcnlfbWFuYWdlGDQgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuTWVtb3J5TWFuYWdlUmVxdWVzdEgAEkAKDHNraWxsX21hbmFnZRg1IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsTWFuYWdlUmVxdWVzdEgAElYKGGZzX2NyZWF0ZV9wcm9qZWN0X2ZvbGRlchg2IAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlUHJvamVjdEZvbGRlclJlcXVlc3RIABJBChB0ZXJtaW5hbF9yZXF1ZXN0GDcgASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxSZXF1ZXN0SAASNgoHZnNfbGlzdBg4IAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzTGlzdFJlcXVlc3RIABJBCg1mc193cml0ZV90ZXh0GDkgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNXcml0ZVRleHRSZXF1ZXN0SAASQQoNZnNfY3JlYXRlX2Rpchg6IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlRGlyUmVxdWVzdEgAEjoKCWZzX3JlbmFtZRg7IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVuYW1lUmVxdWVzdEgAEjoKCWZzX2RlbGV0ZRg8IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzRGVsZXRlUmVxdWVzdEgAEjcKC2dpdF9yZXF1ZXN0GD0gASgLMiAubGl2ZWFnZW50LmdhdGV3YXkudjIuR2l0UmVxdWVzdEgAElAKFWZzX3JlYWRfZWRpdGFibGVfdGV4dBg+IAEoCzIvLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZEVkaXRhYmxlVGV4dFJlcXVlc3RIABJUChdmc19yZWFkX3dvcmtzcGFjZV9pbWFnZRg/IAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZFdvcmtzcGFjZUltYWdlUmVxdWVzdEgAEjkKDHNmdHBfcmVxdWVzdBhAIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBSZXF1ZXN0SAASRgoPcHJvdmlkZXJfbW9kZWxzGEEgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJNb2RlbHNSZXF1ZXN0SAASXwodc2V0dGluZ3NfcmVzZXRfc3NoX2tub3duX2hvc3QYSCABKAsyNi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVxdWVzdEgAEjwKCmNoYXRfcXVldWUYSSABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVSZXF1ZXN0SAASQAoQY2hhdF9pbmdyZXNzX2FjaxhLIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzQWNrSAASQQoMdHVubmVsX3N0YXRlGFAgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsU3RhdGVTbmFwc2hvdEgAEj8KD3R1bm5lbF9tdXRhdGlvbhhRIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbE11dGF0aW9uSAASOQoMdHVubmVsX2ZyYW1lGFIgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsRnJhbWVIABJGCg93b3Jrc3BhY2Vfd2F0Y2gYWiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Xb3Jrc3BhY2VXYXRjaFJlcXVlc3RIABJOChdtYW5hZ2VkX3Byb2Nlc3NfcmVxdWVzdBhbIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLk1hbmFnZWRQcm9jZXNzUmVxdWVzdEgAEkQKDmhpc3RvcnlfYnJhbmNoGFwgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeUJyYW5jaFJlcXVlc3RIABJECg5wcm92aWRlcl91c2FnZRhdIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLlByb3ZpZGVyVXNhZ2VSZXF1ZXN0SAASQwoOY2hhdF9maWxlX29wZW4YXiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0RmlsZU9wZW5SZXF1ZXN0SAASRQoPaGlzdG9yeV9zZXRfY3dkGF8gASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNldEN3ZFJlcXVlc3RIABJRChV3b3Jrc3BhY2Vfcm9vdF9ncmFudHMYYCABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Xb3Jrc3BhY2VSb290R3JhbnRzUmVxdWVzdEgAEkgKEGltcG9ydF9kaXJlY3RvcnkYYSABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5JbXBvcnREaXJlY3RvcnlSZXF1ZXN0SAASPQoKY2hlY2twb2ludBhiIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoZWNrcG9pbnRSZXF1ZXN0SABCCQoHcGF5bG9hZEoECEMQREoECEQQRUoECEUQRkoECEoQSyLVJgoNQWdlbnRFbnZlbG9wZRISCgpyZXF1ZXN0X2lkGAEgASgJEhEKCXRpbWVzdGFtcBgCIAEoAxI1CgpjaGF0X2V2ZW50GAogASgLMh8ubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEV2ZW50SAASRAoQY3Jvbl9tYW5hZ2VfcmVzcBgUIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNyb25NYW5hZ2VSZXNwb25zZUgAEkYKEWhpc3RvcnlfbGlzdF9yZXNwGB4gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeUxpc3RSZXNwb25zZUgAEkQKEGhpc3RvcnlfZ2V0X3Jlc3AYHyABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5R2V0UmVzcG9uc2VIABJKChNoaXN0b3J5X3JlbmFtZV9yZXNwGCAgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVJlbmFtZVJlc3BvbnNlSAASSgoTaGlzdG9yeV9kZWxldGVfcmVzcBghIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlEZWxldGVSZXNwb25zZUgAEj4KDGhpc3Rvcnlfc3luYxgiIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTeW5jRXZlbnRIABJKChNoaXN0b3J5X3ByZWZpeF9yZXNwGCMgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVByZWZpeFJlc3BvbnNlSAASRAoQaGlzdG9yeV9waW5fcmVzcBgkIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlQaW5SZXNwb25zZUgAEk8KFmhpc3Rvcnlfc2hhcmVfZ2V0X3Jlc3AYJSABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2hhcmVHZXRSZXNwb25zZUgAEk8KFmhpc3Rvcnlfc2hhcmVfc2V0X3Jlc3AYJiABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2hhcmVTZXRSZXNwb25zZUgAElcKGmhpc3Rvcnlfc2hhcmVfcmVzb2x2ZV9yZXNwGCcgASgLMjEubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlUmVzb2x2ZVJlc3BvbnNlSAASTgoVaGlzdG9yeV93b3JrZGlyc19yZXNwGDggASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVdvcmtkaXJzUmVzcG9uc2VIABJIChJwcm92aWRlcl9saXN0X3Jlc3AYKCABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Qcm92aWRlckxpc3RSZXNwb25zZUgAEkYKEXNldHRpbmdzX2dldF9yZXNwGCkgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuU2V0dGluZ3NHZXRSZXNwb25zZUgAEkwKFHNldHRpbmdzX3VwZGF0ZV9yZXNwGCogASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuU2V0dGluZ3NVcGRhdGVSZXNwb25zZUgAEkAKDXNldHRpbmdzX3N5bmMYKyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1N5bmNFdmVudEgAEk0KFXNraWxsX2ZpbGVzX2xpc3RfcmVzcBgsIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsRmlsZXNMaXN0UmVzcG9uc2VIABJTChhza2lsbF9tZXRhZGF0YV9yZWFkX3Jlc3AYLSABKAsyLy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbE1ldGFkYXRhUmVhZFJlc3BvbnNlSAASSwoUc2tpbGxfdGV4dF9yZWFkX3Jlc3AYLiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbFRleHRSZWFkUmVzcG9uc2VIABJPChZmaWxlX21lbnRpb25fbGlzdF9yZXNwGC8gASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuRmlsZU1lbnRpb25MaXN0UmVzcG9uc2VIABJXChp1cGxvYWRfcmVhZGFibGVfZmlsZXNfcmVzcBgwIAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYyLlVwbG9hZFJlYWRhYmxlRmlsZXNSZXNwb25zZUgAEj4KDWZzX3Jvb3RzX3Jlc3AYMSABKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1Jvb3RzUmVzcG9uc2VIABIyCgRwb25nGDIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuUG9uZ1Jlc3BvbnNlSAASRQoRZnNfbGlzdF9kaXJzX3Jlc3AYMyABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3REaXJzUmVzcG9uc2VIABJZCht1cGxvYWRlZF9pbWFnZV9wcmV2aWV3X3Jlc3AYNCABKAsyMi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRlZEltYWdlUHJldmlld1Jlc3BvbnNlSAASSAoSbWVtb3J5X21hbmFnZV9yZXNwGDUgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuTWVtb3J5TWFuYWdlUmVzcG9uc2VIABJGChFza2lsbF9tYW5hZ2VfcmVzcBg2IAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsTWFuYWdlUmVzcG9uc2VIABJcCh1mc19jcmVhdGVfcHJvamVjdF9mb2xkZXJfcmVzcBg3IAEoCzIzLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlUHJvamVjdEZvbGRlclJlc3BvbnNlSAASQwoRdGVybWluYWxfcmVzcG9uc2UYOSABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFJlc3BvbnNlSAASPQoOdGVybWluYWxfZXZlbnQYOiABKAsyIy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbEV2ZW50SAASPAoMZnNfbGlzdF9yZXNwGDsgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNMaXN0UmVzcG9uc2VIABJHChJmc193cml0ZV90ZXh0X3Jlc3AYPCABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1dyaXRlVGV4dFJlc3BvbnNlSAASRwoSZnNfY3JlYXRlX2Rpcl9yZXNwGD0gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNDcmVhdGVEaXJSZXNwb25zZUgAEkAKDmZzX3JlbmFtZV9yZXNwGD4gASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSZW5hbWVSZXNwb25zZUgAEkAKDmZzX2RlbGV0ZV9yZXNwGD8gASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNEZWxldGVSZXNwb25zZUgAEjkKDGdpdF9yZXNwb25zZRhAIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYyLkdpdFJlc3BvbnNlSAASVgoaZnNfcmVhZF9lZGl0YWJsZV90ZXh0X3Jlc3AYQSABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1JlYWRFZGl0YWJsZVRleHRSZXNwb25zZUgAEloKHGZzX3JlYWRfd29ya3NwYWNlX2ltYWdlX3Jlc3AYQiABKAsyMi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1JlYWRXb3Jrc3BhY2VJbWFnZVJlc3BvbnNlSAASOwoNc2Z0cF9yZXNwb25zZRhJIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBSZXNwb25zZUgAEjUKCnNmdHBfZXZlbnQYSiABKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZnRwRXZlbnRIABJCCg9jaGF0X3F1ZXVlX3Jlc3AYSyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVSZXNwb25zZUgAEkAKEGNoYXRfcXVldWVfZXZlbnQYTCABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVFdmVudEgAEj4KDGNoYXRfY29udHJvbBhGIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRDb250cm9sRXZlbnRIABJCCg5ydW50aW1lX3N0YXR1cxhHIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlJ1bnRpbWVTdGF0dXNFdmVudEgAEmUKInNldHRpbmdzX3Jlc2V0X3NzaF9rbm93bl9ob3N0X3Jlc3AYSCABKAsyNy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVzcG9uc2VIABJKChVjaGF0X3J1bnRpbWVfc25hcHNob3QYTSABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UnVudGltZVNuYXBzaG90SAASTAoUcHJvdmlkZXJfbW9kZWxzX3Jlc3AYTyABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Qcm92aWRlck1vZGVsc1Jlc3BvbnNlSAASQgoOdHVubmVsX2Rlc2lyZWQYUCABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxEZXNpcmVkU3RhdGVIABJMChZ0dW5uZWxfbXV0YXRpb25fcmVzdWx0GFEgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsTXV0YXRpb25SZXN1bHRIABI5Cgx0dW5uZWxfZnJhbWUYUiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxGcmFtZUgAEkYKE3R1bm5lbF9wcm9iZV9yZXBvcnQYUyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxQcm9iZVJlcG9ydEgAEkoKEndvcmtzcGFjZV9hY3Rpdml0eRhaIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLldvcmtzcGFjZUFjdGl2aXR5RXZlbnRIABJQChhtYW5hZ2VkX3Byb2Nlc3NfcmVzcG9uc2UYWyABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5NYW5hZ2VkUHJvY2Vzc1Jlc3BvbnNlSAASUAoYbWFuYWdlZF9wcm9jZXNzX3NuYXBzaG90GFwgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuTWFuYWdlZFByb2Nlc3NTbmFwc2hvdEgAEkoKE2hpc3RvcnlfYnJhbmNoX3Jlc3AYXSABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5QnJhbmNoUmVzcG9uc2VIABJKChNwcm92aWRlcl91c2FnZV9yZXNwGF4gASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJVc2FnZVJlc3BvbnNlSAASRAoSY2hhdF9pbmdyZXNzX2JhdGNoGF8gASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NCYXRjaEgAEkYKE2NoYXRfaW5ncmVzc19yZXN1bWUYYCABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc1Jlc3VtZUgAEkoKFWNoYXRfaW5ncmVzc19mcmFnbWVudBhhIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzRnJhZ21lbnRIABJJChNjaGF0X2ZpbGVfb3Blbl9yZXNwGGIgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEZpbGVPcGVuUmVzcG9uc2VIABI0CgVlcnJvchhjIAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkVycm9yUmVzcG9uc2VIABJLChRoaXN0b3J5X3NldF9jd2RfcmVzcBhkIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTZXRDd2RSZXNwb25zZUgAElcKGndvcmtzcGFjZV9yb290X2dyYW50c19yZXNwGGUgASgLMjEubGl2ZWFnZW50LmdhdGV3YXkudjIuV29ya3NwYWNlUm9vdEdyYW50c1Jlc3BvbnNlSAASTgoVaW1wb3J0X2RpcmVjdG9yeV9yZXNwGGYgASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuSW1wb3J0RGlyZWN0b3J5UmVzcG9uc2VIABJDCg9jaGVja3BvaW50X3Jlc3AYZyABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGVja3BvaW50UmVzcG9uc2VIAEIJCgdwYXlsb2FkSgQIQxBESgQIRBBFSgQIRRBGSgQIThBPIlUKEUNoYXRTZWxlY3RlZE1vZGVsEhoKEmN1c3RvbV9wcm92aWRlcl9pZBgBIAEoCRINCgVtb2RlbBgCIAEoCRIVCg1wcm92aWRlcl90eXBlGAMgASgJImUKE0NoYXRSdW50aW1lQ29udHJvbHMSGAoQdGhpbmtpbmdfZW5hYmxlZBgBIAEoCBIhChluYXRpdmVfd2ViX3NlYXJjaF9lbmFibGVkGAIgASgIEhEKCXJlYXNvbmluZxgDIAEoCSJ1ChBDaGF0VXBsb2FkZWRGaWxlEhUKDXJlbGF0aXZlX3BhdGgYASABKAkSFQoNYWJzb2x1dGVfcGF0aBgCIAEoCRIRCglmaWxlX25hbWUYAyABKAkSDAoEa2luZBgEIAEoCRISCgpzaXplX2J5dGVzGAUgASgDIksKElVwbG9hZFJlYWRhYmxlRmlsZRIRCglmaWxlX25hbWUYASABKAkSEQoJbWltZV90eXBlGAIgASgJEg8KB2NvbnRlbnQYAyABKAwiZgoaVXBsb2FkUmVhZGFibGVGaWxlc1JlcXVlc3QSDwoHd29ya2RpchgBIAEoCRI3CgVmaWxlcxgCIAMoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlVwbG9hZFJlYWRhYmxlRmlsZSJlChtVcGxvYWRSZWFkYWJsZUZpbGVzUmVzcG9uc2USNQoFZmlsZXMYASADKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0VXBsb2FkZWRGaWxlEg8KB3NraXBwZWQYAiADKAkiPQoTSW1wb3J0RGlyZWN0b3J5RmlsZRIVCg1yZWxhdGl2ZV9wYXRoGAEgASgJEg8KB2NvbnRlbnQYAiABKAwiwwIKFkltcG9ydERpcmVjdG9yeVJlcXVlc3QSDAoEbmFtZRgBIAEoCRIOCgZ0YXJnZXQYAiABKAkSPAoFZmlsZXMYAyADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5JbXBvcnREaXJlY3RvcnlGaWxlQgIYARITCgt0cmFuc2Zlcl9pZBgEIAEoCRJBCglvcGVyYXRpb24YBSABKA4yLi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5JbXBvcnREaXJlY3RvcnlPcGVyYXRpb24SEwoLdG90YWxfZmlsZXMYBiABKA0SEwoLdG90YWxfYnl0ZXMYByABKAQSFQoNcmVsYXRpdmVfcGF0aBgIIAEoCRIOCgZvZmZzZXQYCSABKAQSDQoFY2h1bmsYCiABKAwSFQoNZmlsZV9jb21wbGV0ZRgLIAEoCCJ+ChdJbXBvcnREaXJlY3RvcnlSZXNwb25zZRIRCglyb290X3BhdGgYASABKAkSEgoKZmlsZV9jb3VudBgCIAEoBRIPCgdza2lwcGVkGAMgAygJEhMKC3RyYW5zZmVyX2lkGAQgASgJEhYKDnJlY2VpdmVkX2J5dGVzGAUgASgEIkUKG1VwbG9hZGVkSW1hZ2VQcmV2aWV3UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEhUKDWFic29sdXRlX3BhdGgYAiABKAkiPwocVXBsb2FkZWRJbWFnZVByZXZpZXdSZXNwb25zZRIRCgltaW1lX3R5cGUYASABKAkSDAoEZGF0YRgCIAEoCSJ7CgpUdW5uZWxTcGVjEgoKAmlkGAEgASgJEhEKCXNsdWdfaGludBgCIAEoCRIMCgRuYW1lGAMgASgJEhIKCnRhcmdldF91cmwYBCABKAkSEgoKZXhwaXJlc19hdBgFIAEoAxIYChBwcm9qZWN0X3BhdGhfa2V5GAYgASgJIlkKElR1bm5lbERlc2lyZWRTdGF0ZRIxCgd0dW5uZWxzGAEgAygLMiAubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsU3BlYxIQCghyZXZpc2lvbhgCIAEoBCJmCgxUdW5uZWxIZWFsdGgSDgoGc3RhdHVzGAEgASgJEhMKC2h0dHBfc3RhdHVzGAIgASgNEg0KBWVycm9yGAMgASgJEhIKCmNoZWNrZWRfYXQYBCABKAMSDgoGcnR0X21zGAUgASgNIvABCgxUdW5uZWxTdGF0dXMSCgoCaWQYASABKAkSDAoEc2x1ZxgCIAEoCRIMCgRuYW1lGAMgASgJEhIKCnRhcmdldF91cmwYBCABKAkSEwoLcHVibGljX3BhdGgYBSABKAkSEgoKY3JlYXRlZF9hdBgGIAEoAxISCgpleHBpcmVzX2F0GAcgASgDEhoKEmFjdGl2ZV9jb25uZWN0aW9ucxgIIAEoDRIYChBwcm9qZWN0X3BhdGhfa2V5GAkgASgJEjEKBWxvY2FsGAogASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhbHRoIqUBChNUdW5uZWxTdGF0ZVNuYXBzaG90EjMKB3R1bm5lbHMYASADKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxTdGF0dXMSEAoIcmV2aXNpb24YAiABKAQSFAoMYWdlbnRfb25saW5lGAMgASgIEjEKBXJlbGF5GAQgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhbHRoIpkBCg5UdW5uZWxNdXRhdGlvbhIOCgZhY3Rpb24YASABKAkSEQoJdHVubmVsX2lkGAIgASgJEhIKCnRhcmdldF91cmwYAyABKAkSDAoEbmFtZRgEIAEoCRIYCgt0dGxfc2Vjb25kcxgFIAEoDUgAiAEBEhgKEHByb2plY3RfcGF0aF9rZXkYBiABKAlCDgoMX3R0bF9zZWNvbmRzIlQKFFR1bm5lbE11dGF0aW9uUmVzdWx0EhEKCXR1bm5lbF9pZBgBIAEoCRISCgplcnJvcl9jb2RlGAIgASgJEhUKDWVycm9yX21lc3NhZ2UYAyABKAkiWQoRVHVubmVsUHJvYmVSZXN1bHQSEQoJdHVubmVsX2lkGAEgASgJEjEKBWxvY2FsGAIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhbHRoIk0KEVR1bm5lbFByb2JlUmVwb3J0EjgKB3Jlc3VsdHMYASADKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxQcm9iZVJlc3VsdCIrCgxUdW5uZWxIZWFkZXISDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSL1AgoLVHVubmVsRnJhbWUSEQoJc3RyZWFtX2lkGAEgASgJEjMKBGtpbmQYAiABKA4yJS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxGcmFtZUtpbmQSEgoKdGFyZ2V0X3VybBgDIAEoCRIOCgZtZXRob2QYBCABKAkSDAoEcGF0aBgFIAEoCRIzCgdoZWFkZXJzGAYgAygLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhZGVyEg4KBnN0YXR1cxgHIAEoDRIMCgRib2R5GAggASgMEg0KBWVycm9yGAkgASgJEkIKD3dzX21lc3NhZ2VfdHlwZRgKIAEoDjIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbFdzTWVzc2FnZVR5cGUSFgoOd3Nfc3VicHJvdG9jb2wYCyABKAkSFQoNd3NfY2xvc2VfY29kZRgMIAEoDRIXCg93c19jbG9zZV9yZWFzb24YDSABKAkiKQoVV29ya3NwYWNlV2F0Y2hSZXF1ZXN0EhAKCHdvcmtkaXJzGAEgAygJIn4KFldvcmtzcGFjZUFjdGl2aXR5RXZlbnQSDwoHd29ya2RpchgBIAEoCRIQCghyZXZpc2lvbhgCIAEoBBIKCgJmcxgDIAEoCBILCgNnaXQYBCABKAgSFQoNY2hhbmdlZF9wYXRocxgFIAMoCRIRCgl0cnVuY2F0ZWQYBiABKAgilgIKFE1hbmFnZWRQcm9jZXNzUmVjb3JkEgoKAmlkGAEgASgJEg0KBWxhYmVsGAIgASgJEg8KB2NvbW1hbmQYAyABKAkSCwoDY3dkGAQgASgJEg0KBXNoZWxsGAUgASgJEgsKA3BpZBgGIAEoDRIQCghsb2dfcGF0aBgHIAEoCRISCgpzdGFydGVkX2F0GAggASgDEhgKC2ZpbmlzaGVkX2F0GAkgASgDSACIAQESFgoJZXhpdF9jb2RlGAogASgFSAGIAQESDwoHcnVubmluZxgLIAEoCBIQCghpc29sYXRlZBgMIAEoCBIQCghyZXN0b3JlZBgNIAEoCEIOCgxfZmluaXNoZWRfYXRCDAoKX2V4aXRfY29kZSJpChZNYW5hZ2VkUHJvY2Vzc1NuYXBzaG90Ej0KCXByb2Nlc3NlcxgBIAMoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLk1hbmFnZWRQcm9jZXNzUmVjb3JkEhAKCHJldmlzaW9uGAIgASgEIk4KFU1hbmFnZWRQcm9jZXNzUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSEgoKcHJvY2Vzc19pZBgCIAEoCRIRCgltYXhfYnl0ZXMYAyABKA0itwEKFk1hbmFnZWRQcm9jZXNzUmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEj4KCHNuYXBzaG90GAIgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuTWFuYWdlZFByb2Nlc3NTbmFwc2hvdBITCgtsb2dfY29udGVudBgDIAEoCRIQCghsb2dfcGF0aBgEIAEoCRIVCg1sb2dfdHJ1bmNhdGVkGAUgASgIEg8KB3N0b3BwZWQYBiABKAgiOQoTTWVtb3J5TWFuYWdlUmVxdWVzdBIPCgdjb21tYW5kGAEgASgJEhEKCWFyZ3NfanNvbhgCIAEoCSIrChRNZW1vcnlNYW5hZ2VSZXNwb25zZRITCgtyZXN1bHRfanNvbhgBIAEoCSKYAwoPVGVybWluYWxSZXF1ZXN0Eg4KBmFjdGlvbhgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAyABKAkSCwoDY3dkGAQgASgJEg0KBXNoZWxsGAUgASgJEg0KBXRpdGxlGAYgASgJEgwKBGRhdGEYByABKAkSDAoEY29scxgIIAEoDRIMCgRyb3dzGAkgASgNEhEKCW1heF9ieXRlcxgKIAEoDRITCgtzc2hfaG9zdF9pZBgLIAEoCRIRCglwcm9tcHRfaWQYDCABKAkSFQoNcHJvbXB0X2Fuc3dlchgNIAEoCRIWCg50cnVzdF9ob3N0X2tleRgOIAEoCBIUCgxzZnRwX2VuYWJsZWQYDyABKAgSDgoGdGFiX2lkGBAgASgJEhAKCHRhYl9raW5kGBEgASgJEhMKC3JlbW90ZV9ob3N0GBIgASgJEhMKC3JlbW90ZV9wb3J0GBMgASgNEhIKCmxvY2FsX3BvcnQYFCABKA0SEgoKZm9yd2FyZF9pZBgVIAEoCSKyAgoPVGVybWluYWxTZXNzaW9uEgoKAmlkGAEgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAiABKAkSCwoDY3dkGAMgASgJEg0KBXNoZWxsGAQgASgJEg0KBXRpdGxlGAUgASgJEgsKA3BpZBgGIAEoDRIMCgRjb2xzGAcgASgNEgwKBHJvd3MYCCABKA0SEgoKY3JlYXRlZF9hdBgJIAEoBBISCgp1cGRhdGVkX2F0GAogASgEEhMKC2ZpbmlzaGVkX2F0GAsgASgEEhEKCWV4aXRfY29kZRgMIAEoBRIPCgdydW5uaW5nGA0gASgIEgwKBGtpbmQYDiABKAkSNgoDc3NoGA8gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hNZXRhZGF0YSLbAQoTVGVybWluYWxTc2hNZXRhZGF0YRIPCgdob3N0X2lkGAEgASgJEhEKCWhvc3RfbmFtZRgCIAEoCRIQCgh1c2VybmFtZRgDIAEoCRIMCgRob3N0GAQgASgJEgwKBHBvcnQYBSABKA0SEQoJYXV0aF90eXBlGAYgASgJEg4KBnN0YXR1cxgHIAEoCRIZChFyZWNvbm5lY3RfYXR0ZW1wdBgIIAEoDRIeChZyZWNvbm5lY3RfbWF4X2F0dGVtcHRzGAkgASgNEhQKDHNmdHBfZW5hYmxlZBgKIAEoCCKRAwoLU2Z0cFJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRIPCgd3b3JrZGlyGAQgASgJEhIKCmxvY2FsX3BhdGgYBSABKAkSEwoLcmVtb3RlX3BhdGgYBiABKAkSEQoJZnJvbV9wYXRoGAcgASgJEg8KB3RvX3BhdGgYCCABKAkSEQoJZGlyZWN0aW9uGAkgASgJEhMKC3RhcmdldF9wYXRoGAogASgJEhEKCXJlY3Vyc2l2ZRgLIAEoCBIRCglvdmVyd3JpdGUYDCABKAgSDwoHY29udGVudBgNIAEoCRIOCgZvZmZzZXQYDiABKAQSEQoJbWF4X2J5dGVzGA8gASgEEhMKC3N0cmljdF91dGY4GBAgASgIEhYKDmV4cGVjdGVkX210aW1lGBEgASgEEhsKE2V4cGVjdGVkX3NpemVfYnl0ZXMYEiABKAQSGgoSY3JlYXRlX3BhcmVudF9kaXJzGBMgASgIIlgKCVNmdHBFbnRyeRIMCgRwYXRoGAEgASgJEgwKBG5hbWUYAiABKAkSDAoEa2luZBgDIAEoCRISCgpzaXplX2J5dGVzGAQgASgEEg0KBW10aW1lGAUgASgEIvIBCgxTZnRwVHJhbnNmZXISCgoCaWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIRCglkaXJlY3Rpb24YAyABKAkSDgoGc3RhdHVzGAQgASgJEhMKC3NvdXJjZV9wYXRoGAUgASgJEhMKC3RhcmdldF9wYXRoGAYgASgJEhQKDGN1cnJlbnRfcGF0aBgHIAEoCRISCgpieXRlc19kb25lGAggASgEEhMKC2J5dGVzX3RvdGFsGAkgASgEEhIKCmZpbGVzX2RvbmUYCiABKA0SEwoLZmlsZXNfdG90YWwYCyABKA0SDQoFZXJyb3IYDCABKAkisAIKDFNmdHBSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSDAoEcGF0aBgCIAEoCRIwCgdlbnRyaWVzGAMgAygLMh8ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cEVudHJ5Ei4KBWVudHJ5GAQgASgLMh8ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cEVudHJ5Eg4KBmV4aXN0cxgFIAEoCBI0Cgh0cmFuc2ZlchgGIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBUcmFuc2ZlchIPCgdjb250ZW50GAcgASgJEg4KBm9mZnNldBgIIAEoBBISCgpieXRlc19yZWFkGAkgASgEEhIKCnNpemVfYnl0ZXMYCiABKAQSEQoJdHJ1bmNhdGVkGAsgASgIIk8KCVNmdHBFdmVudBIMCgRraW5kGAEgASgJEjQKCHRyYW5zZmVyGAIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cFRyYW5zZmVyIsEBChFUZXJtaW5hbFNzaFByb21wdBIKCgJpZBgBIAEoCRIMCgRraW5kGAIgASgJEg8KB2hvc3RfaWQYAyABKAkSEQoJaG9zdF9uYW1lGAQgASgJEgwKBGhvc3QYBSABKAkSDAoEcG9ydBgGIAEoDRIPCgdtZXNzYWdlGAcgASgJEhoKEmZpbmdlcnByaW50X3NoYTI1NhgIIAEoCRIQCghrZXlfdHlwZRgJIAEoCRITCgthbnN3ZXJfZWNobxgKIAEoCCJBChNUZXJtaW5hbFNoZWxsT3B0aW9uEgoKAmlkGAEgASgJEg0KBWxhYmVsGAIgASgJEg8KB2NvbW1hbmQYAyABKAkigAEKDlRlcm1pbmFsU3NoVGFiEgoKAmlkGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRIMCgRraW5kGAQgASgJEhIKCmNyZWF0ZWRfYXQYBSABKAQSEgoKdXBkYXRlZF9hdBgGIAEoBCJ/ChdUZXJtaW5hbFNzaFRhYnNTbmFwc2hvdBIYChBwcm9qZWN0X3BhdGhfa2V5GAEgASgJEjIKBHRhYnMYAiADKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaFRhYhIQCghyZXZpc2lvbhgEIAEoBEoECAMQBCL9AQoXVGVybWluYWxTc2hMb2NhbEZvcndhcmQSCgoCaWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEhIKCmxvY2FsX2hvc3QYBCABKAkSEgoKbG9jYWxfcG9ydBgFIAEoDRIPCgdhZGRyZXNzGAYgASgJEhMKC3JlbW90ZV9ob3N0GAcgASgJEhMKC3JlbW90ZV9wb3J0GAggASgNEg4KBnN0YXR1cxgJIAEoCRISCgpjcmVhdGVkX2F0GAogASgEEhIKCnVwZGF0ZWRfYXQYCyABKAQSDQoFZXJyb3IYDCABKAkidQogVGVybWluYWxTc2hMb2NhbEZvcndhcmRzU25hcHNob3QSPwoIZm9yd2FyZHMYASADKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaExvY2FsRm9yd2FyZBIQCghyZXZpc2lvbhgCIAEoBCJ/Ch1UZXJtaW5hbFNzaExvY2FsRm9yd2FyZEFjdGlvbhI+Cgdmb3J3YXJkGAEgASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hMb2NhbEZvcndhcmQSEAoIcmV2aXNpb24YAiABKAQSDAoEa2luZBgDIAEoCSKnBQoQVGVybWluYWxSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSNwoIc2Vzc2lvbnMYAiADKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNlc3Npb24SNgoHc2Vzc2lvbhgDIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhIOCgZvdXRwdXQYBCABKAwSEQoJdHJ1bmNhdGVkGAUgASgIEkAKDXNoZWxsX29wdGlvbnMYBiADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNoZWxsT3B0aW9uEhUKDWRlZmF1bHRfc2hlbGwYByABKAkSGwoTb3V0cHV0X3N0YXJ0X29mZnNldBgIIAEoBBIZChFvdXRwdXRfZW5kX29mZnNldBgJIAEoBBI7Cgpzc2hfcHJvbXB0GAogASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hQcm9tcHQSEgoKbGF0ZW5jeV9tcxgLIAEoDRI/Cghzc2hfdGFicxgMIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoVGFic1NuYXBzaG90ElIKEnNzaF9sb2NhbF9mb3J3YXJkcxgNIAEoCzI2LmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoTG9jYWxGb3J3YXJkc1NuYXBzaG90Ek4KEXNzaF9sb2NhbF9mb3J3YXJkGA4gASgLMjMubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hMb2NhbEZvcndhcmRBY3Rpb24SKAogc3NoX2xvY2FsX2ZvcndhcmRfcG9ydF9hdmFpbGFibGUYDyABKAgi2gIKDVRlcm1pbmFsRXZlbnQSDAoEa2luZBgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAyABKAkSNgoHc2Vzc2lvbhgEIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhIMCgRkYXRhGAUgASgMEhsKE291dHB1dF9zdGFydF9vZmZzZXQYBiABKAQSGQoRb3V0cHV0X2VuZF9vZmZzZXQYByABKAQSPwoIc3NoX3RhYnMYCCABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaFRhYnNTbmFwc2hvdBJOChFzc2hfbG9jYWxfZm9yd2FyZBgJIAEoCzIzLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoTG9jYWxGb3J3YXJkQWN0aW9uIrICChNUZXJtaW5hbFN0cmVhbUZyYW1lEgwKBGtpbmQYASABKAkSEQoJc3RyZWFtX2lkGAIgASgJEhIKCnNlc3Npb25faWQYAyABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgEIAEoCRILCgNzZXEYBSABKAQSFAoMc3RhcnRfb2Zmc2V0GAYgASgEEhIKCmVuZF9vZmZzZXQYByABKAQSDAoEY29scxgIIAEoDRIMCgRyb3dzGAkgASgNEhEKCW1heF9ieXRlcxgKIAEoDRIRCgl0cnVuY2F0ZWQYCyABKAgSDQoFZXJyb3IYDCABKAkSNgoHc2Vzc2lvbhgNIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhIMCgRkYXRhGA4gASgMIkAKCkdpdFJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEg8KB3dvcmtkaXIYAiABKAkSEQoJYXJnc19qc29uGAMgASgJIjIKC0dpdFJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRITCgtyZXN1bHRfanNvbhgCIAEoCSL0AgoLQ2hhdFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSPwoOc2VsZWN0ZWRfbW9kZWwYAyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0U2VsZWN0ZWRNb2RlbBIWCg5leGVjdXRpb25fbW9kZRgEIAEoCRIPCgd3b3JrZGlyGAUgASgJEj4KDnVwbG9hZGVkX2ZpbGVzGAcgAygLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFVwbG9hZGVkRmlsZRIZChFjbGllbnRfcmVxdWVzdF9pZBgIIAEoCRJDChBydW50aW1lX2NvbnRyb2xzGAkgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFJ1bnRpbWVDb250cm9scxIUCgxxdWV1ZV9wb2xpY3kYCiABKAlKBAgGEAdSFXNlbGVjdGVkX3N5c3RlbV90b29scyKKAQoOQ2hhdE1lc3NhZ2VSZWYSFQoNc2VnbWVudF9pbmRleBgBIAEoBRIVCg1tZXNzYWdlX2luZGV4GAIgASgFEhIKCnNlZ21lbnRfaWQYAyABKAkSEgoKbWVzc2FnZV9pZBgEIAEoCRIMCgRyb2xlGAUgASgJEhQKDGNvbnRlbnRfaGFzaBgGIAEoCSI8ChFDYW5jZWxDaGF0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDgoGcnVuX2lkGAIgASgJIs8BChJDaGF0Q29tbWFuZFJlcXVlc3QSDAoEdHlwZRgBIAEoCRIyCgdyZXF1ZXN0GAIgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFJlcXVlc3QSPgoQYmFzZV9tZXNzYWdlX3JlZhgDIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRNZXNzYWdlUmVmEjcKBmNhbmNlbBgEIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNhbmNlbENoYXRSZXF1ZXN0IrgBChBDaGF0UXVldWVSZXF1ZXN0Eg4KBmFjdGlvbhgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSDwoHaXRlbV9pZBgDIAEoCRIRCglkaXJlY3Rpb24YBCABKAkSEAoIcmV2aXNpb24YBSABKAQSEgoKZHJhZnRfanNvbhgGIAEoCRIbChN1cGxvYWRlZF9maWxlc19qc29uGAcgASgJEhQKDHJlcXVlc3RfanNvbhgIIAEoCSKGAQoRQ2hhdFF1ZXVlUmVzcG9uc2USEAoIYWNjZXB0ZWQYASABKAgSDwoHbWVzc2FnZRgCIAEoCRIVCg1zbmFwc2hvdF9qc29uGAMgASgJEhEKCWl0ZW1fanNvbhgEIAEoCRISCgplcnJvcl9jb2RlGAUgASgJEhAKCHJldmlzaW9uGAYgASgEIlIKDkNoYXRRdWV1ZUV2ZW50EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1zbmFwc2hvdF9qc29uGAIgASgJEhAKCHJldmlzaW9uGAMgASgEIoUCCglDaGF0RXZlbnQSOwoEdHlwZRgBIAEoDjItLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRFdmVudC5DaGF0RXZlbnRUeXBlEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRIMCgRkYXRhGAMgASgJIpMBCg1DaGF0RXZlbnRUeXBlEgkKBVRPS0VOEAASDAoIVEhJTktJTkcQARINCglUT09MX0NBTEwQAhIPCgtUT09MX1JFU1VMVBADEggKBERPTkUQBBIJCgVFUlJPUhAFEg8KC1RPT0xfU1RBVFVTEAYSEQoNSE9TVEVEX1NFQVJDSBAHEhAKDFVTRVJfTUVTU0FHRRAIIrwBChBDaGF0Q29udHJvbEV2ZW50EhIKCnJlcXVlc3RfaWQYASABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAiABKAkSFwoPY29udmVyc2F0aW9uX2lkGAMgASgJEhEKCXJ1bl9lcG9jaBgEIAEoAxIMCgR0eXBlGAUgASgJEg0KBXN0YXRlGAYgASgJEhIKCmVycm9yX2NvZGUYByABKAkSDwoHbWVzc2FnZRgIIAEoCRILCgNzZXEYCSABKAMi/AEKE0NoYXRSdW50aW1lU25hcHNob3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgDIAEoCRIRCgl3b3JrZXJfaWQYBCABKAkSDQoFc3RhdGUYBSABKAkSCwoDY3dkGAYgASgJEhIKCnVwZGF0ZWRfYXQYByABKAMSEAoIcmV2aXNpb24YCCABKAMSFAoMZW50cmllc19qc29uGAkgASgJEhMKC3Rvb2xfc3RhdHVzGAogASgJEiEKGXRvb2xfc3RhdHVzX2lzX2NvbXBhY3Rpb24YCyABKAgi6gEKElJ1bnRpbWVTdGF0dXNFdmVudBIRCgl3b3JrZXJfaWQYASABKAkSDQoFc3RhdGUYAiABKAkSDwoHdmlzaWJsZRgDIAEoCBIYChBhY3RpdmVfcnVuX2NvdW50GAQgASgNEhEKCXRpbWVzdGFtcBgFIAEoAxI4CgthY3RpdmVfcnVucxgGIAMoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRSdW5SZXBvcnQSOgoNZmluaXNoZWRfcnVucxgHIAMoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRSdW5SZXBvcnQigAEKDUNoYXRSdW5SZXBvcnQSDgoGcnVuX2lkGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRINCgVzdGF0ZRgDIAEoCRISCgplcnJvcl9jb2RlGAQgASgJEg8KB21lc3NhZ2UYBSABKAkSEgoKdXBkYXRlZF9hdBgGIAEoAyJHChFDcm9uTWFuYWdlUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSDwoHdGFza19pZBgCIAEoCRIRCgl0YXNrX2pzb24YAyABKAkiOQoSQ3Jvbk1hbmFnZVJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRITCgtyZXN1bHRfanNvbhgCIAEoCSJVChJIaXN0b3J5TGlzdFJlcXVlc3QSDAoEcGFnZRgBIAEoBRIRCglwYWdlX3NpemUYAiABKAUSCwoDY3dkGAMgASgJEhEKCWN3ZF9lbXB0eRgEIAEoCCJsChNIaXN0b3J5TGlzdFJlc3BvbnNlEkAKDWNvbnZlcnNhdGlvbnMYASADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5EhMKC3RvdGFsX2NvdW50GAIgASgFIooCChNDb252ZXJzYXRpb25TdW1tYXJ5EgoKAmlkGAEgASgJEg0KBXRpdGxlGAIgASgJEhIKCmNyZWF0ZWRfYXQYAyABKAMSEgoKdXBkYXRlZF9hdBgEIAEoAxIVCg1tZXNzYWdlX2NvdW50GAUgASgFEhMKC3Byb3ZpZGVyX2lkGAYgASgJEg0KBW1vZGVsGAcgASgJEhIKCnNlc3Npb25faWQYCCABKAkSCwoDY3dkGAkgASgJEhEKCWlzX3Bpbm5lZBgKIAEoCBIRCglwaW5uZWRfYXQYCyABKAMSEQoJaXNfc2hhcmVkGAwgASgIEhsKE3NlbGVjdGVkX21vZGVsX2pzb24YDSABKAkiQgoRSGlzdG9yeUdldFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEhQKDG1heF9tZXNzYWdlcxgCIAEoBSLUAQoSSGlzdG9yeUdldFJlc3BvbnNlEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1tZXNzYWdlc19qc29uGAIgASgJEhsKE3RvdGFsX21lc3NhZ2VfY291bnQYAyABKAUSHgoWcmV0dXJuZWRfbWVzc2FnZV9jb3VudBgEIAEoBRIQCghoYXNfbW9yZRgFIAEoCBI/Cgxjb252ZXJzYXRpb24YBiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5IoUBChRIaXN0b3J5UHJlZml4UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFAoMbWF4X21lc3NhZ2VzGAIgASgFEj4KEGJhc2VfbWVzc2FnZV9yZWYYAyABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0TWVzc2FnZVJlZiLXAQoVSGlzdG9yeVByZWZpeFJlc3BvbnNlEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1tZXNzYWdlc19qc29uGAIgASgJEhsKE3RvdGFsX21lc3NhZ2VfY291bnQYAyABKAUSHgoWcmV0dXJuZWRfbWVzc2FnZV9jb3VudBgEIAEoBRIQCghoYXNfbW9yZRgFIAEoCBI/Cgxjb252ZXJzYXRpb24YBiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5Ij4KFEhpc3RvcnlSZW5hbWVSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRINCgV0aXRsZRgCIAEoCSJYChVIaXN0b3J5UmVuYW1lUmVzcG9uc2USPwoMY29udmVyc2F0aW9uGAEgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeSJvChRIaXN0b3J5QnJhbmNoUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSPgoQYmFzZV9tZXNzYWdlX3JlZhgCIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRNZXNzYWdlUmVmIlgKFUhpc3RvcnlCcmFuY2hSZXNwb25zZRI/Cgxjb252ZXJzYXRpb24YASABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5Ij8KEUhpc3RvcnlQaW5SZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIRCglpc19waW5uZWQYAiABKAgiVQoSSGlzdG9yeVBpblJlc3BvbnNlEj8KDGNvbnZlcnNhdGlvbhgBIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNvbnZlcnNhdGlvblN1bW1hcnkikgEKEkhpc3RvcnlTaGFyZVN0YXR1cxIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBINCgV0b2tlbhgDIAEoCRISCgpjcmVhdGVkX2F0GAQgASgDEhIKCnVwZGF0ZWRfYXQYBSABKAMSGwoTcmVkYWN0X3Rvb2xfY29udGVudBgGIAEoCCIxChZIaXN0b3J5U2hhcmVHZXRSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCSJSChdIaXN0b3J5U2hhcmVHZXRSZXNwb25zZRI3CgVzaGFyZRgBIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVN0YXR1cyJ8ChZIaXN0b3J5U2hhcmVTZXRSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIPCgdlbmFibGVkGAIgASgIEiAKE3JlZGFjdF90b29sX2NvbnRlbnQYAyABKAhIAIgBAUIWChRfcmVkYWN0X3Rvb2xfY29udGVudCJSChdIaXN0b3J5U2hhcmVTZXRSZXNwb25zZRI3CgVzaGFyZRgBIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVN0YXR1cyIrChpIaXN0b3J5U2hhcmVSZXNvbHZlUmVxdWVzdBINCgV0b2tlbhgBIAEoCSLIAQobSGlzdG9yeVNoYXJlUmVzb2x2ZVJlc3BvbnNlEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1tZXNzYWdlc19qc29uGAIgASgJEhsKE3RvdGFsX21lc3NhZ2VfY291bnQYAyABKAUSPwoMY29udmVyc2F0aW9uGAQgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeRIbChNyZWRhY3RfdG9vbF9jb250ZW50GAUgASgIIhgKFkhpc3RvcnlXb3JrZGlyc1JlcXVlc3QiVQoVSGlzdG9yeVdvcmtkaXJTdW1tYXJ5EgwKBHBhdGgYASABKAkSGgoSY29udmVyc2F0aW9uX2NvdW50GAIgASgFEhIKCnVwZGF0ZWRfYXQYAyABKAMiWAoXSGlzdG9yeVdvcmtkaXJzUmVzcG9uc2USPQoId29ya2RpcnMYASADKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5V29ya2RpclN1bW1hcnkiLwoUSGlzdG9yeURlbGV0ZVJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJIhcKFUhpc3RvcnlEZWxldGVSZXNwb25zZSJ6ChBIaXN0b3J5U3luY0V2ZW50EgwKBGtpbmQYASABKAkSPwoMY29udmVyc2F0aW9uGAIgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeRIXCg9jb252ZXJzYXRpb25faWQYAyABKAkiFQoTUHJvdmlkZXJMaXN0UmVxdWVzdCIuChRQcm92aWRlckxpc3RSZXNwb25zZRIWCg5wcm92aWRlcnNfanNvbhgBIAEoCSIUChJTZXR0aW5nc0dldFJlcXVlc3QiLAoTU2V0dGluZ3NHZXRSZXNwb25zZRIVCg1zZXR0aW5nc19qc29uGAEgASgJIi4KFVNldHRpbmdzVXBkYXRlUmVxdWVzdBIVCg1zZXR0aW5nc19qc29uGAEgASgJIjsKFlNldHRpbmdzVXBkYXRlUmVzcG9uc2USEAoIYWNjZXB0ZWQYASABKAgSDwoHbWVzc2FnZRgCIAEoCSI+CiBTZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVxdWVzdBIMCgRob3N0GAEgASgJEgwKBHBvcnQYAiABKA0iNAohU2V0dGluZ3NSZXNldFNzaEtub3duSG9zdFJlc3BvbnNlEg8KB2RlbGV0ZWQYASABKA0iKgoRU2V0dGluZ3NTeW5jRXZlbnQSFQoNc2V0dGluZ3NfanNvbhgBIAEoCSIXChVTa2lsbEZpbGVzTGlzdFJlcXVlc3QiTAoWU2tpbGxGaWxlc0xpc3RSZXNwb25zZRIQCghyb290X2RpchgBIAEoCRINCgVwYXRocxgCIAMoCRIRCgl0cnVuY2F0ZWQYAyABKAgiKAoYU2tpbGxNZXRhZGF0YVJlYWRSZXF1ZXN0EgwKBHBhdGgYASABKAkiPgoZU2tpbGxNZXRhZGF0YVJlYWRSZXNwb25zZRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJIkQKFFNraWxsVGV4dFJlYWRSZXF1ZXN0EgwKBHBhdGgYASABKAkSDgoGb2Zmc2V0GAIgASgNEg4KBmxlbmd0aBgDIAEoDSI7ChVTa2lsbFRleHRSZWFkUmVzcG9uc2USDwoHY29udGVudBgBIAEoCRIRCgl0cnVuY2F0ZWQYAiABKAgiKgoSU2tpbGxNYW5hZ2VSZXF1ZXN0EhQKDHBheWxvYWRfanNvbhgBIAEoCSIqChNTa2lsbE1hbmFnZVJlc3BvbnNlEhMKC3Jlc3VsdF9qc29uGAEgASgJIncKFkZpbGVNZW50aW9uTGlzdFJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRITCgttYXhfcmVzdWx0cxgCIAEoDRINCgVxdWVyeRgDIAEoCRIYCgtzaG93X2hpZGRlbhgEIAEoCEgAiAEBQg4KDF9zaG93X2hpZGRlbiI+ChBGaWxlTWVudGlvbkVudHJ5EgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCRIOCgZoaWRkZW4YAyABKAgiZQoXRmlsZU1lbnRpb25MaXN0UmVzcG9uc2USNwoHZW50cmllcxgBIAMoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZpbGVNZW50aW9uRW50cnkSEQoJdHJ1bmNhdGVkGAIgASgIIj8KBkZzUm9vdBIKCgJpZBgBIAEoCRIMCgRwYXRoGAIgASgJEgwKBGtpbmQYAyABKAkSDQoFbGFiZWwYBCABKAkiEAoORnNSb290c1JlcXVlc3QiPgoPRnNSb290c1Jlc3BvbnNlEisKBXJvb3RzGAEgAygLMhwubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSb290IjYKEUZzTGlzdERpcnNSZXF1ZXN0EgwKBHBhdGgYASABKAkSEwoLbWF4X3Jlc3VsdHMYAiABKA0iKAoKRnNEaXJFbnRyeRIMCgRwYXRoGAEgASgJEgwKBG5hbWUYAiABKAkiaAoSRnNMaXN0RGlyc1Jlc3BvbnNlEgwKBHBhdGgYASABKAkSMQoHZW50cmllcxgCIAMoCzIgLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzRGlyRW50cnkSEQoJdHJ1bmNhdGVkGAMgASgIIjwKHEZzQ3JlYXRlUHJvamVjdEZvbGRlclJlcXVlc3QSDgoGcGFyZW50GAEgASgJEgwKBG5hbWUYAiABKAkiLQodRnNDcmVhdGVQcm9qZWN0Rm9sZGVyUmVzcG9uc2USDAoEcGF0aBgBIAEoCSKMAQoNRnNMaXN0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkSDQoFZGVwdGgYAyABKA0SDgoGb2Zmc2V0GAQgASgNEhMKC21heF9yZXN1bHRzGAUgASgNEhgKC3Nob3dfaGlkZGVuGAYgASgISACIAQFCDgoMX3Nob3dfaGlkZGVuIjkKC0ZzTGlzdEVudHJ5EgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCRIOCgZoaWRkZW4YAyABKAgiuQEKDkZzTGlzdFJlc3BvbnNlEgwKBHBhdGgYASABKAkSEAoIaGFzX3BhdGgYAiABKAgSDQoFZGVwdGgYAyABKA0SDgoGb2Zmc2V0GAQgASgNEhMKC21heF9yZXN1bHRzGAUgASgNEg0KBXRvdGFsGAYgASgNEhAKCGhhc19tb3JlGAcgASgIEjIKB2VudHJpZXMYCCADKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3RFbnRyeSI6ChlGc1JlYWRFZGl0YWJsZVRleHRSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSDAoEcGF0aBgCIAEoCSKMAQoaRnNSZWFkRWRpdGFibGVUZXh0UmVzcG9uc2USDAoEcGF0aBgBIAEoCRIPCgdjb250ZW50GAIgASgJEhAKCG10aW1lX21zGAMgASgEEhQKDGNvbnRlbnRfaGFzaBgEIAEoCRISCgpzaXplX2J5dGVzGAUgASgEEhMKC3RvdGFsX2xpbmVzGAYgASgEIjwKG0ZzUmVhZFdvcmtzcGFjZUltYWdlUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkiiQEKHEZzUmVhZFdvcmtzcGFjZUltYWdlUmVzcG9uc2USDAoEcGF0aBgBIAEoCRIRCgltaW1lX3R5cGUYAiABKAkSDAoEZGF0YRgDIAEoCRISCgpzaXplX2J5dGVzGAQgASgEEhAKCG10aW1lX21zGAUgASgEEhQKDGNvbnRlbnRfaGFzaBgGIAEoCSLbAQoTQ2hhdEZpbGVPcGVuUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHd29ya2RpchgCIAEoCRIMCgRwYXRoGAMgASgJEg4KBnNvdXJjZRgEIAEoCRIRCgRsaW5lGAUgASgNSACIAQESFQoIZW5kX2xpbmUYBiABKA1IAYgBARITCgZjb2x1bW4YByABKA1IAogBARIcChRvcGVuX2luX2ZpbGVfbWFuYWdlchgIIAEoCEIHCgVfbGluZUILCglfZW5kX2xpbmVCCQoHX2NvbHVtbiLOAQoUQ2hhdEZpbGVPcGVuUmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEgwKBGtpbmQYAiABKAkSDwoHd29ya2RpchgDIAEoCRIMCgRwYXRoGAQgASgJEhEKBGxpbmUYBSABKA1IAIgBARIVCghlbmRfbGluZRgGIAEoDUgBiAEBEhMKBmNvbHVtbhgHIAEoDUgCiAEBEhkKEW91dHNpZGVfd29ya3NwYWNlGAggASgIQgcKBV9saW5lQgsKCV9lbmRfbGluZUIJCgdfY29sdW1uIs4BChJGc1dyaXRlVGV4dFJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIMCgRwYXRoGAIgASgJEg8KB2NvbnRlbnQYAyABKAkSDAoEbW9kZRgEIAEoCRIZChFleHBlY3RlZF9tdGltZV9tcxgFIAEoBBIdChVleHBlY3RlZF9jb250ZW50X2hhc2gYBiABKAkSHQoVaGFzX2V4cGVjdGVkX210aW1lX21zGAcgASgIEiEKGWhhc19leHBlY3RlZF9jb250ZW50X2hhc2gYCCABKAginQEKE0ZzV3JpdGVUZXh0UmVzcG9uc2USDAoEcGF0aBgBIAEoCRIMCgRtb2RlGAIgASgJEhYKDmV4aXN0ZWRfYmVmb3JlGAMgASgIEhUKDWJ5dGVzX3dyaXR0ZW4YBCABKAQSEAoIbXRpbWVfbXMYBSABKAQSFAoMY29udGVudF9oYXNoGAYgASgJEhMKC3RvdGFsX2xpbmVzGAcgASgEIjMKEkZzQ3JlYXRlRGlyUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkiMQoTRnNDcmVhdGVEaXJSZXNwb25zZRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkiRgoPRnNSZW5hbWVSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSEQoJZnJvbV9wYXRoGAIgASgJEg8KB3RvX3BhdGgYAyABKAkiQQoQRnNSZW5hbWVSZXNwb25zZRIRCglmcm9tX3BhdGgYASABKAkSDAoEcGF0aBgCIAEoCRIMCgRraW5kGAMgASgJIjAKD0ZzRGVsZXRlUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkiLgoQRnNEZWxldGVSZXNwb25zZRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkiIAoLUGluZ1JlcXVlc3QSEQoJdGltZXN0YW1wGAEgASgDIiEKDFBvbmdSZXNwb25zZRIRCgl0aW1lc3RhbXAYASABKAMiLgoNRXJyb3JSZXNwb25zZRIMCgRjb2RlGAEgASgFEg8KB21lc3NhZ2UYAiABKAkivgEKFVByb3ZpZGVyTW9kZWxzUmVxdWVzdBIVCg1wcm92aWRlcl90eXBlGAEgASgJEhAKCGJhc2VfdXJsGAIgASgJEg8KB2FwaV9rZXkYAyABKAkSGAoQdXNlX3N5c3RlbV9wcm94eRgEIAEoCBISCgptb2RlbHNfdXJsGAUgASgJEhMKC3Byb3ZpZGVyX2lkGAYgASgJEhgKC2lzX2Z1bGxfdXJsGAcgASgISACIAQFCDgoMX2lzX2Z1bGxfdXJsIi0KFlByb3ZpZGVyTW9kZWxzUmVzcG9uc2USEwoLbW9kZWxzX2pzb24YASABKAkiUQoUUHJvdmlkZXJVc2FnZVJlcXVlc3QSEwoLcHJvdmlkZXJfaWQYASABKAkSDwoHcmVmcmVzaBgCIAEoCBITCgtjb25maWdfanNvbhgDIAEoCSIsChVQcm92aWRlclVzYWdlUmVzcG9uc2USEwoLcmVzdWx0X2pzb24YASABKAkiiAEKEENoYXRJbmdyZXNzQmF0Y2gSDgoGcnVuX2lkGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRIRCglmaXJzdF9zZXEYAyABKAQSOAoHcmVjb3JkcxgEIAMoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzUmVjb3JkIpoCChFDaGF0SW5ncmVzc1JlY29yZBI3CgVkZWx0YRgBIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzRGVsdGFIABJBCgpjaGVja3BvaW50GAIgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NDaGVja3BvaW50SAASPQoIdGVybWluYWwYAyABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc1Rlcm1pbmFsSAASPwoJaGVhcnRiZWF0GAQgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NIZWFydGJlYXRIAEIJCgdwYXlsb2FkIjkKEENoYXRJbmdyZXNzRGVsdGESEgoKZXZlbnRfanNvbhgBIAEoCRIRCgl3b3JrZXJfaWQYAiABKAkiKgoUQ2hhdEluZ3Jlc3NIZWFydGJlYXQSEgoKdXBkYXRlZF9hdBgBIAEoAyLEAQoVQ2hhdEluZ3Jlc3NDaGVja3BvaW50EhoKEmNvdmVyc190aHJvdWdoX3NlcRgBIAEoBBIQCghyZXZpc2lvbhgCIAEoBBIdChVjb21wcmVzc2VkX3Byb2plY3Rpb24YAyABKAwSGgoSdW5jb21wcmVzc2VkX2J5dGVzGAQgASgEEg4KBnNoYTI1NhgFIAEoCRIYChBjb250ZW50X2NvbXBsZXRlGAYgASgIEhgKEGhpc3RvcnlfcmVxdWlyZWQYByABKAgi/AEKE0NoYXRJbmdyZXNzVGVybWluYWwSGgoSY292ZXJzX3Rocm91Z2hfc2VxGAEgASgEEhAKCHJldmlzaW9uGAIgASgEEh0KFWNvbXByZXNzZWRfcHJvamVjdGlvbhgDIAEoDBIaChJ1bmNvbXByZXNzZWRfYnl0ZXMYBCABKAQSDgoGc2hhMjU2GAUgASgJEhgKEGNvbnRlbnRfY29tcGxldGUYBiABKAgSGAoQaGlzdG9yeV9yZXF1aXJlZBgHIAEoCBINCgVzdGF0ZRgIIAEoCRISCgplcnJvcl9jb2RlGAkgASgJEhUKDWVycm9yX21lc3NhZ2UYCiABKAkiTQoRQ2hhdEluZ3Jlc3NSZXN1bWUSOAoEcnVucxgBIAMoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzUnVuUmVzdW1lItUBChRDaGF0SW5ncmVzc1J1blJlc3VtZRIOCgZydW5faWQYASABKAkSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEhcKD3JlcGxheV9mcm9tX3NlcRgDIAEoBBIaChJyZXBsYXlfdGhyb3VnaF9zZXEYBCABKAQSEAoIbmV4dF9zZXEYBSABKAQSHQoVbGF0ZXN0X2NoZWNrcG9pbnRfc2VxGAYgASgEEhQKDHRlcm1pbmFsX3NlcRgHIAEoBBIYChB0ZXJtaW5hbF9wZW5kaW5nGAggASgIIs4BChNDaGF0SW5ncmVzc0ZyYWdtZW50Eg4KBnJ1bl9pZBgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSEgoKc291cmNlX3NlcRgDIAEoBBIWCg5mcmFnbWVudF9pbmRleBgEIAEoDRIWCg5mcmFnbWVudF9jb3VudBgFIAEoDRIcChRlbmNvZGVkX3JlY29yZF9jaHVuaxgGIAEoDBIcChRlbmNvZGVkX3JlY29yZF9ieXRlcxgHIAEoBBIOCgZzaGEyNTYYCCABKAki3AIKDkNoYXRJbmdyZXNzQWNrEg4KBnJ1bl9pZBgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSGQoRY29tbWl0dGVkX3Rocm91Z2gYAyABKAQSFQoNZXhwZWN0ZWRfbmV4dBgEIAEoBBI7CgZhY3Rpb24YBSABKA4yKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc0Fjay5BY3Rpb24SGgoSdGVybWluYWxfY29tbWl0dGVkGAYgASgIEhIKCmVycm9yX2NvZGUYByABKAkSFQoNZXJyb3JfbWVzc2FnZRgIIAEoCSJrCgZBY3Rpb24SFgoSQUNUSU9OX1VOU1BFQ0lGSUVEEAASDAoIQ09OVElOVUUQARIYChRSRVBMQVlfRlJPTV9FWFBFQ1RFRBACEhMKD1NFTkRfQ0hFQ0tQT0lOVBADEgwKCFJFSkVDVEVEEAQiPAoUSGlzdG9yeVNldEN3ZFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEgsKA2N3ZBgCIAEoCSJYChVIaXN0b3J5U2V0Q3dkUmVzcG9uc2USPwoMY29udmVyc2F0aW9uGAEgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeSJmChdXb3Jrc3BhY2VSb290R3JhbnREcmFmdBIPCgJpZBgBIAEoCUgAiAEBEg0KBWFsaWFzGAIgASgJEhQKDGRpc3BsYXlfcGF0aBgDIAEoCRIOCgZhY2Nlc3MYBCABKAlCBQoDX2lkItIBChJXb3Jrc3BhY2VSb290R3JhbnQSCgoCaWQYASABKAkSEgoKcHJvamVjdF9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEg0KBWFsaWFzGAQgASgJEhQKDGRpc3BsYXlfcGF0aBgFIAEoCRIWCg5jYW5vbmljYWxfcGF0aBgGIAEoCRIOCgZhY2Nlc3MYByABKAkSDQoFc3RhdGUYCCABKAkSEgoKY3JlYXRlZF9hdBgJIAEoAxISCgp1cGRhdGVkX2F0GAogASgDIpUBChpXb3Jrc3BhY2VSb290R3JhbnRzUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSEgoKcHJvamVjdF9pZBgCIAEoCRIUCgxwcm9qZWN0X3BhdGgYAyABKAkSPQoGZ3JhbnRzGAQgAygLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuV29ya3NwYWNlUm9vdEdyYW50RHJhZnQiVwobV29ya3NwYWNlUm9vdEdyYW50c1Jlc3BvbnNlEjgKBmdyYW50cxgBIAMoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLldvcmtzcGFjZVJvb3RHcmFudCI8ChdDaGVja3BvaW50RXhwZWN0ZWRFbnRyeRILCgNrZXkYASABKAkSFAoMY3VycmVudF9oYXNoGAIgASgJIqkBChFDaGVja3BvaW50UmVxdWVzdBIOCgZhY3Rpb24YASABKAkSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEhAKCHR1cm5fc2VxGAMgASgEEhgKEGF1dGhvcml6ZWRfcm9vdHMYBCADKAkSPwoIZXhwZWN0ZWQYBSADKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGVja3BvaW50RXhwZWN0ZWRFbnRyeSI5ChJDaGVja3BvaW50UmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEhMKC3Jlc3VsdF9qc29uGAIgASgJKuUBChhJbXBvcnREaXJlY3RvcnlPcGVyYXRpb24SKgomSU1QT1JUX0RJUkVDVE9SWV9PUEVSQVRJT05fVU5TUEVDSUZJRUQQABIkCiBJTVBPUlRfRElSRUNUT1JZX09QRVJBVElPTl9TVEFSVBABEioKJklNUE9SVF9ESVJFQ1RPUllfT1BFUkFUSU9OX1dSSVRFX0NIVU5LEAISJQohSU1QT1JUX0RJUkVDVE9SWV9PUEVSQVRJT05fQ09NTUlUEAMSJAogSU1QT1JUX0RJUkVDVE9SWV9PUEVSQVRJT05fQUJPUlQQBCrGBAoPVHVubmVsRnJhbWVLaW5kEiEKHVRVTk5FTF9GUkFNRV9LSU5EX1VOU1BFQ0lGSUVEEAASKAokVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVFVRVNUX1NUQVJUEAESJwojVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVFVRVNUX0JPRFkQAhImCiJUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFUVVFU1RfRU5EEAMSKQolVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVNQT05TRV9TVEFSVBAEEigKJFRVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVTUE9OU0VfQk9EWRAFEicKI1RVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVTUE9OU0VfRU5EEAYSHQoZVFVOTkVMX0ZSQU1FX0tJTkRfV1NfRElBTBAHEiAKHFRVTk5FTF9GUkFNRV9LSU5EX1dTX0RJQUxfT0sQCBIjCh9UVU5ORUxfRlJBTUVfS0lORF9XU19ESUFMX0VSUk9SEAkSHgoaVFVOTkVMX0ZSQU1FX0tJTkRfV1NfRlJBTUUQChIeChpUVU5ORUxfRlJBTUVfS0lORF9XU19DTE9TRRALEhsKF1RVTk5FTF9GUkFNRV9LSU5EX0VSUk9SEAwSHAoYVFVOTkVMX0ZSQU1FX0tJTkRfQ0FOQ0VMEA0SGgoWVFVOTkVMX0ZSQU1FX0tJTkRfUElORxAOEhoKFlRVTk5FTF9GUkFNRV9LSU5EX1BPTkcQDyqBAQoTVHVubmVsV3NNZXNzYWdlVHlwZRImCiJUVU5ORUxfV1NfTUVTU0FHRV9UWVBFX1VOU1BFQ0lGSUVEEAASHwobVFVOTkVMX1dTX01FU1NBR0VfVFlQRV9URVhUEAESIQodVFVOTkVMX1dTX01FU1NBR0VfVFlQRV9CSU5BUlkQAkJAWj5naXRodWIuY29tL2xpdmVhZ2VudC9hZ2VudC1nYXRld2F5L2ludGVybmFsL3Byb3RvL3YyO2dhdGV3YXl2MmIGcHJvdG8z"); + fileDesc("ChZwcm90by92Mi9nYXRld2F5LnByb3RvEhRsaXZlYWdlbnQuZ2F0ZXdheS52MiLZHQoPR2F0ZXdheUVudmVsb3BlEhIKCnJlcXVlc3RfaWQYASABKAkSEQoJdGltZXN0YW1wGAIgASgDEkAKDGNoYXRfY29tbWFuZBgKIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRDb21tYW5kUmVxdWVzdEgAEj4KC2Nyb25fbWFuYWdlGBQgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuQ3Jvbk1hbmFnZVJlcXVlc3RIABJACgxoaXN0b3J5X2xpc3QYHiABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5TGlzdFJlcXVlc3RIABI+CgtoaXN0b3J5X2dldBgfIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlHZXRSZXF1ZXN0SAASRAoOaGlzdG9yeV9yZW5hbWUYICABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UmVuYW1lUmVxdWVzdEgAEkQKDmhpc3RvcnlfZGVsZXRlGCEgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeURlbGV0ZVJlcXVlc3RIABJECg5oaXN0b3J5X3ByZWZpeBgiIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlQcmVmaXhSZXF1ZXN0SAASPgoLaGlzdG9yeV9waW4YIyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UGluUmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfZ2V0GCQgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlR2V0UmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfc2V0GCUgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlU2V0UmVxdWVzdEgAElEKFWhpc3Rvcnlfc2hhcmVfcmVzb2x2ZRgmIAEoCzIwLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVJlc29sdmVSZXF1ZXN0SAASSAoQaGlzdG9yeV93b3JrZGlycxgnIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlXb3JrZGlyc1JlcXVlc3RIABJCCg1wcm92aWRlcl9saXN0GCggASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJMaXN0UmVxdWVzdEgAEkAKDHNldHRpbmdzX2dldBgpIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzR2V0UmVxdWVzdEgAEkYKD3NldHRpbmdzX3VwZGF0ZRgqIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzVXBkYXRlUmVxdWVzdEgAEkcKEHNraWxsX2ZpbGVzX2xpc3QYKyABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbEZpbGVzTGlzdFJlcXVlc3RIABJNChNza2lsbF9tZXRhZGF0YV9yZWFkGCwgASgLMi4ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxNZXRhZGF0YVJlYWRSZXF1ZXN0SAASRQoPc2tpbGxfdGV4dF9yZWFkGC0gASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxUZXh0UmVhZFJlcXVlc3RIABJJChFmaWxlX21lbnRpb25fbGlzdBguIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZpbGVNZW50aW9uTGlzdFJlcXVlc3RIABJRChV1cGxvYWRfcmVhZGFibGVfZmlsZXMYLyABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRSZWFkYWJsZUZpbGVzUmVxdWVzdEgAEjgKCGZzX3Jvb3RzGDAgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSb290c1JlcXVlc3RIABI/Cgxmc19saXN0X2RpcnMYMSABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3REaXJzUmVxdWVzdEgAEjEKBHBpbmcYMiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5QaW5nUmVxdWVzdEgAElMKFnVwbG9hZGVkX2ltYWdlX3ByZXZpZXcYMyABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRlZEltYWdlUHJldmlld1JlcXVlc3RIABJCCg1tZW1vcnlfbWFuYWdlGDQgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuTWVtb3J5TWFuYWdlUmVxdWVzdEgAEkAKDHNraWxsX21hbmFnZRg1IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsTWFuYWdlUmVxdWVzdEgAElYKGGZzX2NyZWF0ZV9wcm9qZWN0X2ZvbGRlchg2IAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlUHJvamVjdEZvbGRlclJlcXVlc3RIABJBChB0ZXJtaW5hbF9yZXF1ZXN0GDcgASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxSZXF1ZXN0SAASNgoHZnNfbGlzdBg4IAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzTGlzdFJlcXVlc3RIABJBCg1mc193cml0ZV90ZXh0GDkgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNXcml0ZVRleHRSZXF1ZXN0SAASQQoNZnNfY3JlYXRlX2Rpchg6IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlRGlyUmVxdWVzdEgAEjoKCWZzX3JlbmFtZRg7IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVuYW1lUmVxdWVzdEgAEjoKCWZzX2RlbGV0ZRg8IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzRGVsZXRlUmVxdWVzdEgAEjcKC2dpdF9yZXF1ZXN0GD0gASgLMiAubGl2ZWFnZW50LmdhdGV3YXkudjIuR2l0UmVxdWVzdEgAElAKFWZzX3JlYWRfZWRpdGFibGVfdGV4dBg+IAEoCzIvLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZEVkaXRhYmxlVGV4dFJlcXVlc3RIABJUChdmc19yZWFkX3dvcmtzcGFjZV9pbWFnZRg/IAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZFdvcmtzcGFjZUltYWdlUmVxdWVzdEgAEjkKDHNmdHBfcmVxdWVzdBhAIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBSZXF1ZXN0SAASRgoPcHJvdmlkZXJfbW9kZWxzGEEgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJNb2RlbHNSZXF1ZXN0SAASXwodc2V0dGluZ3NfcmVzZXRfc3NoX2tub3duX2hvc3QYSCABKAsyNi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVxdWVzdEgAEjwKCmNoYXRfcXVldWUYSSABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVSZXF1ZXN0SAASQAoQY2hhdF9pbmdyZXNzX2FjaxhLIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzQWNrSAASQQoMdHVubmVsX3N0YXRlGFAgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsU3RhdGVTbmFwc2hvdEgAEj8KD3R1bm5lbF9tdXRhdGlvbhhRIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbE11dGF0aW9uSAASOQoMdHVubmVsX2ZyYW1lGFIgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsRnJhbWVIABJGCg93b3Jrc3BhY2Vfd2F0Y2gYWiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Xb3Jrc3BhY2VXYXRjaFJlcXVlc3RIABJOChdtYW5hZ2VkX3Byb2Nlc3NfcmVxdWVzdBhbIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLk1hbmFnZWRQcm9jZXNzUmVxdWVzdEgAEkQKDmhpc3RvcnlfYnJhbmNoGFwgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeUJyYW5jaFJlcXVlc3RIABJECg5wcm92aWRlcl91c2FnZRhdIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLlByb3ZpZGVyVXNhZ2VSZXF1ZXN0SAASQwoOY2hhdF9maWxlX29wZW4YXiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0RmlsZU9wZW5SZXF1ZXN0SAASRQoPaGlzdG9yeV9zZXRfY3dkGF8gASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNldEN3ZFJlcXVlc3RIABJRChV3b3Jrc3BhY2Vfcm9vdF9ncmFudHMYYCABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Xb3Jrc3BhY2VSb290R3JhbnRzUmVxdWVzdEgAEkgKEGltcG9ydF9kaXJlY3RvcnkYYSABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5JbXBvcnREaXJlY3RvcnlSZXF1ZXN0SAASPQoKY2hlY2twb2ludBhiIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoZWNrcG9pbnRSZXF1ZXN0SABCCQoHcGF5bG9hZEoECEMQREoECEQQRUoECEUQRkoECEoQSyLVJgoNQWdlbnRFbnZlbG9wZRISCgpyZXF1ZXN0X2lkGAEgASgJEhEKCXRpbWVzdGFtcBgCIAEoAxI1CgpjaGF0X2V2ZW50GAogASgLMh8ubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEV2ZW50SAASRAoQY3Jvbl9tYW5hZ2VfcmVzcBgUIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNyb25NYW5hZ2VSZXNwb25zZUgAEkYKEWhpc3RvcnlfbGlzdF9yZXNwGB4gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeUxpc3RSZXNwb25zZUgAEkQKEGhpc3RvcnlfZ2V0X3Jlc3AYHyABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5R2V0UmVzcG9uc2VIABJKChNoaXN0b3J5X3JlbmFtZV9yZXNwGCAgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVJlbmFtZVJlc3BvbnNlSAASSgoTaGlzdG9yeV9kZWxldGVfcmVzcBghIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlEZWxldGVSZXNwb25zZUgAEj4KDGhpc3Rvcnlfc3luYxgiIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTeW5jRXZlbnRIABJKChNoaXN0b3J5X3ByZWZpeF9yZXNwGCMgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVByZWZpeFJlc3BvbnNlSAASRAoQaGlzdG9yeV9waW5fcmVzcBgkIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlQaW5SZXNwb25zZUgAEk8KFmhpc3Rvcnlfc2hhcmVfZ2V0X3Jlc3AYJSABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2hhcmVHZXRSZXNwb25zZUgAEk8KFmhpc3Rvcnlfc2hhcmVfc2V0X3Jlc3AYJiABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2hhcmVTZXRSZXNwb25zZUgAElcKGmhpc3Rvcnlfc2hhcmVfcmVzb2x2ZV9yZXNwGCcgASgLMjEubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlUmVzb2x2ZVJlc3BvbnNlSAASTgoVaGlzdG9yeV93b3JrZGlyc19yZXNwGDggASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVdvcmtkaXJzUmVzcG9uc2VIABJIChJwcm92aWRlcl9saXN0X3Jlc3AYKCABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Qcm92aWRlckxpc3RSZXNwb25zZUgAEkYKEXNldHRpbmdzX2dldF9yZXNwGCkgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuU2V0dGluZ3NHZXRSZXNwb25zZUgAEkwKFHNldHRpbmdzX3VwZGF0ZV9yZXNwGCogASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuU2V0dGluZ3NVcGRhdGVSZXNwb25zZUgAEkAKDXNldHRpbmdzX3N5bmMYKyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1N5bmNFdmVudEgAEk0KFXNraWxsX2ZpbGVzX2xpc3RfcmVzcBgsIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsRmlsZXNMaXN0UmVzcG9uc2VIABJTChhza2lsbF9tZXRhZGF0YV9yZWFkX3Jlc3AYLSABKAsyLy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbE1ldGFkYXRhUmVhZFJlc3BvbnNlSAASSwoUc2tpbGxfdGV4dF9yZWFkX3Jlc3AYLiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbFRleHRSZWFkUmVzcG9uc2VIABJPChZmaWxlX21lbnRpb25fbGlzdF9yZXNwGC8gASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuRmlsZU1lbnRpb25MaXN0UmVzcG9uc2VIABJXChp1cGxvYWRfcmVhZGFibGVfZmlsZXNfcmVzcBgwIAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYyLlVwbG9hZFJlYWRhYmxlRmlsZXNSZXNwb25zZUgAEj4KDWZzX3Jvb3RzX3Jlc3AYMSABKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1Jvb3RzUmVzcG9uc2VIABIyCgRwb25nGDIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuUG9uZ1Jlc3BvbnNlSAASRQoRZnNfbGlzdF9kaXJzX3Jlc3AYMyABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3REaXJzUmVzcG9uc2VIABJZCht1cGxvYWRlZF9pbWFnZV9wcmV2aWV3X3Jlc3AYNCABKAsyMi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRlZEltYWdlUHJldmlld1Jlc3BvbnNlSAASSAoSbWVtb3J5X21hbmFnZV9yZXNwGDUgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuTWVtb3J5TWFuYWdlUmVzcG9uc2VIABJGChFza2lsbF9tYW5hZ2VfcmVzcBg2IAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsTWFuYWdlUmVzcG9uc2VIABJcCh1mc19jcmVhdGVfcHJvamVjdF9mb2xkZXJfcmVzcBg3IAEoCzIzLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlUHJvamVjdEZvbGRlclJlc3BvbnNlSAASQwoRdGVybWluYWxfcmVzcG9uc2UYOSABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFJlc3BvbnNlSAASPQoOdGVybWluYWxfZXZlbnQYOiABKAsyIy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbEV2ZW50SAASPAoMZnNfbGlzdF9yZXNwGDsgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNMaXN0UmVzcG9uc2VIABJHChJmc193cml0ZV90ZXh0X3Jlc3AYPCABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1dyaXRlVGV4dFJlc3BvbnNlSAASRwoSZnNfY3JlYXRlX2Rpcl9yZXNwGD0gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNDcmVhdGVEaXJSZXNwb25zZUgAEkAKDmZzX3JlbmFtZV9yZXNwGD4gASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSZW5hbWVSZXNwb25zZUgAEkAKDmZzX2RlbGV0ZV9yZXNwGD8gASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNEZWxldGVSZXNwb25zZUgAEjkKDGdpdF9yZXNwb25zZRhAIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYyLkdpdFJlc3BvbnNlSAASVgoaZnNfcmVhZF9lZGl0YWJsZV90ZXh0X3Jlc3AYQSABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1JlYWRFZGl0YWJsZVRleHRSZXNwb25zZUgAEloKHGZzX3JlYWRfd29ya3NwYWNlX2ltYWdlX3Jlc3AYQiABKAsyMi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1JlYWRXb3Jrc3BhY2VJbWFnZVJlc3BvbnNlSAASOwoNc2Z0cF9yZXNwb25zZRhJIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBSZXNwb25zZUgAEjUKCnNmdHBfZXZlbnQYSiABKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZnRwRXZlbnRIABJCCg9jaGF0X3F1ZXVlX3Jlc3AYSyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVSZXNwb25zZUgAEkAKEGNoYXRfcXVldWVfZXZlbnQYTCABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVFdmVudEgAEj4KDGNoYXRfY29udHJvbBhGIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRDb250cm9sRXZlbnRIABJCCg5ydW50aW1lX3N0YXR1cxhHIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlJ1bnRpbWVTdGF0dXNFdmVudEgAEmUKInNldHRpbmdzX3Jlc2V0X3NzaF9rbm93bl9ob3N0X3Jlc3AYSCABKAsyNy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVzcG9uc2VIABJKChVjaGF0X3J1bnRpbWVfc25hcHNob3QYTSABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UnVudGltZVNuYXBzaG90SAASTAoUcHJvdmlkZXJfbW9kZWxzX3Jlc3AYTyABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Qcm92aWRlck1vZGVsc1Jlc3BvbnNlSAASQgoOdHVubmVsX2Rlc2lyZWQYUCABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxEZXNpcmVkU3RhdGVIABJMChZ0dW5uZWxfbXV0YXRpb25fcmVzdWx0GFEgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsTXV0YXRpb25SZXN1bHRIABI5Cgx0dW5uZWxfZnJhbWUYUiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxGcmFtZUgAEkYKE3R1bm5lbF9wcm9iZV9yZXBvcnQYUyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxQcm9iZVJlcG9ydEgAEkoKEndvcmtzcGFjZV9hY3Rpdml0eRhaIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLldvcmtzcGFjZUFjdGl2aXR5RXZlbnRIABJQChhtYW5hZ2VkX3Byb2Nlc3NfcmVzcG9uc2UYWyABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5NYW5hZ2VkUHJvY2Vzc1Jlc3BvbnNlSAASUAoYbWFuYWdlZF9wcm9jZXNzX3NuYXBzaG90GFwgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuTWFuYWdlZFByb2Nlc3NTbmFwc2hvdEgAEkoKE2hpc3RvcnlfYnJhbmNoX3Jlc3AYXSABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5QnJhbmNoUmVzcG9uc2VIABJKChNwcm92aWRlcl91c2FnZV9yZXNwGF4gASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJVc2FnZVJlc3BvbnNlSAASRAoSY2hhdF9pbmdyZXNzX2JhdGNoGF8gASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NCYXRjaEgAEkYKE2NoYXRfaW5ncmVzc19yZXN1bWUYYCABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc1Jlc3VtZUgAEkoKFWNoYXRfaW5ncmVzc19mcmFnbWVudBhhIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzRnJhZ21lbnRIABJJChNjaGF0X2ZpbGVfb3Blbl9yZXNwGGIgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEZpbGVPcGVuUmVzcG9uc2VIABI0CgVlcnJvchhjIAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkVycm9yUmVzcG9uc2VIABJLChRoaXN0b3J5X3NldF9jd2RfcmVzcBhkIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTZXRDd2RSZXNwb25zZUgAElcKGndvcmtzcGFjZV9yb290X2dyYW50c19yZXNwGGUgASgLMjEubGl2ZWFnZW50LmdhdGV3YXkudjIuV29ya3NwYWNlUm9vdEdyYW50c1Jlc3BvbnNlSAASTgoVaW1wb3J0X2RpcmVjdG9yeV9yZXNwGGYgASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuSW1wb3J0RGlyZWN0b3J5UmVzcG9uc2VIABJDCg9jaGVja3BvaW50X3Jlc3AYZyABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGVja3BvaW50UmVzcG9uc2VIAEIJCgdwYXlsb2FkSgQIQxBESgQIRBBFSgQIRRBGSgQIThBPIlUKEUNoYXRTZWxlY3RlZE1vZGVsEhoKEmN1c3RvbV9wcm92aWRlcl9pZBgBIAEoCRINCgVtb2RlbBgCIAEoCRIVCg1wcm92aWRlcl90eXBlGAMgASgJImUKE0NoYXRSdW50aW1lQ29udHJvbHMSGAoQdGhpbmtpbmdfZW5hYmxlZBgBIAEoCBIhChluYXRpdmVfd2ViX3NlYXJjaF9lbmFibGVkGAIgASgIEhEKCXJlYXNvbmluZxgDIAEoCSJ1ChBDaGF0VXBsb2FkZWRGaWxlEhUKDXJlbGF0aXZlX3BhdGgYASABKAkSFQoNYWJzb2x1dGVfcGF0aBgCIAEoCRIRCglmaWxlX25hbWUYAyABKAkSDAoEa2luZBgEIAEoCRISCgpzaXplX2J5dGVzGAUgASgDIksKElVwbG9hZFJlYWRhYmxlRmlsZRIRCglmaWxlX25hbWUYASABKAkSEQoJbWltZV90eXBlGAIgASgJEg8KB2NvbnRlbnQYAyABKAwiZgoaVXBsb2FkUmVhZGFibGVGaWxlc1JlcXVlc3QSDwoHd29ya2RpchgBIAEoCRI3CgVmaWxlcxgCIAMoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlVwbG9hZFJlYWRhYmxlRmlsZSJlChtVcGxvYWRSZWFkYWJsZUZpbGVzUmVzcG9uc2USNQoFZmlsZXMYASADKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0VXBsb2FkZWRGaWxlEg8KB3NraXBwZWQYAiADKAkiPQoTSW1wb3J0RGlyZWN0b3J5RmlsZRIVCg1yZWxhdGl2ZV9wYXRoGAEgASgJEg8KB2NvbnRlbnQYAiABKAwiwwIKFkltcG9ydERpcmVjdG9yeVJlcXVlc3QSDAoEbmFtZRgBIAEoCRIOCgZ0YXJnZXQYAiABKAkSPAoFZmlsZXMYAyADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5JbXBvcnREaXJlY3RvcnlGaWxlQgIYARITCgt0cmFuc2Zlcl9pZBgEIAEoCRJBCglvcGVyYXRpb24YBSABKA4yLi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5JbXBvcnREaXJlY3RvcnlPcGVyYXRpb24SEwoLdG90YWxfZmlsZXMYBiABKA0SEwoLdG90YWxfYnl0ZXMYByABKAQSFQoNcmVsYXRpdmVfcGF0aBgIIAEoCRIOCgZvZmZzZXQYCSABKAQSDQoFY2h1bmsYCiABKAwSFQoNZmlsZV9jb21wbGV0ZRgLIAEoCCJ+ChdJbXBvcnREaXJlY3RvcnlSZXNwb25zZRIRCglyb290X3BhdGgYASABKAkSEgoKZmlsZV9jb3VudBgCIAEoBRIPCgdza2lwcGVkGAMgAygJEhMKC3RyYW5zZmVyX2lkGAQgASgJEhYKDnJlY2VpdmVkX2J5dGVzGAUgASgEIkUKG1VwbG9hZGVkSW1hZ2VQcmV2aWV3UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEhUKDWFic29sdXRlX3BhdGgYAiABKAkiPwocVXBsb2FkZWRJbWFnZVByZXZpZXdSZXNwb25zZRIRCgltaW1lX3R5cGUYASABKAkSDAoEZGF0YRgCIAEoCSJ7CgpUdW5uZWxTcGVjEgoKAmlkGAEgASgJEhEKCXNsdWdfaGludBgCIAEoCRIMCgRuYW1lGAMgASgJEhIKCnRhcmdldF91cmwYBCABKAkSEgoKZXhwaXJlc19hdBgFIAEoAxIYChBwcm9qZWN0X3BhdGhfa2V5GAYgASgJIlkKElR1bm5lbERlc2lyZWRTdGF0ZRIxCgd0dW5uZWxzGAEgAygLMiAubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsU3BlYxIQCghyZXZpc2lvbhgCIAEoBCJmCgxUdW5uZWxIZWFsdGgSDgoGc3RhdHVzGAEgASgJEhMKC2h0dHBfc3RhdHVzGAIgASgNEg0KBWVycm9yGAMgASgJEhIKCmNoZWNrZWRfYXQYBCABKAMSDgoGcnR0X21zGAUgASgNIvABCgxUdW5uZWxTdGF0dXMSCgoCaWQYASABKAkSDAoEc2x1ZxgCIAEoCRIMCgRuYW1lGAMgASgJEhIKCnRhcmdldF91cmwYBCABKAkSEwoLcHVibGljX3BhdGgYBSABKAkSEgoKY3JlYXRlZF9hdBgGIAEoAxISCgpleHBpcmVzX2F0GAcgASgDEhoKEmFjdGl2ZV9jb25uZWN0aW9ucxgIIAEoDRIYChBwcm9qZWN0X3BhdGhfa2V5GAkgASgJEjEKBWxvY2FsGAogASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhbHRoIqUBChNUdW5uZWxTdGF0ZVNuYXBzaG90EjMKB3R1bm5lbHMYASADKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxTdGF0dXMSEAoIcmV2aXNpb24YAiABKAQSFAoMYWdlbnRfb25saW5lGAMgASgIEjEKBXJlbGF5GAQgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhbHRoIpkBCg5UdW5uZWxNdXRhdGlvbhIOCgZhY3Rpb24YASABKAkSEQoJdHVubmVsX2lkGAIgASgJEhIKCnRhcmdldF91cmwYAyABKAkSDAoEbmFtZRgEIAEoCRIYCgt0dGxfc2Vjb25kcxgFIAEoDUgAiAEBEhgKEHByb2plY3RfcGF0aF9rZXkYBiABKAlCDgoMX3R0bF9zZWNvbmRzIlQKFFR1bm5lbE11dGF0aW9uUmVzdWx0EhEKCXR1bm5lbF9pZBgBIAEoCRISCgplcnJvcl9jb2RlGAIgASgJEhUKDWVycm9yX21lc3NhZ2UYAyABKAkiWQoRVHVubmVsUHJvYmVSZXN1bHQSEQoJdHVubmVsX2lkGAEgASgJEjEKBWxvY2FsGAIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhbHRoIk0KEVR1bm5lbFByb2JlUmVwb3J0EjgKB3Jlc3VsdHMYASADKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxQcm9iZVJlc3VsdCIrCgxUdW5uZWxIZWFkZXISDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSL1AgoLVHVubmVsRnJhbWUSEQoJc3RyZWFtX2lkGAEgASgJEjMKBGtpbmQYAiABKA4yJS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxGcmFtZUtpbmQSEgoKdGFyZ2V0X3VybBgDIAEoCRIOCgZtZXRob2QYBCABKAkSDAoEcGF0aBgFIAEoCRIzCgdoZWFkZXJzGAYgAygLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhZGVyEg4KBnN0YXR1cxgHIAEoDRIMCgRib2R5GAggASgMEg0KBWVycm9yGAkgASgJEkIKD3dzX21lc3NhZ2VfdHlwZRgKIAEoDjIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbFdzTWVzc2FnZVR5cGUSFgoOd3Nfc3VicHJvdG9jb2wYCyABKAkSFQoNd3NfY2xvc2VfY29kZRgMIAEoDRIXCg93c19jbG9zZV9yZWFzb24YDSABKAkiKQoVV29ya3NwYWNlV2F0Y2hSZXF1ZXN0EhAKCHdvcmtkaXJzGAEgAygJIn4KFldvcmtzcGFjZUFjdGl2aXR5RXZlbnQSDwoHd29ya2RpchgBIAEoCRIQCghyZXZpc2lvbhgCIAEoBBIKCgJmcxgDIAEoCBILCgNnaXQYBCABKAgSFQoNY2hhbmdlZF9wYXRocxgFIAMoCRIRCgl0cnVuY2F0ZWQYBiABKAgilgIKFE1hbmFnZWRQcm9jZXNzUmVjb3JkEgoKAmlkGAEgASgJEg0KBWxhYmVsGAIgASgJEg8KB2NvbW1hbmQYAyABKAkSCwoDY3dkGAQgASgJEg0KBXNoZWxsGAUgASgJEgsKA3BpZBgGIAEoDRIQCghsb2dfcGF0aBgHIAEoCRISCgpzdGFydGVkX2F0GAggASgDEhgKC2ZpbmlzaGVkX2F0GAkgASgDSACIAQESFgoJZXhpdF9jb2RlGAogASgFSAGIAQESDwoHcnVubmluZxgLIAEoCBIQCghpc29sYXRlZBgMIAEoCBIQCghyZXN0b3JlZBgNIAEoCEIOCgxfZmluaXNoZWRfYXRCDAoKX2V4aXRfY29kZSJpChZNYW5hZ2VkUHJvY2Vzc1NuYXBzaG90Ej0KCXByb2Nlc3NlcxgBIAMoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLk1hbmFnZWRQcm9jZXNzUmVjb3JkEhAKCHJldmlzaW9uGAIgASgEIk4KFU1hbmFnZWRQcm9jZXNzUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSEgoKcHJvY2Vzc19pZBgCIAEoCRIRCgltYXhfYnl0ZXMYAyABKA0itwEKFk1hbmFnZWRQcm9jZXNzUmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEj4KCHNuYXBzaG90GAIgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuTWFuYWdlZFByb2Nlc3NTbmFwc2hvdBITCgtsb2dfY29udGVudBgDIAEoCRIQCghsb2dfcGF0aBgEIAEoCRIVCg1sb2dfdHJ1bmNhdGVkGAUgASgIEg8KB3N0b3BwZWQYBiABKAgiOQoTTWVtb3J5TWFuYWdlUmVxdWVzdBIPCgdjb21tYW5kGAEgASgJEhEKCWFyZ3NfanNvbhgCIAEoCSIrChRNZW1vcnlNYW5hZ2VSZXNwb25zZRITCgtyZXN1bHRfanNvbhgBIAEoCSKYAwoPVGVybWluYWxSZXF1ZXN0Eg4KBmFjdGlvbhgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAyABKAkSCwoDY3dkGAQgASgJEg0KBXNoZWxsGAUgASgJEg0KBXRpdGxlGAYgASgJEgwKBGRhdGEYByABKAkSDAoEY29scxgIIAEoDRIMCgRyb3dzGAkgASgNEhEKCW1heF9ieXRlcxgKIAEoDRITCgtzc2hfaG9zdF9pZBgLIAEoCRIRCglwcm9tcHRfaWQYDCABKAkSFQoNcHJvbXB0X2Fuc3dlchgNIAEoCRIWCg50cnVzdF9ob3N0X2tleRgOIAEoCBIUCgxzZnRwX2VuYWJsZWQYDyABKAgSDgoGdGFiX2lkGBAgASgJEhAKCHRhYl9raW5kGBEgASgJEhMKC3JlbW90ZV9ob3N0GBIgASgJEhMKC3JlbW90ZV9wb3J0GBMgASgNEhIKCmxvY2FsX3BvcnQYFCABKA0SEgoKZm9yd2FyZF9pZBgVIAEoCSKyAgoPVGVybWluYWxTZXNzaW9uEgoKAmlkGAEgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAiABKAkSCwoDY3dkGAMgASgJEg0KBXNoZWxsGAQgASgJEg0KBXRpdGxlGAUgASgJEgsKA3BpZBgGIAEoDRIMCgRjb2xzGAcgASgNEgwKBHJvd3MYCCABKA0SEgoKY3JlYXRlZF9hdBgJIAEoBBISCgp1cGRhdGVkX2F0GAogASgEEhMKC2ZpbmlzaGVkX2F0GAsgASgEEhEKCWV4aXRfY29kZRgMIAEoBRIPCgdydW5uaW5nGA0gASgIEgwKBGtpbmQYDiABKAkSNgoDc3NoGA8gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hNZXRhZGF0YSLbAQoTVGVybWluYWxTc2hNZXRhZGF0YRIPCgdob3N0X2lkGAEgASgJEhEKCWhvc3RfbmFtZRgCIAEoCRIQCgh1c2VybmFtZRgDIAEoCRIMCgRob3N0GAQgASgJEgwKBHBvcnQYBSABKA0SEQoJYXV0aF90eXBlGAYgASgJEg4KBnN0YXR1cxgHIAEoCRIZChFyZWNvbm5lY3RfYXR0ZW1wdBgIIAEoDRIeChZyZWNvbm5lY3RfbWF4X2F0dGVtcHRzGAkgASgNEhQKDHNmdHBfZW5hYmxlZBgKIAEoCCKRAwoLU2Z0cFJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRIPCgd3b3JrZGlyGAQgASgJEhIKCmxvY2FsX3BhdGgYBSABKAkSEwoLcmVtb3RlX3BhdGgYBiABKAkSEQoJZnJvbV9wYXRoGAcgASgJEg8KB3RvX3BhdGgYCCABKAkSEQoJZGlyZWN0aW9uGAkgASgJEhMKC3RhcmdldF9wYXRoGAogASgJEhEKCXJlY3Vyc2l2ZRgLIAEoCBIRCglvdmVyd3JpdGUYDCABKAgSDwoHY29udGVudBgNIAEoCRIOCgZvZmZzZXQYDiABKAQSEQoJbWF4X2J5dGVzGA8gASgEEhMKC3N0cmljdF91dGY4GBAgASgIEhYKDmV4cGVjdGVkX210aW1lGBEgASgEEhsKE2V4cGVjdGVkX3NpemVfYnl0ZXMYEiABKAQSGgoSY3JlYXRlX3BhcmVudF9kaXJzGBMgASgIIlgKCVNmdHBFbnRyeRIMCgRwYXRoGAEgASgJEgwKBG5hbWUYAiABKAkSDAoEa2luZBgDIAEoCRISCgpzaXplX2J5dGVzGAQgASgEEg0KBW10aW1lGAUgASgEIvIBCgxTZnRwVHJhbnNmZXISCgoCaWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIRCglkaXJlY3Rpb24YAyABKAkSDgoGc3RhdHVzGAQgASgJEhMKC3NvdXJjZV9wYXRoGAUgASgJEhMKC3RhcmdldF9wYXRoGAYgASgJEhQKDGN1cnJlbnRfcGF0aBgHIAEoCRISCgpieXRlc19kb25lGAggASgEEhMKC2J5dGVzX3RvdGFsGAkgASgEEhIKCmZpbGVzX2RvbmUYCiABKA0SEwoLZmlsZXNfdG90YWwYCyABKA0SDQoFZXJyb3IYDCABKAkisAIKDFNmdHBSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSDAoEcGF0aBgCIAEoCRIwCgdlbnRyaWVzGAMgAygLMh8ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cEVudHJ5Ei4KBWVudHJ5GAQgASgLMh8ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cEVudHJ5Eg4KBmV4aXN0cxgFIAEoCBI0Cgh0cmFuc2ZlchgGIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBUcmFuc2ZlchIPCgdjb250ZW50GAcgASgJEg4KBm9mZnNldBgIIAEoBBISCgpieXRlc19yZWFkGAkgASgEEhIKCnNpemVfYnl0ZXMYCiABKAQSEQoJdHJ1bmNhdGVkGAsgASgIIk8KCVNmdHBFdmVudBIMCgRraW5kGAEgASgJEjQKCHRyYW5zZmVyGAIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cFRyYW5zZmVyIsEBChFUZXJtaW5hbFNzaFByb21wdBIKCgJpZBgBIAEoCRIMCgRraW5kGAIgASgJEg8KB2hvc3RfaWQYAyABKAkSEQoJaG9zdF9uYW1lGAQgASgJEgwKBGhvc3QYBSABKAkSDAoEcG9ydBgGIAEoDRIPCgdtZXNzYWdlGAcgASgJEhoKEmZpbmdlcnByaW50X3NoYTI1NhgIIAEoCRIQCghrZXlfdHlwZRgJIAEoCRITCgthbnN3ZXJfZWNobxgKIAEoCCJBChNUZXJtaW5hbFNoZWxsT3B0aW9uEgoKAmlkGAEgASgJEg0KBWxhYmVsGAIgASgJEg8KB2NvbW1hbmQYAyABKAkigAEKDlRlcm1pbmFsU3NoVGFiEgoKAmlkGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRIMCgRraW5kGAQgASgJEhIKCmNyZWF0ZWRfYXQYBSABKAQSEgoKdXBkYXRlZF9hdBgGIAEoBCJ/ChdUZXJtaW5hbFNzaFRhYnNTbmFwc2hvdBIYChBwcm9qZWN0X3BhdGhfa2V5GAEgASgJEjIKBHRhYnMYAiADKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaFRhYhIQCghyZXZpc2lvbhgEIAEoBEoECAMQBCL9AQoXVGVybWluYWxTc2hMb2NhbEZvcndhcmQSCgoCaWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEhIKCmxvY2FsX2hvc3QYBCABKAkSEgoKbG9jYWxfcG9ydBgFIAEoDRIPCgdhZGRyZXNzGAYgASgJEhMKC3JlbW90ZV9ob3N0GAcgASgJEhMKC3JlbW90ZV9wb3J0GAggASgNEg4KBnN0YXR1cxgJIAEoCRISCgpjcmVhdGVkX2F0GAogASgEEhIKCnVwZGF0ZWRfYXQYCyABKAQSDQoFZXJyb3IYDCABKAkidQogVGVybWluYWxTc2hMb2NhbEZvcndhcmRzU25hcHNob3QSPwoIZm9yd2FyZHMYASADKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaExvY2FsRm9yd2FyZBIQCghyZXZpc2lvbhgCIAEoBCJ/Ch1UZXJtaW5hbFNzaExvY2FsRm9yd2FyZEFjdGlvbhI+Cgdmb3J3YXJkGAEgASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hMb2NhbEZvcndhcmQSEAoIcmV2aXNpb24YAiABKAQSDAoEa2luZBgDIAEoCSKnBQoQVGVybWluYWxSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSNwoIc2Vzc2lvbnMYAiADKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNlc3Npb24SNgoHc2Vzc2lvbhgDIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhIOCgZvdXRwdXQYBCABKAwSEQoJdHJ1bmNhdGVkGAUgASgIEkAKDXNoZWxsX29wdGlvbnMYBiADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNoZWxsT3B0aW9uEhUKDWRlZmF1bHRfc2hlbGwYByABKAkSGwoTb3V0cHV0X3N0YXJ0X29mZnNldBgIIAEoBBIZChFvdXRwdXRfZW5kX29mZnNldBgJIAEoBBI7Cgpzc2hfcHJvbXB0GAogASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hQcm9tcHQSEgoKbGF0ZW5jeV9tcxgLIAEoDRI/Cghzc2hfdGFicxgMIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoVGFic1NuYXBzaG90ElIKEnNzaF9sb2NhbF9mb3J3YXJkcxgNIAEoCzI2LmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoTG9jYWxGb3J3YXJkc1NuYXBzaG90Ek4KEXNzaF9sb2NhbF9mb3J3YXJkGA4gASgLMjMubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hMb2NhbEZvcndhcmRBY3Rpb24SKAogc3NoX2xvY2FsX2ZvcndhcmRfcG9ydF9hdmFpbGFibGUYDyABKAgi2gIKDVRlcm1pbmFsRXZlbnQSDAoEa2luZBgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAyABKAkSNgoHc2Vzc2lvbhgEIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhIMCgRkYXRhGAUgASgMEhsKE291dHB1dF9zdGFydF9vZmZzZXQYBiABKAQSGQoRb3V0cHV0X2VuZF9vZmZzZXQYByABKAQSPwoIc3NoX3RhYnMYCCABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaFRhYnNTbmFwc2hvdBJOChFzc2hfbG9jYWxfZm9yd2FyZBgJIAEoCzIzLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoTG9jYWxGb3J3YXJkQWN0aW9uIrICChNUZXJtaW5hbFN0cmVhbUZyYW1lEgwKBGtpbmQYASABKAkSEQoJc3RyZWFtX2lkGAIgASgJEhIKCnNlc3Npb25faWQYAyABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgEIAEoCRILCgNzZXEYBSABKAQSFAoMc3RhcnRfb2Zmc2V0GAYgASgEEhIKCmVuZF9vZmZzZXQYByABKAQSDAoEY29scxgIIAEoDRIMCgRyb3dzGAkgASgNEhEKCW1heF9ieXRlcxgKIAEoDRIRCgl0cnVuY2F0ZWQYCyABKAgSDQoFZXJyb3IYDCABKAkSNgoHc2Vzc2lvbhgNIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhIMCgRkYXRhGA4gASgMIkAKCkdpdFJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEg8KB3dvcmtkaXIYAiABKAkSEQoJYXJnc19qc29uGAMgASgJIjIKC0dpdFJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRITCgtyZXN1bHRfanNvbhgCIAEoCSKRAwoLQ2hhdFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSPwoOc2VsZWN0ZWRfbW9kZWwYAyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0U2VsZWN0ZWRNb2RlbBIWCg5leGVjdXRpb25fbW9kZRgEIAEoCRIPCgd3b3JrZGlyGAUgASgJEj4KDnVwbG9hZGVkX2ZpbGVzGAcgAygLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFVwbG9hZGVkRmlsZRIZChFjbGllbnRfcmVxdWVzdF9pZBgIIAEoCRJDChBydW50aW1lX2NvbnRyb2xzGAkgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFJ1bnRpbWVDb250cm9scxIUCgxxdWV1ZV9wb2xpY3kYCiABKAkSGwoTY29tbWFuZF9zYWZldHlfbW9kZRgLIAEoCUoECAYQB1IVc2VsZWN0ZWRfc3lzdGVtX3Rvb2xzIooBCg5DaGF0TWVzc2FnZVJlZhIVCg1zZWdtZW50X2luZGV4GAEgASgFEhUKDW1lc3NhZ2VfaW5kZXgYAiABKAUSEgoKc2VnbWVudF9pZBgDIAEoCRISCgptZXNzYWdlX2lkGAQgASgJEgwKBHJvbGUYBSABKAkSFAoMY29udGVudF9oYXNoGAYgASgJIjwKEUNhbmNlbENoYXRSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIOCgZydW5faWQYAiABKAkizwEKEkNoYXRDb21tYW5kUmVxdWVzdBIMCgR0eXBlGAEgASgJEjIKB3JlcXVlc3QYAiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UmVxdWVzdBI+ChBiYXNlX21lc3NhZ2VfcmVmGAMgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdE1lc3NhZ2VSZWYSNwoGY2FuY2VsGAQgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2FuY2VsQ2hhdFJlcXVlc3QiuAEKEENoYXRRdWV1ZVJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRIPCgdpdGVtX2lkGAMgASgJEhEKCWRpcmVjdGlvbhgEIAEoCRIQCghyZXZpc2lvbhgFIAEoBBISCgpkcmFmdF9qc29uGAYgASgJEhsKE3VwbG9hZGVkX2ZpbGVzX2pzb24YByABKAkSFAoMcmVxdWVzdF9qc29uGAggASgJIoYBChFDaGF0UXVldWVSZXNwb25zZRIQCghhY2NlcHRlZBgBIAEoCBIPCgdtZXNzYWdlGAIgASgJEhUKDXNuYXBzaG90X2pzb24YAyABKAkSEQoJaXRlbV9qc29uGAQgASgJEhIKCmVycm9yX2NvZGUYBSABKAkSEAoIcmV2aXNpb24YBiABKAQiUgoOQ2hhdFF1ZXVlRXZlbnQSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEhUKDXNuYXBzaG90X2pzb24YAiABKAkSEAoIcmV2aXNpb24YAyABKAQihQIKCUNoYXRFdmVudBI7CgR0eXBlGAEgASgOMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEV2ZW50LkNoYXRFdmVudFR5cGUSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEgwKBGRhdGEYAyABKAkikwEKDUNoYXRFdmVudFR5cGUSCQoFVE9LRU4QABIMCghUSElOS0lORxABEg0KCVRPT0xfQ0FMTBACEg8KC1RPT0xfUkVTVUxUEAMSCAoERE9ORRAEEgkKBUVSUk9SEAUSDwoLVE9PTF9TVEFUVVMQBhIRCg1IT1NURURfU0VBUkNIEAcSEAoMVVNFUl9NRVNTQUdFEAgivAEKEENoYXRDb250cm9sRXZlbnQSEgoKcmVxdWVzdF9pZBgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgCIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAyABKAkSEQoJcnVuX2Vwb2NoGAQgASgDEgwKBHR5cGUYBSABKAkSDQoFc3RhdGUYBiABKAkSEgoKZXJyb3JfY29kZRgHIAEoCRIPCgdtZXNzYWdlGAggASgJEgsKA3NlcRgJIAEoAyL8AQoTQ2hhdFJ1bnRpbWVTbmFwc2hvdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDgoGcnVuX2lkGAIgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAMgASgJEhEKCXdvcmtlcl9pZBgEIAEoCRINCgVzdGF0ZRgFIAEoCRILCgNjd2QYBiABKAkSEgoKdXBkYXRlZF9hdBgHIAEoAxIQCghyZXZpc2lvbhgIIAEoAxIUCgxlbnRyaWVzX2pzb24YCSABKAkSEwoLdG9vbF9zdGF0dXMYCiABKAkSIQoZdG9vbF9zdGF0dXNfaXNfY29tcGFjdGlvbhgLIAEoCCLqAQoSUnVudGltZVN0YXR1c0V2ZW50EhEKCXdvcmtlcl9pZBgBIAEoCRINCgVzdGF0ZRgCIAEoCRIPCgd2aXNpYmxlGAMgASgIEhgKEGFjdGl2ZV9ydW5fY291bnQYBCABKA0SEQoJdGltZXN0YW1wGAUgASgDEjgKC2FjdGl2ZV9ydW5zGAYgAygLMiMubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFJ1blJlcG9ydBI6Cg1maW5pc2hlZF9ydW5zGAcgAygLMiMubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFJ1blJlcG9ydCKAAQoNQ2hhdFJ1blJlcG9ydBIOCgZydW5faWQYASABKAkSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEg0KBXN0YXRlGAMgASgJEhIKCmVycm9yX2NvZGUYBCABKAkSDwoHbWVzc2FnZRgFIAEoCRISCgp1cGRhdGVkX2F0GAYgASgDIkcKEUNyb25NYW5hZ2VSZXF1ZXN0Eg4KBmFjdGlvbhgBIAEoCRIPCgd0YXNrX2lkGAIgASgJEhEKCXRhc2tfanNvbhgDIAEoCSI5ChJDcm9uTWFuYWdlUmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEhMKC3Jlc3VsdF9qc29uGAIgASgJIlUKEkhpc3RvcnlMaXN0UmVxdWVzdBIMCgRwYWdlGAEgASgFEhEKCXBhZ2Vfc2l6ZRgCIAEoBRILCgNjd2QYAyABKAkSEQoJY3dkX2VtcHR5GAQgASgIImwKE0hpc3RvcnlMaXN0UmVzcG9uc2USQAoNY29udmVyc2F0aW9ucxgBIAMoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNvbnZlcnNhdGlvblN1bW1hcnkSEwoLdG90YWxfY291bnQYAiABKAUiigIKE0NvbnZlcnNhdGlvblN1bW1hcnkSCgoCaWQYASABKAkSDQoFdGl0bGUYAiABKAkSEgoKY3JlYXRlZF9hdBgDIAEoAxISCgp1cGRhdGVkX2F0GAQgASgDEhUKDW1lc3NhZ2VfY291bnQYBSABKAUSEwoLcHJvdmlkZXJfaWQYBiABKAkSDQoFbW9kZWwYByABKAkSEgoKc2Vzc2lvbl9pZBgIIAEoCRILCgNjd2QYCSABKAkSEQoJaXNfcGlubmVkGAogASgIEhEKCXBpbm5lZF9hdBgLIAEoAxIRCglpc19zaGFyZWQYDCABKAgSGwoTc2VsZWN0ZWRfbW9kZWxfanNvbhgNIAEoCSJCChFIaXN0b3J5R2V0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFAoMbWF4X21lc3NhZ2VzGAIgASgFItQBChJIaXN0b3J5R2V0UmVzcG9uc2USFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEhUKDW1lc3NhZ2VzX2pzb24YAiABKAkSGwoTdG90YWxfbWVzc2FnZV9jb3VudBgDIAEoBRIeChZyZXR1cm5lZF9tZXNzYWdlX2NvdW50GAQgASgFEhAKCGhhc19tb3JlGAUgASgIEj8KDGNvbnZlcnNhdGlvbhgGIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNvbnZlcnNhdGlvblN1bW1hcnkihQEKFEhpc3RvcnlQcmVmaXhSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIUCgxtYXhfbWVzc2FnZXMYAiABKAUSPgoQYmFzZV9tZXNzYWdlX3JlZhgDIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRNZXNzYWdlUmVmItcBChVIaXN0b3J5UHJlZml4UmVzcG9uc2USFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEhUKDW1lc3NhZ2VzX2pzb24YAiABKAkSGwoTdG90YWxfbWVzc2FnZV9jb3VudBgDIAEoBRIeChZyZXR1cm5lZF9tZXNzYWdlX2NvdW50GAQgASgFEhAKCGhhc19tb3JlGAUgASgIEj8KDGNvbnZlcnNhdGlvbhgGIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNvbnZlcnNhdGlvblN1bW1hcnkiPgoUSGlzdG9yeVJlbmFtZVJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg0KBXRpdGxlGAIgASgJIlgKFUhpc3RvcnlSZW5hbWVSZXNwb25zZRI/Cgxjb252ZXJzYXRpb24YASABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5Im8KFEhpc3RvcnlCcmFuY2hSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRI+ChBiYXNlX21lc3NhZ2VfcmVmGAIgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdE1lc3NhZ2VSZWYiWAoVSGlzdG9yeUJyYW5jaFJlc3BvbnNlEj8KDGNvbnZlcnNhdGlvbhgBIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNvbnZlcnNhdGlvblN1bW1hcnkiPwoRSGlzdG9yeVBpblJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEhEKCWlzX3Bpbm5lZBgCIAEoCCJVChJIaXN0b3J5UGluUmVzcG9uc2USPwoMY29udmVyc2F0aW9uGAEgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeSKSAQoSSGlzdG9yeVNoYXJlU3RhdHVzEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIPCgdlbmFibGVkGAIgASgIEg0KBXRva2VuGAMgASgJEhIKCmNyZWF0ZWRfYXQYBCABKAMSEgoKdXBkYXRlZF9hdBgFIAEoAxIbChNyZWRhY3RfdG9vbF9jb250ZW50GAYgASgIIjEKFkhpc3RvcnlTaGFyZUdldFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJIlIKF0hpc3RvcnlTaGFyZUdldFJlc3BvbnNlEjcKBXNoYXJlGAEgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlU3RhdHVzInwKFkhpc3RvcnlTaGFyZVNldFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg8KB2VuYWJsZWQYAiABKAgSIAoTcmVkYWN0X3Rvb2xfY29udGVudBgDIAEoCEgAiAEBQhYKFF9yZWRhY3RfdG9vbF9jb250ZW50IlIKF0hpc3RvcnlTaGFyZVNldFJlc3BvbnNlEjcKBXNoYXJlGAEgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlU3RhdHVzIisKGkhpc3RvcnlTaGFyZVJlc29sdmVSZXF1ZXN0Eg0KBXRva2VuGAEgASgJIsgBChtIaXN0b3J5U2hhcmVSZXNvbHZlUmVzcG9uc2USFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEhUKDW1lc3NhZ2VzX2pzb24YAiABKAkSGwoTdG90YWxfbWVzc2FnZV9jb3VudBgDIAEoBRI/Cgxjb252ZXJzYXRpb24YBCABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5EhsKE3JlZGFjdF90b29sX2NvbnRlbnQYBSABKAgiGAoWSGlzdG9yeVdvcmtkaXJzUmVxdWVzdCJVChVIaXN0b3J5V29ya2RpclN1bW1hcnkSDAoEcGF0aBgBIAEoCRIaChJjb252ZXJzYXRpb25fY291bnQYAiABKAUSEgoKdXBkYXRlZF9hdBgDIAEoAyJYChdIaXN0b3J5V29ya2RpcnNSZXNwb25zZRI9Cgh3b3JrZGlycxgBIAMoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlXb3JrZGlyU3VtbWFyeSIvChRIaXN0b3J5RGVsZXRlUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkiFwoVSGlzdG9yeURlbGV0ZVJlc3BvbnNlInoKEEhpc3RvcnlTeW5jRXZlbnQSDAoEa2luZBgBIAEoCRI/Cgxjb252ZXJzYXRpb24YAiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5EhcKD2NvbnZlcnNhdGlvbl9pZBgDIAEoCSIVChNQcm92aWRlckxpc3RSZXF1ZXN0Ii4KFFByb3ZpZGVyTGlzdFJlc3BvbnNlEhYKDnByb3ZpZGVyc19qc29uGAEgASgJIhQKElNldHRpbmdzR2V0UmVxdWVzdCIsChNTZXR0aW5nc0dldFJlc3BvbnNlEhUKDXNldHRpbmdzX2pzb24YASABKAkiLgoVU2V0dGluZ3NVcGRhdGVSZXF1ZXN0EhUKDXNldHRpbmdzX2pzb24YASABKAkiOwoWU2V0dGluZ3NVcGRhdGVSZXNwb25zZRIQCghhY2NlcHRlZBgBIAEoCBIPCgdtZXNzYWdlGAIgASgJIj4KIFNldHRpbmdzUmVzZXRTc2hLbm93bkhvc3RSZXF1ZXN0EgwKBGhvc3QYASABKAkSDAoEcG9ydBgCIAEoDSI0CiFTZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVzcG9uc2USDwoHZGVsZXRlZBgBIAEoDSIqChFTZXR0aW5nc1N5bmNFdmVudBIVCg1zZXR0aW5nc19qc29uGAEgASgJIhcKFVNraWxsRmlsZXNMaXN0UmVxdWVzdCJMChZTa2lsbEZpbGVzTGlzdFJlc3BvbnNlEhAKCHJvb3RfZGlyGAEgASgJEg0KBXBhdGhzGAIgAygJEhEKCXRydW5jYXRlZBgDIAEoCCIoChhTa2lsbE1ldGFkYXRhUmVhZFJlcXVlc3QSDAoEcGF0aBgBIAEoCSI+ChlTa2lsbE1ldGFkYXRhUmVhZFJlc3BvbnNlEgwKBG5hbWUYASABKAkSEwoLZGVzY3JpcHRpb24YAiABKAkiRAoUU2tpbGxUZXh0UmVhZFJlcXVlc3QSDAoEcGF0aBgBIAEoCRIOCgZvZmZzZXQYAiABKA0SDgoGbGVuZ3RoGAMgASgNIjsKFVNraWxsVGV4dFJlYWRSZXNwb25zZRIPCgdjb250ZW50GAEgASgJEhEKCXRydW5jYXRlZBgCIAEoCCIqChJTa2lsbE1hbmFnZVJlcXVlc3QSFAoMcGF5bG9hZF9qc29uGAEgASgJIioKE1NraWxsTWFuYWdlUmVzcG9uc2USEwoLcmVzdWx0X2pzb24YASABKAkidwoWRmlsZU1lbnRpb25MaXN0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEhMKC21heF9yZXN1bHRzGAIgASgNEg0KBXF1ZXJ5GAMgASgJEhgKC3Nob3dfaGlkZGVuGAQgASgISACIAQFCDgoMX3Nob3dfaGlkZGVuIj4KEEZpbGVNZW50aW9uRW50cnkSDAoEcGF0aBgBIAEoCRIMCgRraW5kGAIgASgJEg4KBmhpZGRlbhgDIAEoCCJlChdGaWxlTWVudGlvbkxpc3RSZXNwb25zZRI3CgdlbnRyaWVzGAEgAygLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuRmlsZU1lbnRpb25FbnRyeRIRCgl0cnVuY2F0ZWQYAiABKAgiPwoGRnNSb290EgoKAmlkGAEgASgJEgwKBHBhdGgYAiABKAkSDAoEa2luZBgDIAEoCRINCgVsYWJlbBgEIAEoCSIQCg5Gc1Jvb3RzUmVxdWVzdCI+Cg9Gc1Jvb3RzUmVzcG9uc2USKwoFcm9vdHMYASADKAsyHC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1Jvb3QiNgoRRnNMaXN0RGlyc1JlcXVlc3QSDAoEcGF0aBgBIAEoCRITCgttYXhfcmVzdWx0cxgCIAEoDSIoCgpGc0RpckVudHJ5EgwKBHBhdGgYASABKAkSDAoEbmFtZRgCIAEoCSJoChJGc0xpc3REaXJzUmVzcG9uc2USDAoEcGF0aBgBIAEoCRIxCgdlbnRyaWVzGAIgAygLMiAubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNEaXJFbnRyeRIRCgl0cnVuY2F0ZWQYAyABKAgiPAocRnNDcmVhdGVQcm9qZWN0Rm9sZGVyUmVxdWVzdBIOCgZwYXJlbnQYASABKAkSDAoEbmFtZRgCIAEoCSItCh1Gc0NyZWF0ZVByb2plY3RGb2xkZXJSZXNwb25zZRIMCgRwYXRoGAEgASgJIowBCg1Gc0xpc3RSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSDAoEcGF0aBgCIAEoCRINCgVkZXB0aBgDIAEoDRIOCgZvZmZzZXQYBCABKA0SEwoLbWF4X3Jlc3VsdHMYBSABKA0SGAoLc2hvd19oaWRkZW4YBiABKAhIAIgBAUIOCgxfc2hvd19oaWRkZW4iOQoLRnNMaXN0RW50cnkSDAoEcGF0aBgBIAEoCRIMCgRraW5kGAIgASgJEg4KBmhpZGRlbhgDIAEoCCK5AQoORnNMaXN0UmVzcG9uc2USDAoEcGF0aBgBIAEoCRIQCghoYXNfcGF0aBgCIAEoCBINCgVkZXB0aBgDIAEoDRIOCgZvZmZzZXQYBCABKA0SEwoLbWF4X3Jlc3VsdHMYBSABKA0SDQoFdG90YWwYBiABKA0SEAoIaGFzX21vcmUYByABKAgSMgoHZW50cmllcxgIIAMoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzTGlzdEVudHJ5IjoKGUZzUmVhZEVkaXRhYmxlVGV4dFJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIMCgRwYXRoGAIgASgJIowBChpGc1JlYWRFZGl0YWJsZVRleHRSZXNwb25zZRIMCgRwYXRoGAEgASgJEg8KB2NvbnRlbnQYAiABKAkSEAoIbXRpbWVfbXMYAyABKAQSFAoMY29udGVudF9oYXNoGAQgASgJEhIKCnNpemVfYnl0ZXMYBSABKAQSEwoLdG90YWxfbGluZXMYBiABKAQiPAobRnNSZWFkV29ya3NwYWNlSW1hZ2VSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSDAoEcGF0aBgCIAEoCSKJAQocRnNSZWFkV29ya3NwYWNlSW1hZ2VSZXNwb25zZRIMCgRwYXRoGAEgASgJEhEKCW1pbWVfdHlwZRgCIAEoCRIMCgRkYXRhGAMgASgJEhIKCnNpemVfYnl0ZXMYBCABKAQSEAoIbXRpbWVfbXMYBSABKAQSFAoMY29udGVudF9oYXNoGAYgASgJItsBChNDaGF0RmlsZU9wZW5SZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIPCgd3b3JrZGlyGAIgASgJEgwKBHBhdGgYAyABKAkSDgoGc291cmNlGAQgASgJEhEKBGxpbmUYBSABKA1IAIgBARIVCghlbmRfbGluZRgGIAEoDUgBiAEBEhMKBmNvbHVtbhgHIAEoDUgCiAEBEhwKFG9wZW5faW5fZmlsZV9tYW5hZ2VyGAggASgIQgcKBV9saW5lQgsKCV9lbmRfbGluZUIJCgdfY29sdW1uIs4BChRDaGF0RmlsZU9wZW5SZXNwb25zZRIOCgZhY3Rpb24YASABKAkSDAoEa2luZBgCIAEoCRIPCgd3b3JrZGlyGAMgASgJEgwKBHBhdGgYBCABKAkSEQoEbGluZRgFIAEoDUgAiAEBEhUKCGVuZF9saW5lGAYgASgNSAGIAQESEwoGY29sdW1uGAcgASgNSAKIAQESGQoRb3V0c2lkZV93b3Jrc3BhY2UYCCABKAhCBwoFX2xpbmVCCwoJX2VuZF9saW5lQgkKB19jb2x1bW4izgEKEkZzV3JpdGVUZXh0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkSDwoHY29udGVudBgDIAEoCRIMCgRtb2RlGAQgASgJEhkKEWV4cGVjdGVkX210aW1lX21zGAUgASgEEh0KFWV4cGVjdGVkX2NvbnRlbnRfaGFzaBgGIAEoCRIdChVoYXNfZXhwZWN0ZWRfbXRpbWVfbXMYByABKAgSIQoZaGFzX2V4cGVjdGVkX2NvbnRlbnRfaGFzaBgIIAEoCCKdAQoTRnNXcml0ZVRleHRSZXNwb25zZRIMCgRwYXRoGAEgASgJEgwKBG1vZGUYAiABKAkSFgoOZXhpc3RlZF9iZWZvcmUYAyABKAgSFQoNYnl0ZXNfd3JpdHRlbhgEIAEoBBIQCghtdGltZV9tcxgFIAEoBBIUCgxjb250ZW50X2hhc2gYBiABKAkSEwoLdG90YWxfbGluZXMYByABKAQiMwoSRnNDcmVhdGVEaXJSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSDAoEcGF0aBgCIAEoCSIxChNGc0NyZWF0ZURpclJlc3BvbnNlEgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCSJGCg9Gc1JlbmFtZVJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIRCglmcm9tX3BhdGgYAiABKAkSDwoHdG9fcGF0aBgDIAEoCSJBChBGc1JlbmFtZVJlc3BvbnNlEhEKCWZyb21fcGF0aBgBIAEoCRIMCgRwYXRoGAIgASgJEgwKBGtpbmQYAyABKAkiMAoPRnNEZWxldGVSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSDAoEcGF0aBgCIAEoCSIuChBGc0RlbGV0ZVJlc3BvbnNlEgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCSIgCgtQaW5nUmVxdWVzdBIRCgl0aW1lc3RhbXAYASABKAMiIQoMUG9uZ1Jlc3BvbnNlEhEKCXRpbWVzdGFtcBgBIAEoAyIuCg1FcnJvclJlc3BvbnNlEgwKBGNvZGUYASABKAUSDwoHbWVzc2FnZRgCIAEoCSK+AQoVUHJvdmlkZXJNb2RlbHNSZXF1ZXN0EhUKDXByb3ZpZGVyX3R5cGUYASABKAkSEAoIYmFzZV91cmwYAiABKAkSDwoHYXBpX2tleRgDIAEoCRIYChB1c2Vfc3lzdGVtX3Byb3h5GAQgASgIEhIKCm1vZGVsc191cmwYBSABKAkSEwoLcHJvdmlkZXJfaWQYBiABKAkSGAoLaXNfZnVsbF91cmwYByABKAhIAIgBAUIOCgxfaXNfZnVsbF91cmwiLQoWUHJvdmlkZXJNb2RlbHNSZXNwb25zZRITCgttb2RlbHNfanNvbhgBIAEoCSJRChRQcm92aWRlclVzYWdlUmVxdWVzdBITCgtwcm92aWRlcl9pZBgBIAEoCRIPCgdyZWZyZXNoGAIgASgIEhMKC2NvbmZpZ19qc29uGAMgASgJIiwKFVByb3ZpZGVyVXNhZ2VSZXNwb25zZRITCgtyZXN1bHRfanNvbhgBIAEoCSKIAQoQQ2hhdEluZ3Jlc3NCYXRjaBIOCgZydW5faWQYASABKAkSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEhEKCWZpcnN0X3NlcRgDIAEoBBI4CgdyZWNvcmRzGAQgAygLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NSZWNvcmQimgIKEUNoYXRJbmdyZXNzUmVjb3JkEjcKBWRlbHRhGAEgASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NEZWx0YUgAEkEKCmNoZWNrcG9pbnQYAiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc0NoZWNrcG9pbnRIABI9Cgh0ZXJtaW5hbBgDIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzVGVybWluYWxIABI/CgloZWFydGJlYXQYBCABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc0hlYXJ0YmVhdEgAQgkKB3BheWxvYWQiOQoQQ2hhdEluZ3Jlc3NEZWx0YRISCgpldmVudF9qc29uGAEgASgJEhEKCXdvcmtlcl9pZBgCIAEoCSIqChRDaGF0SW5ncmVzc0hlYXJ0YmVhdBISCgp1cGRhdGVkX2F0GAEgASgDIsQBChVDaGF0SW5ncmVzc0NoZWNrcG9pbnQSGgoSY292ZXJzX3Rocm91Z2hfc2VxGAEgASgEEhAKCHJldmlzaW9uGAIgASgEEh0KFWNvbXByZXNzZWRfcHJvamVjdGlvbhgDIAEoDBIaChJ1bmNvbXByZXNzZWRfYnl0ZXMYBCABKAQSDgoGc2hhMjU2GAUgASgJEhgKEGNvbnRlbnRfY29tcGxldGUYBiABKAgSGAoQaGlzdG9yeV9yZXF1aXJlZBgHIAEoCCL8AQoTQ2hhdEluZ3Jlc3NUZXJtaW5hbBIaChJjb3ZlcnNfdGhyb3VnaF9zZXEYASABKAQSEAoIcmV2aXNpb24YAiABKAQSHQoVY29tcHJlc3NlZF9wcm9qZWN0aW9uGAMgASgMEhoKEnVuY29tcHJlc3NlZF9ieXRlcxgEIAEoBBIOCgZzaGEyNTYYBSABKAkSGAoQY29udGVudF9jb21wbGV0ZRgGIAEoCBIYChBoaXN0b3J5X3JlcXVpcmVkGAcgASgIEg0KBXN0YXRlGAggASgJEhIKCmVycm9yX2NvZGUYCSABKAkSFQoNZXJyb3JfbWVzc2FnZRgKIAEoCSJNChFDaGF0SW5ncmVzc1Jlc3VtZRI4CgRydW5zGAEgAygLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NSdW5SZXN1bWUi1QEKFENoYXRJbmdyZXNzUnVuUmVzdW1lEg4KBnJ1bl9pZBgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSFwoPcmVwbGF5X2Zyb21fc2VxGAMgASgEEhoKEnJlcGxheV90aHJvdWdoX3NlcRgEIAEoBBIQCghuZXh0X3NlcRgFIAEoBBIdChVsYXRlc3RfY2hlY2twb2ludF9zZXEYBiABKAQSFAoMdGVybWluYWxfc2VxGAcgASgEEhgKEHRlcm1pbmFsX3BlbmRpbmcYCCABKAgizgEKE0NoYXRJbmdyZXNzRnJhZ21lbnQSDgoGcnVuX2lkGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRISCgpzb3VyY2Vfc2VxGAMgASgEEhYKDmZyYWdtZW50X2luZGV4GAQgASgNEhYKDmZyYWdtZW50X2NvdW50GAUgASgNEhwKFGVuY29kZWRfcmVjb3JkX2NodW5rGAYgASgMEhwKFGVuY29kZWRfcmVjb3JkX2J5dGVzGAcgASgEEg4KBnNoYTI1NhgIIAEoCSLcAgoOQ2hhdEluZ3Jlc3NBY2sSDgoGcnVuX2lkGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRIZChFjb21taXR0ZWRfdGhyb3VnaBgDIAEoBBIVCg1leHBlY3RlZF9uZXh0GAQgASgEEjsKBmFjdGlvbhgFIAEoDjIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzQWNrLkFjdGlvbhIaChJ0ZXJtaW5hbF9jb21taXR0ZWQYBiABKAgSEgoKZXJyb3JfY29kZRgHIAEoCRIVCg1lcnJvcl9tZXNzYWdlGAggASgJImsKBkFjdGlvbhIWChJBQ1RJT05fVU5TUEVDSUZJRUQQABIMCghDT05USU5VRRABEhgKFFJFUExBWV9GUk9NX0VYUEVDVEVEEAISEwoPU0VORF9DSEVDS1BPSU5UEAMSDAoIUkVKRUNURUQQBCI8ChRIaXN0b3J5U2V0Q3dkUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSCwoDY3dkGAIgASgJIlgKFUhpc3RvcnlTZXRDd2RSZXNwb25zZRI/Cgxjb252ZXJzYXRpb24YASABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5ImYKF1dvcmtzcGFjZVJvb3RHcmFudERyYWZ0Eg8KAmlkGAEgASgJSACIAQESDQoFYWxpYXMYAiABKAkSFAoMZGlzcGxheV9wYXRoGAMgASgJEg4KBmFjY2VzcxgEIAEoCUIFCgNfaWQi0gEKEldvcmtzcGFjZVJvb3RHcmFudBIKCgJpZBgBIAEoCRISCgpwcm9qZWN0X2lkGAIgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAyABKAkSDQoFYWxpYXMYBCABKAkSFAoMZGlzcGxheV9wYXRoGAUgASgJEhYKDmNhbm9uaWNhbF9wYXRoGAYgASgJEg4KBmFjY2VzcxgHIAEoCRINCgVzdGF0ZRgIIAEoCRISCgpjcmVhdGVkX2F0GAkgASgDEhIKCnVwZGF0ZWRfYXQYCiABKAMilQEKGldvcmtzcGFjZVJvb3RHcmFudHNSZXF1ZXN0Eg4KBmFjdGlvbhgBIAEoCRISCgpwcm9qZWN0X2lkGAIgASgJEhQKDHByb2plY3RfcGF0aBgDIAEoCRI9CgZncmFudHMYBCADKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Xb3Jrc3BhY2VSb290R3JhbnREcmFmdCJXChtXb3Jrc3BhY2VSb290R3JhbnRzUmVzcG9uc2USOAoGZ3JhbnRzGAEgAygLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuV29ya3NwYWNlUm9vdEdyYW50IjwKF0NoZWNrcG9pbnRFeHBlY3RlZEVudHJ5EgsKA2tleRgBIAEoCRIUCgxjdXJyZW50X2hhc2gYAiABKAkiqQEKEUNoZWNrcG9pbnRSZXF1ZXN0Eg4KBmFjdGlvbhgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSEAoIdHVybl9zZXEYAyABKAQSGAoQYXV0aG9yaXplZF9yb290cxgEIAMoCRI/CghleHBlY3RlZBgFIAMoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoZWNrcG9pbnRFeHBlY3RlZEVudHJ5IjkKEkNoZWNrcG9pbnRSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSEwoLcmVzdWx0X2pzb24YAiABKAkq5QEKGEltcG9ydERpcmVjdG9yeU9wZXJhdGlvbhIqCiZJTVBPUlRfRElSRUNUT1JZX09QRVJBVElPTl9VTlNQRUNJRklFRBAAEiQKIElNUE9SVF9ESVJFQ1RPUllfT1BFUkFUSU9OX1NUQVJUEAESKgomSU1QT1JUX0RJUkVDVE9SWV9PUEVSQVRJT05fV1JJVEVfQ0hVTksQAhIlCiFJTVBPUlRfRElSRUNUT1JZX09QRVJBVElPTl9DT01NSVQQAxIkCiBJTVBPUlRfRElSRUNUT1JZX09QRVJBVElPTl9BQk9SVBAEKsYECg9UdW5uZWxGcmFtZUtpbmQSIQodVFVOTkVMX0ZSQU1FX0tJTkRfVU5TUEVDSUZJRUQQABIoCiRUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFUVVFU1RfU1RBUlQQARInCiNUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFUVVFU1RfQk9EWRACEiYKIlRVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVRVUVTVF9FTkQQAxIpCiVUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFU1BPTlNFX1NUQVJUEAQSKAokVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVNQT05TRV9CT0RZEAUSJwojVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVNQT05TRV9FTkQQBhIdChlUVU5ORUxfRlJBTUVfS0lORF9XU19ESUFMEAcSIAocVFVOTkVMX0ZSQU1FX0tJTkRfV1NfRElBTF9PSxAIEiMKH1RVTk5FTF9GUkFNRV9LSU5EX1dTX0RJQUxfRVJST1IQCRIeChpUVU5ORUxfRlJBTUVfS0lORF9XU19GUkFNRRAKEh4KGlRVTk5FTF9GUkFNRV9LSU5EX1dTX0NMT1NFEAsSGwoXVFVOTkVMX0ZSQU1FX0tJTkRfRVJST1IQDBIcChhUVU5ORUxfRlJBTUVfS0lORF9DQU5DRUwQDRIaChZUVU5ORUxfRlJBTUVfS0lORF9QSU5HEA4SGgoWVFVOTkVMX0ZSQU1FX0tJTkRfUE9ORxAPKoEBChNUdW5uZWxXc01lc3NhZ2VUeXBlEiYKIlRVTk5FTF9XU19NRVNTQUdFX1RZUEVfVU5TUEVDSUZJRUQQABIfChtUVU5ORUxfV1NfTUVTU0FHRV9UWVBFX1RFWFQQARIhCh1UVU5ORUxfV1NfTUVTU0FHRV9UWVBFX0JJTkFSWRACQkBaPmdpdGh1Yi5jb20vbGl2ZWFnZW50L2FnZW50LWdhdGV3YXkvaW50ZXJuYWwvcHJvdG8vdjI7Z2F0ZXdheXYyYgZwcm90bzM"); /** * @generated from message liveagent.gateway.v2.GatewayEnvelope @@ -3061,6 +3061,14 @@ export type ChatRequest = Message<"liveagent.gateway.v2.ChatRequest"> & { * @generated from field: string queue_policy = 10; */ queuePolicy: string; + + /** + * 命令安全模式(ask/auto/sandbox/sandboxOffline)。远端 WebUI 直带,桌面端据此 + * 覆盖本地 settings.system.commandSafetyMode;空串表示未指定(回落本地设置)。 + * + * @generated from field: string command_safety_mode = 11; + */ + commandSafetyMode: string; }; /** diff --git a/crates/agent-gui/src-tauri/src/commands/app/system.rs b/crates/agent-gui/src-tauri/src/commands/app/system.rs index 94baf0208..a32fadb72 100644 --- a/crates/agent-gui/src-tauri/src/commands/app/system.rs +++ b/crates/agent-gui/src-tauri/src/commands/app/system.rs @@ -2486,6 +2486,11 @@ pub(crate) fn system_create_project_folder_sync( }) } +#[tauri::command(rename_all = "snake_case")] +pub fn system_sandbox_capability() -> crate::runtime::sandbox::SandboxCapability { + crate::runtime::sandbox::capability() +} + #[tauri::command(rename_all = "snake_case")] pub async fn system_pick_folder(initial_workdir: Option) -> Result, String> { tauri::async_runtime::spawn_blocking(move || { diff --git a/crates/agent-gui/src-tauri/src/commands/automation/hook.rs b/crates/agent-gui/src-tauri/src/commands/automation/hook.rs index a806c15bf..0fa886df4 100644 --- a/crates/agent-gui/src-tauri/src/commands/automation/hook.rs +++ b/crates/agent-gui/src-tauri/src/commands/automation/hook.rs @@ -166,6 +166,8 @@ pub(crate) fn run_hook_script_sync( None, token.clone(), &context, + // Hook 脚本是用户显式配置的自动化,不属于模型驱动面,不套沙箱。 + None, ); if let (Some(scope), Some(token)) = (&scope_id, &token) { diff --git a/crates/agent-gui/src-tauri/src/commands/config/settings/mod.rs b/crates/agent-gui/src-tauri/src/commands/config/settings/mod.rs index ed0308a78..fd29bed50 100644 --- a/crates/agent-gui/src-tauri/src/commands/config/settings/mod.rs +++ b/crates/agent-gui/src-tauri/src/commands/config/settings/mod.rs @@ -32,6 +32,9 @@ const SYSTEM_WORKDIR_KEY: &str = "workdir"; // 工具审批策略(按工具名/`group:`/`server:` 键 → allow/ask/deny)。此前未纳入 // 保存白名单,导致重启后设置丢失;补入本键持久化。 const SYSTEM_TOOL_POLICIES_KEY: &str = "toolPolicies"; +// 命令执行方式("ask"/"auto"/"sandbox"/"sandboxOffline"),与前端 +// SystemSettings.commandSafetyMode 对齐;sandbox* 由执行层映射为 OS 沙箱参数。 +const SYSTEM_COMMAND_SAFETY_MODE_KEY: &str = "commandSafetyMode"; const SYSTEM_WORKSPACE_PROJECTS_KEY: &str = "workspaceProjects"; const SYSTEM_WORKSPACE_PROJECT_GROUPS_KEY: &str = "workspaceProjectGroups"; const SYSTEM_ACTIVE_WORKSPACE_PROJECT_ID_KEY: &str = "activeWorkspaceProjectId"; diff --git a/crates/agent-gui/src-tauri/src/commands/config/settings/system.rs b/crates/agent-gui/src-tauri/src/commands/config/settings/system.rs index 00954b9ca..b27ab7f97 100644 --- a/crates/agent-gui/src-tauri/src/commands/config/settings/system.rs +++ b/crates/agent-gui/src-tauri/src/commands/config/settings/system.rs @@ -459,10 +459,25 @@ fn system_value_with_defaults(raw: Option, default_workdir: &str) -> Valu SYSTEM_SYSTEM_PROXY_KEY.to_string(), normalize_system_proxy_value(system.get(SYSTEM_SYSTEM_PROXY_KEY)), ); + system.insert( + SYSTEM_COMMAND_SAFETY_MODE_KEY.to_string(), + normalize_command_safety_mode_value(system.get(SYSTEM_COMMAND_SAFETY_MODE_KEY)), + ); Value::Object(system) } +/// "ask" | "auto" | "sandbox" | "sandboxOffline",缺省 "auto",与前端 +/// normalizeCommandSafetyMode 一致。 +fn normalize_command_safety_mode_value(raw: Option<&Value>) -> Value { + let mode = raw + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| matches!(*value, "ask" | "auto" | "sandbox" | "sandboxOffline")) + .unwrap_or("auto"); + Value::String(mode.to_string()) +} + fn load_system_with_defaults(conn: &Connection, default_workdir: &str) -> Result { Ok(system_value_with_defaults( load_system(conn)?, @@ -500,6 +515,7 @@ fn save_system_with_default_workdir( SYSTEM_EXECUTION_MODE_KEY, SYSTEM_WORKDIR_KEY, SYSTEM_TOOL_POLICIES_KEY, + SYSTEM_COMMAND_SAFETY_MODE_KEY, SYSTEM_WORKSPACE_PROJECTS_KEY, SYSTEM_WORKSPACE_PROJECT_GROUPS_KEY, SYSTEM_ACTIVE_WORKSPACE_PROJECT_ID_KEY, diff --git a/crates/agent-gui/src-tauri/src/commands/config/settings/tests.rs b/crates/agent-gui/src-tauri/src/commands/config/settings/tests.rs index 8b935cb50..5cf2af893 100644 --- a/crates/agent-gui/src-tauri/src/commands/config/settings/tests.rs +++ b/crates/agent-gui/src-tauri/src/commands/config/settings/tests.rs @@ -1129,12 +1129,13 @@ mod tests { }; let loaded = load_system(&conn).expect("load system"); - assert_eq!(row_count, 11); + assert_eq!(row_count, 12); assert_eq!( keys, vec![ SYSTEM_ACTIVE_WORKSPACE_PROJECT_ID_KEY.to_string(), SYSTEM_ARCHIVED_WORKSPACE_PROJECT_PATHS_KEY.to_string(), + SYSTEM_COMMAND_SAFETY_MODE_KEY.to_string(), SYSTEM_EXECUTION_MODE_KEY.to_string(), SYSTEM_HIDDEN_WORKSPACE_PROJECT_PATHS_KEY.to_string(), SYSTEM_MISSING_WORKSPACE_PROJECT_PATHS_KEY.to_string(), @@ -1155,6 +1156,7 @@ mod tests { "missingWorkspaceProjectPaths": [], "archivedWorkspaceProjectPaths": [], "workspaceResourceSettings": {}, + "commandSafetyMode": "auto", "systemProxy": default_system_proxy_json(), "workdir": default_workdir.clone(), "toolPolicies": { "Bash": "ask", "server:docs-mcp": "deny" }, @@ -1453,6 +1455,7 @@ mod tests { "missingWorkspaceProjectPaths": [], "archivedWorkspaceProjectPaths": [], "workspaceResourceSettings": {}, + "commandSafetyMode": "auto", "systemProxy": default_system_proxy_json(), "workdir": "/tmp/liveagent-default-project", "toolPolicies": null, @@ -1506,6 +1509,7 @@ mod tests { "missingWorkspaceProjectPaths": [], "archivedWorkspaceProjectPaths": [], "workspaceResourceSettings": {}, + "commandSafetyMode": "auto", "systemProxy": default_system_proxy_json(), "workdir": "/tmp/liveagent-default-project", "toolPolicies": null, @@ -1541,6 +1545,7 @@ mod tests { "missingWorkspaceProjectPaths": [], "archivedWorkspaceProjectPaths": [], "workspaceResourceSettings": {}, + "commandSafetyMode": "auto", "systemProxy": default_system_proxy_json(), "workdir": "/tmp/liveagent-default-project", "workspaceProjects": [ diff --git a/crates/agent-gui/src-tauri/src/commands/runtime/process.rs b/crates/agent-gui/src-tauri/src/commands/runtime/process.rs index 9a9b0e524..5343930fd 100644 --- a/crates/agent-gui/src-tauri/src/commands/runtime/process.rs +++ b/crates/agent-gui/src-tauri/src/commands/runtime/process.rs @@ -6,8 +6,10 @@ use crate::runtime::managed_process::{ ManagedProcessLogResponse, ManagedProcessRegistry, ManagedProcessSnapshot, ManagedProcessStartResponse, ManagedProcessStatusResponse, ManagedProcessStopResponse, }; +use crate::runtime::sandbox::SandboxOptions; #[tauri::command(rename_all = "snake_case")] +#[allow(clippy::too_many_arguments)] pub fn managed_process_start( registry: State<'_, Arc>, workdir: String, @@ -15,8 +17,20 @@ pub fn managed_process_start( cwd: Option, label: Option, isolated: Option, + sandbox: Option, + sandbox_allow_network: Option, ) -> Result { - registry.start(workdir, command, cwd, label, isolated.unwrap_or(false)) + let sandbox_options = (sandbox == Some(true)).then(|| SandboxOptions { + allow_network: sandbox_allow_network.unwrap_or(true), + }); + registry.start( + workdir, + command, + cwd, + label, + isolated.unwrap_or(false), + sandbox_options, + ) } #[tauri::command(rename_all = "snake_case")] diff --git a/crates/agent-gui/src-tauri/src/commands/runtime/shell.rs b/crates/agent-gui/src-tauri/src/commands/runtime/shell.rs index 663f4496c..8f7bdddbd 100644 --- a/crates/agent-gui/src-tauri/src/commands/runtime/shell.rs +++ b/crates/agent-gui/src-tauri/src/commands/runtime/shell.rs @@ -3,7 +3,10 @@ use std::sync::Arc; use serde::Serialize; use tauri::State; -use crate::runtime::shell_runner::{run_shell_script, ShellRunRegistry, ShellRunResponse}; +use crate::runtime::sandbox::SandboxOptions; +use crate::runtime::shell_runner::{ + run_shell_script_with_envs, ShellRunRegistry, ShellRunResponse, +}; use crate::runtime::shell_session::{ShellSessionManager, ShellSessionResponse}; #[derive(Debug, Serialize)] @@ -12,6 +15,7 @@ pub struct ShellCancelResponse { } #[tauri::command(rename_all = "snake_case")] +#[allow(clippy::too_many_arguments)] pub async fn shell_run( registry: State<'_, Arc>, workdir: String, @@ -21,15 +25,20 @@ pub async fn shell_run( max_timeout_ms: Option, provider_id: Option, run_id: Option, + sandbox: Option, + sandbox_allow_network: Option, ) -> Result { let normalized_run_id = run_id .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()); let cancel_token = normalized_run_id.as_deref().map(|id| registry.register(id)); let registered_token = cancel_token.clone(); + let sandbox_options = (sandbox == Some(true)).then(|| SandboxOptions { + allow_network: sandbox_allow_network.unwrap_or(true), + }); let join_result = tauri::async_runtime::spawn_blocking(move || { - run_shell_script( + run_shell_script_with_envs( workdir, command, cwd, @@ -37,6 +46,8 @@ pub async fn shell_run( max_timeout_ms, provider_id, cancel_token, + &[], + sandbox_options, ) }) .await; diff --git a/crates/agent-gui/src-tauri/src/lib.rs b/crates/agent-gui/src-tauri/src/lib.rs index 1271b1442..fef627b10 100644 --- a/crates/agent-gui/src-tauri/src/lib.rs +++ b/crates/agent-gui/src-tauri/src/lib.rs @@ -257,6 +257,7 @@ macro_rules! app_invoke_handler { commands::system::system_resolve_dropped_workspace_folders, commands::system::system_classify_dropped_paths, commands::system::system_pick_file, + commands::system::system_sandbox_capability, commands::system::system_save_preview_file, commands::system::system_create_project_folder, commands::system::system_import_pasted_texts, diff --git a/crates/agent-gui/src-tauri/src/runtime/managed_process.rs b/crates/agent-gui/src-tauri/src/runtime/managed_process.rs index 01b29cf0f..1c58a3aa4 100644 --- a/crates/agent-gui/src-tauri/src/runtime/managed_process.rs +++ b/crates/agent-gui/src-tauri/src/runtime/managed_process.rs @@ -18,6 +18,7 @@ use crate::runtime::process::{ signal_process_tree_by_pid, terminate_child_process_tree, terminate_process_tree_by_pid, ProcessProbe, }; +use crate::runtime::sandbox::{SandboxOptions, SandboxSpec}; use crate::runtime::shell_runner::spawn_platform_shell_command; use crate::services::gateway::GatewayController; @@ -249,12 +250,17 @@ fn sanitize_rel_cwd(input: Option, workdir: &Path) -> Result Result<(Child, String), String> { +fn spawn_shell_command( + command: &str, + cwd: &Path, + log: File, + sandbox_spec: Option<&SandboxSpec>, +) -> Result<(Child, String), String> { let stderr = log .try_clone() .map_err(|err| format!("Failed to clone process log: {err}"))?; - let spawned = spawn_platform_shell_command(command, cwd, &[], || { + let spawned = spawn_platform_shell_command(command, cwd, &[], sandbox_spec, || { Ok(( Stdio::from(log.try_clone()?), Stdio::from(stderr.try_clone()?), @@ -517,6 +523,7 @@ impl ManagedProcessRegistry { cwd: Option, label: Option, isolated: bool, + sandbox_options: Option, ) -> Result { let command = command.trim().to_string(); if command.is_empty() { @@ -531,7 +538,15 @@ impl ManagedProcessRegistry { .append(true) .open(&log_path) .map_err(|err| format!("Failed to open process log: {err}"))?; - let (child, shell) = spawn_shell_command(&command, &cwd, log)?; + // 写围栏锚定工作区根;dev server 等常驻进程通常要监听端口,是否放网络 + // 由调用方经 SandboxOptions 决定。isolated 常驻进程须在 LiveAgent 退出后 + // 存活,透传给沙箱规格以省略 Linux 的 --die-with-parent 死亡耦合。 + let sandbox_spec = sandbox_options.map(|options| { + let mut spec = SandboxSpec::from_options(workdir.clone(), options); + spec.isolated = isolated; + spec + }); + let (child, shell) = spawn_shell_command(&command, &cwd, log, sandbox_spec.as_ref())?; let pid = child.id(); let entry = ManagedProcessEntry { id: id.clone(), @@ -960,6 +975,7 @@ mod tests { None, Some("test process".to_string()), false, + None, ) .expect("process should start"); let process_id = started.process.id.clone(); @@ -1003,6 +1019,7 @@ mod tests { None, Some("drop test process".to_string()), false, + None, ) .expect("process should start"); let pid = started.process.pid; @@ -1028,6 +1045,7 @@ mod tests { None, None, false, + None, ) .expect("process should start"); let pid = started.process.pid; @@ -1061,6 +1079,7 @@ mod tests { None, None, false, + None, ) .expect("process should start"); let pid = started.process.pid; @@ -1093,6 +1112,7 @@ mod tests { None, None, false, + None, ) .expect("process should start"); let pid = started.process.pid; @@ -1122,6 +1142,7 @@ mod tests { None, None, false, + None, ) .expect("plain process should start"); let isolated = registry @@ -1131,6 +1152,7 @@ mod tests { None, None, true, + None, ) .expect("isolated process should start"); @@ -1163,6 +1185,7 @@ mod tests { None, None, false, + None, ) .expect("plain process should start"); let isolated = registry @@ -1172,6 +1195,7 @@ mod tests { None, Some("isolated service".to_string()), true, + None, ) .expect("isolated process should start"); let ids = ( @@ -1229,6 +1253,7 @@ mod tests { None, None, false, + None, ) .expect("short process should start"); let running = registry @@ -1238,6 +1263,7 @@ mod tests { None, None, false, + None, ) .expect("long process should start"); diff --git a/crates/agent-gui/src-tauri/src/runtime/mod.rs b/crates/agent-gui/src-tauri/src/runtime/mod.rs index 6395c5d83..98999d172 100644 --- a/crates/agent-gui/src-tauri/src/runtime/mod.rs +++ b/crates/agent-gui/src-tauri/src/runtime/mod.rs @@ -3,6 +3,7 @@ pub mod managed_process_journal; pub mod platform; pub mod process; pub mod project_path; +pub mod sandbox; pub mod sftp; pub mod shell_runner; pub mod shell_session; diff --git a/crates/agent-gui/src-tauri/src/runtime/sandbox.rs b/crates/agent-gui/src-tauri/src/runtime/sandbox.rs new file mode 100644 index 000000000..024f49826 --- /dev/null +++ b/crates/agent-gui/src-tauri/src/runtime/sandbox.rs @@ -0,0 +1,498 @@ +//! OS 级沙箱(沙箱模式 v1):模型驱动的 Bash / ManagedProcess 在生成子进程前 +//! 由平台原生机制包裹——macOS 走 Seatbelt(/usr/bin/sandbox-exec),Linux 走 +//! bubblewrap(bwrap),Windows 暂不支持(受限令牌 + Job Object + WFP 路线待实现)。 +//! +//! 语义为 workspace-write:读默认放行(工具链/依赖散布全盘,default-deny 不现实), +//! 写仅限工作区根 + 临时目录,敏感目录(~/.ssh、应用配置库等)读写全掩蔽,网络可 +//! 整体关断。fail-closed:沙箱被请求而平台机制不可用时直接报错,绝不静默降级为 +//! 无沙箱执行。 + +use serde::Serialize; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, Copy)] +pub(crate) struct SandboxOptions { + pub allow_network: bool, +} + +/// 展开后的沙箱规格:write_root 是允许写入的工作区根(须为 canonicalize 后的 +/// 绝对路径,shell_runner / managed_process 的 workdir 校验已保证)。 +#[derive(Debug, Clone)] +pub(crate) struct SandboxSpec { + pub write_root: PathBuf, + pub allow_network: bool, + /// isolated 常驻进程须在 LiveAgent 退出后继续存活(managed_process 的 isolated + /// 语义),因此 Linux bwrap 不能加 `--die-with-parent`。仅 bubblewrap 后端消费; + /// macOS/Windows 无父进程死亡耦合,不读取本字段。 + #[cfg_attr(any(target_os = "macos", windows), allow(dead_code))] + pub isolated: bool, +} + +impl SandboxSpec { + pub(crate) fn from_options(write_root: PathBuf, options: SandboxOptions) -> Self { + Self { + write_root, + allow_network: options.allow_network, + // 默认非 isolated(Bash 工具子进程随 LiveAgent 退出而终止); + // managed_process 的 isolated 常驻进程构造后显式置 true。 + isolated: false, + } + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct SandboxCapability { + pub supported: bool, + pub mechanism: &'static str, + pub platform: &'static str, + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, +} + +/// 敏感目录掩蔽表(相对 home)。应用自身配置目录(provider 密钥、审批策略所在的 +/// config.sqlite)一并掩蔽;默认工作区在其内部,由 write_root 的后置 allow 规则 +/// 重新放行,不受影响。 +fn sensitive_home_subdirs() -> [&'static str; 4] { + [".ssh", ".aws", ".gnupg", ".config/gh"] +} + +fn app_config_dir() -> Option { + dirs::home_dir().map(|home| home.join(format!(".{}", env!("CARGO_PKG_NAME")))) +} + +fn sensitive_dirs() -> Vec { + let mut dirs_out = Vec::new(); + if let Some(home) = dirs::home_dir() { + for sub in sensitive_home_subdirs() { + dirs_out.push(home.join(sub)); + } + } + if let Some(config) = app_config_dir() { + dirs_out.push(config); + } + dirs_out +} + +fn canonical_or_self(path: &Path) -> PathBuf { + std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()) +} + +/// fail-closed 工作区校验(P1#2):拒绝会让写围栏重新暴露敏感目录的工作区。 +/// +/// 写围栏对 write_root 有后置 re-allow(macOS)/后置 --bind(Linux),因此: +/// - **祖先或相等**:工作区包含或等于任一敏感目录(如工作区取 home 或 /), +/// re-allow 会把该敏感目录重新放行 → 一律拒绝。 +/// - **后代**:工作区落在敏感目录内部。凭据目录(~/.ssh/.aws/.gnupg/.config/gh) +/// 下的工作区一律拒绝;应用配置目录(~/.liveagent)豁免——默认工作区 +/// ~/.liveagent/default-project 正位于其内,拒绝它会直接打断开箱即用。 +fn validate_workspace(write_root: &Path) -> Result<(), String> { + let root = canonical_or_self(write_root); + let app_config = app_config_dir().map(|p| canonical_or_self(&p)); + + for dir in sensitive_dirs() { + let dir = canonical_or_self(&dir); + if dir.starts_with(&root) { + return Err(format!( + "Sandbox refuses workspace \"{}\": it encloses or equals the sensitive directory \ +\"{}\", which the workspace write fence would re-expose. Choose a workspace that does not \ +contain credential or app-config directories.", + root.display(), + dir.display() + )); + } + if root.starts_with(&dir) { + // 应用配置目录内部豁免(默认工作区在此),其余敏感目录内部一律拒绝。 + if app_config.as_deref() == Some(dir.as_path()) { + continue; + } + return Err(format!( + "Sandbox refuses workspace \"{}\": it lives inside the sensitive directory \"{}\". \ +Choose a workspace outside credential directories.", + root.display(), + dir.display() + )); + } + } + Ok(()) +} + +/// 临时目录允许写入集合:TMPDIR(macOS 上归并到 /var/folders 的用户私有目录, +/// 取其父级同时覆盖 confstr 缓存目录)、std::env::temp_dir、以及系统级 tmp。 +fn writable_temp_dirs() -> Vec { + let mut out: Vec = Vec::new(); + let mut push_canonical = |path: PathBuf| { + let canonical = std::fs::canonicalize(&path).unwrap_or(path); + if !out.iter().any(|existing| *existing == canonical) { + out.push(canonical); + } + }; + + if let Ok(tmpdir) = std::env::var("TMPDIR") { + let tmpdir = PathBuf::from(tmpdir.trim_end_matches('/')); + if tmpdir.is_absolute() && tmpdir.is_dir() { + // /var/folders/xx/yyy/T → 放行父级 /var/folders/xx/yyy,同时覆盖 + // DARWIN_USER_CACHE_DIR(…/C,clang 模块缓存等会写它)。 + #[cfg(target_os = "macos")] + if let Some(parent) = tmpdir.parent() { + push_canonical(parent.to_path_buf()); + } + push_canonical(tmpdir); + } + } + push_canonical(std::env::temp_dir()); + for path in ["/tmp", "/var/tmp", "/private/tmp", "/private/var/tmp"] { + let path = Path::new(path); + if path.is_dir() { + push_canonical(path.to_path_buf()); + } + } + out +} + +pub fn capability() -> SandboxCapability { + platform::capability() +} + +/// 把即将执行的 (program, args) 包进平台沙箱,返回替换后的 +/// (program, args, mechanism)。平台不支持或依赖缺失时报错(fail-closed)。 +pub(crate) fn wrap_command( + spec: &SandboxSpec, + program: &Path, + args: &[String], +) -> Result<(PathBuf, Vec, &'static str), String> { + let capability = capability(); + if !capability.supported { + return Err(format!( + "Sandbox mode is enabled but unavailable on this platform: {}. \ +Disable sandbox mode in Settings → System, or resolve the issue and retry.", + capability.reason.as_deref().unwrap_or("unsupported platform") + )); + } + validate_workspace(&spec.write_root)?; + platform::wrap_command(spec, program, args) +} + +#[cfg(target_os = "macos")] +mod platform { + use super::*; + + const SANDBOX_EXEC: &str = "/usr/bin/sandbox-exec"; + + pub(super) fn capability() -> SandboxCapability { + if Path::new(SANDBOX_EXEC).exists() { + SandboxCapability { + supported: true, + mechanism: "seatbelt", + platform: "macos", + reason: None, + } + } else { + SandboxCapability { + supported: false, + mechanism: "seatbelt", + platform: "macos", + reason: Some(format!("{SANDBOX_EXEC} not found")), + } + } + } + + /// Seatbelt 字符串字面量转义:反斜杠与双引号。 + fn escape(path: &Path) -> String { + path.to_string_lossy().replace('\\', "\\\\").replace('"', "\\\"") + } + + fn subpath_filters(paths: &[PathBuf]) -> String { + paths + .iter() + .map(|p| format!("(subpath \"{}\")", escape(p))) + .collect::>() + .join(" ") + } + + /// allow-default + 写入围栏的 Seatbelt profile。规则匹配以“最后命中者优先”, + /// 顺序:全局 allow → 全盘写 deny → 工作区/临时目录写 allow → 设备节点写 + /// allow → 敏感目录读 deny → 工作区读写 re-allow(默认工作区位于应用配置目录 + /// 内,须排在敏感目录 deny 之后)→ 可选网络 deny。 + pub(super) fn seatbelt_profile(spec: &SandboxSpec) -> String { + let mut writable = vec![spec.write_root.clone()]; + writable.extend(writable_temp_dirs()); + + let mut profile = String::from("(version 1)\n(allow default)\n(deny file-write*)\n"); + profile.push_str(&format!("(allow file-write* {})\n", subpath_filters(&writable))); + profile.push_str( + "(allow file-write-data file-ioctl (literal \"/dev/null\") (literal \"/dev/zero\") \ +(literal \"/dev/tty\") (literal \"/dev/stdout\") (literal \"/dev/stderr\") \ +(literal \"/dev/dtracehelper\"))\n(allow file-write* (subpath \"/dev/fd\"))\n", + ); + let sensitive = sensitive_dirs(); + if !sensitive.is_empty() { + profile.push_str(&format!("(deny file-read* {})\n", subpath_filters(&sensitive))); + } + profile.push_str(&format!( + "(allow file-read* file-write* (subpath \"{}\"))\n", + escape(&spec.write_root) + )); + if !spec.allow_network { + profile.push_str("(deny network*)\n"); + } + profile + } + + pub(super) fn wrap_command( + spec: &SandboxSpec, + program: &Path, + args: &[String], + ) -> Result<(PathBuf, Vec, &'static str), String> { + let mut out = vec!["-p".to_string(), seatbelt_profile(spec)]; + out.push(program.to_string_lossy().into_owned()); + out.extend(args.iter().cloned()); + Ok((PathBuf::from(SANDBOX_EXEC), out, "seatbelt")) + } +} + +#[cfg(all(not(windows), not(target_os = "macos")))] +mod platform { + use super::*; + use std::process::Command; + use std::sync::OnceLock; + + static CAPABILITY: OnceLock = OnceLock::new(); + + fn probe() -> SandboxCapability { + let unsupported = |reason: String| SandboxCapability { + supported: false, + mechanism: "bubblewrap", + platform: "linux", + reason: Some(reason), + }; + // 探测真实可用性(容器/受限内核里 bwrap 可能存在但无法建 namespace)。 + match Command::new("bwrap") + .args([ + "--die-with-parent", + "--unshare-pid", + "--ro-bind", + "/", + "/", + "--proc", + "/proc", + "--dev", + "/dev", + "--", + "/bin/true", + ]) + .output() + { + Ok(output) if output.status.success() => SandboxCapability { + supported: true, + mechanism: "bubblewrap", + platform: "linux", + reason: None, + }, + Ok(output) => unsupported(format!( + "bubblewrap probe failed: {}", + String::from_utf8_lossy(&output.stderr).trim() + )), + Err(err) => unsupported(format!( + "bubblewrap (bwrap) is not available: {err}. Install it, e.g. `apt install bubblewrap`." + )), + } + } + + pub(super) fn capability() -> SandboxCapability { + CAPABILITY.get_or_init(probe).clone() + } + + pub(super) fn bwrap_args(spec: &SandboxSpec) -> Vec { + // isolated 常驻进程须在 LiveAgent 退出后存活,不能与父进程死亡耦合; + // 非 isolated(Bash 工具子进程)保持 --die-with-parent,避免遗留孤儿。 + let mut args: Vec = Vec::new(); + if !spec.isolated { + args.push("--die-with-parent".to_string()); + } + args.extend( + [ + "--unshare-pid", + "--ro-bind", + "/", + "/", + "--proc", + "/proc", + "--dev", + "/dev", + ] + .into_iter() + .map(String::from), + ); + + for tmp in writable_temp_dirs() { + let tmp = tmp.to_string_lossy().into_owned(); + args.extend(["--bind".to_string(), tmp.clone(), tmp]); + } + // 掩蔽须在 write_root 绑定之前:默认工作区位于应用配置目录内,后置的 + // --bind 会在 tmpfs 掩蔽之上重新暴露工作区。 + for dir in sensitive_dirs() { + if dir.is_dir() { + args.extend(["--tmpfs".to_string(), dir.to_string_lossy().into_owned()]); + } + } + let root = spec.write_root.to_string_lossy().into_owned(); + args.extend(["--bind".to_string(), root.clone(), root]); + if !spec.allow_network { + args.push("--unshare-net".to_string()); + } + args.push("--".to_string()); + args + } + + pub(super) fn wrap_command( + spec: &SandboxSpec, + program: &Path, + args: &[String], + ) -> Result<(PathBuf, Vec, &'static str), String> { + let mut out = bwrap_args(spec); + out.push(program.to_string_lossy().into_owned()); + out.extend(args.iter().cloned()); + Ok((PathBuf::from("bwrap"), out, "bubblewrap")) + } +} + +#[cfg(windows)] +mod platform { + use super::*; + + pub(super) fn capability() -> SandboxCapability { + SandboxCapability { + supported: false, + mechanism: "none", + platform: "windows", + reason: Some( + "Windows sandbox (restricted token + job object + WFP) is not implemented yet" + .to_string(), + ), + } + } + + pub(super) fn wrap_command( + _spec: &SandboxSpec, + _program: &Path, + _args: &[String], + ) -> Result<(PathBuf, Vec, &'static str), String> { + Err("Windows sandbox is not implemented yet".to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(target_os = "macos")] + #[test] + fn seatbelt_profile_contains_write_root_and_ordering() { + let spec = SandboxSpec { + write_root: PathBuf::from("/tmp/liveagent \"quoted\" ws"), + allow_network: false, + isolated: false, + }; + let profile = platform::seatbelt_profile(&spec); + assert!(profile.starts_with("(version 1)\n(allow default)\n(deny file-write*)\n")); + assert!(profile.contains("liveagent \\\"quoted\\\" ws")); + assert!(profile.ends_with("(deny network*)\n")); + // 工作区 re-allow 必须位于敏感目录 deny 之后(最后命中者优先)。 + let deny_read = profile.find("(deny file-read*").expect("deny file-read rule"); + let reallow = profile + .find("(allow file-read* file-write*") + .expect("workspace re-allow rule"); + assert!(reallow > deny_read); + } + + #[cfg(target_os = "macos")] + #[test] + fn seatbelt_network_allowed_omits_network_rule() { + let spec = SandboxSpec { + write_root: PathBuf::from("/tmp/ws"), + allow_network: true, + isolated: false, + }; + assert!(!platform::seatbelt_profile(&spec).contains("network")); + } + + #[cfg(all(not(windows), not(target_os = "macos")))] + #[test] + fn bwrap_args_order_masks_before_write_root_bind() { + let spec = SandboxSpec { + write_root: PathBuf::from("/home/user/project"), + allow_network: false, + isolated: false, + }; + let args = platform::bwrap_args(&spec); + assert_eq!(args.first().map(String::as_str), Some("--die-with-parent")); + assert!(args.contains(&"--unshare-net".to_string())); + assert_eq!(args.last().map(String::as_str), Some("--")); + let root_bind = args + .iter() + .position(|a| a == "/home/user/project") + .expect("write root bind"); + if let Some(mask) = args.iter().position(|a| a == "--tmpfs") { + assert!(mask < root_bind); + } + } + + // P1#3:isolated 常驻进程不能与父进程死亡耦合,bwrap 须省略 --die-with-parent。 + #[cfg(all(not(windows), not(target_os = "macos")))] + #[test] + fn bwrap_args_isolated_omits_die_with_parent() { + let base = PathBuf::from("/home/user/project"); + let attached = platform::bwrap_args(&SandboxSpec { + write_root: base.clone(), + allow_network: false, + isolated: false, + }); + assert!(attached.contains(&"--die-with-parent".to_string())); + + let isolated = platform::bwrap_args(&SandboxSpec { + write_root: base, + allow_network: false, + isolated: true, + }); + assert!(!isolated.contains(&"--die-with-parent".to_string())); + // 省略死亡耦合后,其余围栏(pid namespace、根只读绑定)保持不变。 + assert_eq!(isolated.first().map(String::as_str), Some("--unshare-pid")); + assert_eq!(isolated.last().map(String::as_str), Some("--")); + } + + // P1#2:工作区若包含/等于敏感目录,写围栏 re-allow 会重新暴露之 → 拒绝。 + #[test] + fn validate_workspace_rejects_ancestor_of_sensitive_dir() { + let Some(home) = dirs::home_dir() else { + return; + }; + // home 本身包含 ~/.ssh 等敏感目录。 + assert!(validate_workspace(&home).is_err()); + } + + // P1#2:凭据目录内部的工作区一律拒绝。 + #[test] + fn validate_workspace_rejects_inside_credential_dir() { + let Some(home) = dirs::home_dir() else { + return; + }; + let inside_ssh = home.join(".ssh").join("ws"); + assert!(validate_workspace(&inside_ssh).is_err()); + } + + // P1#2:应用配置目录内部豁免——默认工作区 ~/.liveagent/default-project 必须放行。 + #[test] + fn validate_workspace_allows_default_project_under_app_config() { + let Some(config) = app_config_dir() else { + return; + }; + let default_project = config.join("default-project"); + assert!(validate_workspace(&default_project).is_ok()); + } + + // P1#2:与任何敏感目录无祖先/后代关系的普通工作区放行。 + #[test] + fn validate_workspace_allows_ordinary_workspace() { + assert!(validate_workspace(Path::new("/tmp/liveagent-ordinary-ws")).is_ok()); + } +} diff --git a/crates/agent-gui/src-tauri/src/runtime/shell_runner.rs b/crates/agent-gui/src-tauri/src/runtime/shell_runner.rs index e91ac58d9..dee3715b7 100644 --- a/crates/agent-gui/src-tauri/src/runtime/shell_runner.rs +++ b/crates/agent-gui/src-tauri/src/runtime/shell_runner.rs @@ -14,6 +14,7 @@ use crate::runtime::platform::{ expand_tilde_path, maybe_augment_macos_path, shell_basename, strip_windows_verbatim_prefix, }; use crate::runtime::process::{configure_child_process_group, terminate_child_process_tree}; +use crate::runtime::sandbox::{self, SandboxOptions, SandboxSpec}; const MAX_STDOUT_BYTES: usize = 400 * 1024; // 400KB const MAX_STDERR_BYTES: usize = 400 * 1024; // 400KB @@ -111,6 +112,9 @@ pub struct ShellRunResponse { pub platform: String, pub profile: String, pub shell_family: String, + /// 沙箱机制("seatbelt"/"bubblewrap"),未启用沙箱时为 None。 + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox: Option, pub stdout: String, pub stderr: String, pub stdout_truncated: bool, @@ -511,6 +515,8 @@ struct ShellCandidate { pub(crate) struct SpawnedPlatformShell { pub child: std::process::Child, pub profile: ShellExecutionProfile, + /// 生效的沙箱机制;None 表示未启用沙箱。 + pub sandbox: Option<&'static str>, } fn platform_shell_candidates(cmd: &str) -> Vec { @@ -684,6 +690,7 @@ pub(crate) fn spawn_platform_shell_command( command: &str, cwd: &Path, envs: &[(String, String)], + sandbox_spec: Option<&SandboxSpec>, mut stdio_factory: F, ) -> Result where @@ -693,10 +700,21 @@ where let system_proxy_envs = crate::services::system_proxy::shell_proxy_envs()?; for candidate in platform_shell_candidates(command) { + // 沙箱包裹在 shell candidate 选定后、spawn 前进行,fail-closed:包裹 + // 失败(平台不支持/依赖缺失)直接报错,绝不回退为无沙箱执行。 + // sandbox-exec/bwrap 按名字解析 shell 时同样遵循 PATH,语义不变。 + let (spawn_program, spawn_args, sandbox_mechanism) = match sandbox_spec { + Some(spec) => { + let (program, args, mechanism) = + sandbox::wrap_command(spec, &candidate.program, &candidate.args)?; + (program, args, Some(mechanism)) + } + None => (candidate.program.clone(), candidate.args.clone(), None), + }; let (stdout, stderr) = stdio_factory().map_err(|err| format!("Failed to prepare shell stdio: {err}"))?; - let mut c = Command::new(&candidate.program); - c.args(&candidate.args); + let mut c = Command::new(&spawn_program); + c.args(&spawn_args); // 系统代理 env 先注入,调用方 envs(如 LIVEAGENT_HOOK_*)后写保持更高优先级。 for (key, value) in &system_proxy_envs { c.env(key, value); @@ -721,6 +739,7 @@ where return Ok(SpawnedPlatformShell { child, profile: candidate.profile, + sandbox: sandbox_mechanism, }); } Err(err) => errors.push(format!( @@ -756,6 +775,7 @@ pub(crate) fn run_shell_script( provider_id, cancel_token, &[], + None, ) } @@ -769,6 +789,7 @@ pub(crate) fn run_shell_script_with_envs( _provider_id: Option, cancel_token: Option, envs: &[(String, String)], + sandbox_options: Option, ) -> Result { let cmd = command.trim(); if cmd.is_empty() { @@ -781,11 +802,22 @@ pub(crate) fn run_shell_script_with_envs( let timeout = Duration::from_millis(effective_timeout_ms); let start = Instant::now(); - let spawned = spawn_platform_shell_command(cmd, &actual_cwd, envs, || { - Ok((Stdio::piped(), Stdio::piped())) - })?; + // 沙箱写围栏锚定 workdir(工作区根)而非 cwd:cwd 可能是子目录,但工具语义 + // 允许写整个工作区。 + let sandbox_spec = match sandbox_options { + Some(options) => { + let wd = canonicalize_workdir(&workdir).map_err(|e| e.to_string())?; + Some(SandboxSpec::from_options(wd, options)) + } + None => None, + }; + let spawned = + spawn_platform_shell_command(cmd, &actual_cwd, envs, sandbox_spec.as_ref(), || { + Ok((Stdio::piped(), Stdio::piped())) + })?; let mut child = spawned.child; let shell_profile = spawned.profile; + let sandbox_mechanism = spawned.sandbox; let shell_name = shell_basename(shell_profile.display_shell); let stdout = child @@ -868,6 +900,7 @@ process output to a log file, for example: `nohup command > /tmp/liveagent-task. platform: shell_profile.platform.to_string(), profile: shell_profile.profile.to_string(), shell_family: shell_profile.shell_family.to_string(), + sandbox: sandbox_mechanism.map(str::to_string), stdout: stdout_str, stderr: stderr_str, stdout_truncated, diff --git a/crates/agent-gui/src-tauri/src/runtime/shell_session.rs b/crates/agent-gui/src-tauri/src/runtime/shell_session.rs index 5851ee9da..e23623995 100644 --- a/crates/agent-gui/src-tauri/src/runtime/shell_session.rs +++ b/crates/agent-gui/src-tauri/src/runtime/shell_session.rs @@ -436,7 +436,9 @@ impl ShellSessionManager { } self.make_room_locked(&mut sessions)?; - let spawned = spawn_platform_shell_command(command, &actual_cwd, &[], || { + // 可恢复 shell session 暂不进入沙箱执行(沙箱围栏面向一次性命令), + // 因此 sandbox_spec 传 None。 + let spawned = spawn_platform_shell_command(command, &actual_cwd, &[], None, || { Ok((Stdio::piped(), Stdio::piped())) })?; let mut child = spawned.child; diff --git a/crates/agent-gui/src-tauri/src/services/gateway/chat.rs b/crates/agent-gui/src-tauri/src/services/gateway/chat.rs index ae538d1ee..579068460 100644 --- a/crates/agent-gui/src-tauri/src/services/gateway/chat.rs +++ b/crates/agent-gui/src-tauri/src/services/gateway/chat.rs @@ -170,6 +170,7 @@ impl GatewayController { runtime_controls, execution_mode, workdir, + command_safety_mode, uploaded_files, queue_policy, } = request; @@ -204,6 +205,7 @@ impl GatewayController { runtime_controls, execution_mode, workdir, + command_safety_mode, uploaded_files: uploaded_files .into_iter() .map(|file| GatewayUploadedFileEvent { diff --git a/crates/agent-gui/src-tauri/src/services/gateway/tests.rs b/crates/agent-gui/src-tauri/src/services/gateway/tests.rs index dc83e612a..42bf2742b 100644 --- a/crates/agent-gui/src-tauri/src/services/gateway/tests.rs +++ b/crates/agent-gui/src-tauri/src/services/gateway/tests.rs @@ -79,6 +79,7 @@ fn gateway_chat_request( runtime_controls: None, execution_mode: String::new(), workdir: String::new(), + command_safety_mode: String::new(), uploaded_files: Vec::new(), queue_policy: String::new(), } @@ -111,6 +112,7 @@ fn gateway_chat_command_mapping_preserves_rebase_signal() { message: "edited".to_string(), execution_mode: "tools".to_string(), workdir: "/workspace".to_string(), + command_safety_mode: "sandbox".to_string(), ..Default::default() }; @@ -145,6 +147,7 @@ fn gateway_chat_command_mapping_preserves_rebase_signal() { assert_eq!(base_message_ref.content_hash, "fnv1a32:00000000"); assert_eq!(event.execution_mode, "tools"); assert_eq!(event.workdir, "/workspace"); + assert_eq!(event.command_safety_mode, "sandbox"); } #[test] diff --git a/crates/agent-gui/src-tauri/src/services/gateway/types.rs b/crates/agent-gui/src-tauri/src/services/gateway/types.rs index 655ced1d6..1d1513591 100644 --- a/crates/agent-gui/src-tauri/src/services/gateway/types.rs +++ b/crates/agent-gui/src-tauri/src/services/gateway/types.rs @@ -68,6 +68,9 @@ pub struct GatewayChatRequestEvent { pub runtime_controls: Option, pub execution_mode: String, pub workdir: String, + /// 远端 WebUI 直带的命令安全模式(ask/auto/sandbox/sandboxOffline);空串表示 + /// 未指定,桌面端回落到本地 settings.system.commandSafetyMode。 + pub command_safety_mode: String, pub uploaded_files: Vec, pub queue_policy: String, } diff --git a/crates/agent-gui/src/agent-ui-adapters/sandboxCapability.ts b/crates/agent-gui/src/agent-ui-adapters/sandboxCapability.ts new file mode 100644 index 000000000..7d6c1f8f1 --- /dev/null +++ b/crates/agent-gui/src/agent-ui-adapters/sandboxCapability.ts @@ -0,0 +1,40 @@ +import { invoke } from "@tauri-apps/api/core"; +import { useEffect, useState } from "react"; +import { inferRuntimePlatform } from "../lib/runtimePlatform"; + +export type SandboxCapability = { + supported: boolean; + mechanism: string; + platform: string; + reason?: string; +}; + +/** 桌面端:本机平台即沙箱执行平台,可在探测返回前先按平台渲染文案/禁用开关。 */ +export function inferSandboxPlatform(): "macos" | "linux" | "windows" | null { + return inferRuntimePlatform(); +} + +let cachedCapability: SandboxCapability | null = null; + +/** 桌面端:探测本机 OS 沙箱可用性(macOS Seatbelt / Linux bwrap / Windows 未实现)。 */ +export function useSandboxCapability(): SandboxCapability | null { + const [capability, setCapability] = useState(cachedCapability); + + useEffect(() => { + if (cachedCapability) return; + let disposed = false; + invoke("system_sandbox_capability") + .then((result) => { + cachedCapability = result; + if (!disposed) setCapability(result); + }) + .catch((error) => { + console.warn("system_sandbox_capability failed", error); + }); + return () => { + disposed = true; + }; + }, []); + + return capability; +} diff --git a/crates/agent-gui/src/lib/tools/builtinRegistry.ts b/crates/agent-gui/src/lib/tools/builtinRegistry.ts index 4c94949a6..c7f0c6a16 100644 --- a/crates/agent-gui/src/lib/tools/builtinRegistry.ts +++ b/crates/agent-gui/src/lib/tools/builtinRegistry.ts @@ -28,7 +28,7 @@ import { createFsTools } from "./fsTools"; import { createMcpManagerTools } from "./mcpManagerTools"; import { createMcpTools } from "./mcpTools"; import { createMemoryTools } from "./memoryTools"; -import { createShellTools } from "./shellTools"; +import { createShellTools, type ShellSandboxSettings } from "./shellTools"; import type { SkillAccessPolicy } from "./skillAccessPolicy"; import { createSkillTools } from "./skillTools"; import { createSSHManagerTools, type SshManagerSessionChange } from "./sshManagerTools"; @@ -157,6 +157,8 @@ type BuildBuiltinBaseToolRegistryParams = { providerId: ProviderId; runtimePlatform?: RuntimePlatform; fileState: FileToolState; + /** OS 级沙箱设置;透传给 Bash / ManagedProcess 执行层。 */ + sandbox?: ShellSandboxSettings; skillsEnabled: boolean; skillsRootDir?: string; skillAccessPolicy?: SkillAccessPolicy; @@ -214,6 +216,7 @@ async function buildBaseBuiltinToolBundles(params: BuildBuiltinBaseToolRegistryP managedProcessEnabled: params.runtimeScope === "chat", resumableShellEnabled: params.runtimeScope === "chat", resolveHomeDir, + sandbox: params.sandbox, }), ...(params.skillsEnabled ? [ diff --git a/crates/agent-gui/src/lib/tools/shellTools.ts b/crates/agent-gui/src/lib/tools/shellTools.ts index 862bc7c81..c15f9e65e 100644 --- a/crates/agent-gui/src/lib/tools/shellTools.ts +++ b/crates/agent-gui/src/lib/tools/shellTools.ts @@ -31,6 +31,7 @@ type ShellRunResponse = { platform?: string; profile?: string; shell_family?: string; + sandbox?: string; stdout: string; stderr: string; stdout_truncated: boolean; @@ -486,6 +487,11 @@ function buildCancelledResult(params: { }; } +export type ShellSandboxSettings = { + enabled: boolean; + allowNetwork: boolean; +}; + export function createShellTools(params: { workdir: string; providerId: ProviderId; @@ -496,11 +502,18 @@ export function createShellTools(params: { managedProcessEnabled?: boolean; resumableShellEnabled?: boolean; resolveHomeDir?: () => Promise; + /** OS 级沙箱(macOS Seatbelt / Linux bwrap);undefined 或 enabled=false 时直跑。 */ + sandbox?: ShellSandboxSettings; }): BuiltinToolBundle { const timeoutPolicy = resolveBashTimeoutPolicy(params.providerId); const runtimePlatform = normalizeRuntimePlatform(params.runtimePlatform) ?? inferRuntimePlatform(); const platformLabel = runtimePlatformLabel(runtimePlatform); + const sandboxEnabled = params.sandbox?.enabled === true; + const sandboxAllowNetwork = params.sandbox?.allowNetwork !== false; + const sandboxPolicy = sandboxEnabled + ? ` Sandbox mode is ON: commands run inside an OS-level sandbox — writes are limited to the workspace and temp dirs, credential dirs (~/.ssh etc.) are masked${sandboxAllowNetwork ? "" : ", and network access is blocked"}. "Operation not permitted" outside the workspace means the sandbox blocked it; do not retry with sudo — ask the user instead.` + : ""; const shellPolicy = runtimePlatform === "windows" ? "Windows runs Bash commands with Git Bash (POSIX semantics) when available, falling back to pwsh, then Windows PowerShell, then cmd only if Git Bash is not installed. Write POSIX/bash syntax by default: `export NAME=value`, `&&`, `/dev/null`, forward-slash paths. If the result header reports `shell_family: powershell` or `shell_family: cmd`, Git Bash is missing on this machine — switch to PowerShell syntax and suggest installing Git for Windows or setting LIVEAGENT_GIT_BASH_PATH." @@ -755,7 +768,7 @@ export function createShellTools(params: { const toolBash: Tool = { name: "Bash", - description: `Execute a non-interactive shell command on the local machine for builds, tests, package managers, external CLIs, curl/API calls, running Skill scripts, or explicitly requested shell work. Runtime platform: ${platformLabel}. ${shellPolicy} Reserve it for commands that truly require a shell — do NOT use Bash for file operations the dedicated tools handle: use Read/List/Glob/Grep instead of cat/ls/find/grep/rg for any workspace or Skill content; always use Delete for intentional workspace or Skill deletions instead of Bash, scripts, or deletion-oriented CLIs such as rm/rmdir/unlink/find -delete/git rm/git clean/PowerShell Remove-Item/cmd del, erase, or rd, because only structured Delete calls make deletions visible in Edited Files and file-ledger tracking; use Image instead of open/xdg-open/file paths to show pictures. Use curl with an explicit timeout such as \`--max-time 30\` for endpoint tests. ${backgroundPolicy} Running a Skill script: set cwd to \`skill:///scripts\` and run a relative command, or execute the absolute script path directly when that Skill is enabled. Use / as the path separator; Windows \\ is auto-normalized. Returns stdout, stderr, exit_code, platform, profile, and shell_family. ${allowResumableShell ? `Bash waits up to yield_time_ms (default 10000ms), then returns a session_id while the same command continues; use ProcessWait to continue waiting and ProcessStop to terminate it. Session responses report session_duration_ms as cumulative elapsed time from the original Bash start, so never add it across responses. Terminal statuses are completed, failed, cancelled, and timed_out. timeout_ms is an optional hard runtime limit and is capped at ${GLOBAL_BASH_MAX_TIMEOUT_MS}ms; omit it for no hard limit.` : `For ${timeoutPolicy.providerLabel}, timeout defaults to ${timeoutPolicy.defaultTimeoutMs}ms and is capped at ${timeoutPolicy.maxTimeoutMs}ms; larger timeout_ms values are accepted by the schema but clamped before execution.`} High risk: use carefully.`, + description: `Execute a non-interactive shell command on the local machine for builds, tests, package managers, external CLIs, curl/API calls, running Skill scripts, or explicitly requested shell work. Runtime platform: ${platformLabel}. ${shellPolicy}${sandboxPolicy} Reserve it for commands that truly require a shell — do NOT use Bash for file operations the dedicated tools handle: use Read/List/Glob/Grep instead of cat/ls/find/grep/rg for any workspace or Skill content; always use Delete for intentional workspace or Skill deletions instead of Bash, scripts, or deletion-oriented CLIs such as rm/rmdir/unlink/find -delete/git rm/git clean/PowerShell Remove-Item/cmd del, erase, or rd, because only structured Delete calls make deletions visible in Edited Files and file-ledger tracking; use Image instead of open/xdg-open/file paths to show pictures. Use curl with an explicit timeout such as \`--max-time 30\` for endpoint tests. ${backgroundPolicy} Running a Skill script: set cwd to \`skill:///scripts\` and run a relative command, or execute the absolute script path directly when that Skill is enabled. Use / as the path separator; Windows \\ is auto-normalized. Returns stdout, stderr, exit_code, platform, profile, and shell_family. ${allowResumableShell ? `Bash waits up to yield_time_ms (default 10000ms), then returns a session_id while the same command continues; use ProcessWait to continue waiting and ProcessStop to terminate it. Session responses report session_duration_ms as cumulative elapsed time from the original Bash start, so never add it across responses. Terminal statuses are completed, failed, cancelled, and timed_out. timeout_ms is an optional hard runtime limit and is capped at ${GLOBAL_BASH_MAX_TIMEOUT_MS}ms; omit it for no hard limit.` : `For ${timeoutPolicy.providerLabel}, timeout defaults to ${timeoutPolicy.defaultTimeoutMs}ms and is capped at ${timeoutPolicy.maxTimeoutMs}ms; larger timeout_ms values are accepted by the schema but clamped before execution.`} High risk: use carefully.`, parameters: strictToolParameters({ command: Type.String({ description: "Shell command to execute (prefer non-interactive, idempotent commands).", @@ -1074,6 +1087,10 @@ export function createShellTools(params: { cwd: cwd || undefined, label: label || undefined, isolated: isolated || undefined, + // 跟随所选模式:sandboxOffline 下常驻进程同样断网(无法对外提供服务, + // 需要 dev server 时应切回"沙箱"模式)。 + sandbox: sandboxEnabled || undefined, + sandbox_allow_network: sandboxEnabled ? sandboxAllowNetwork : undefined, }, signal, { @@ -1423,6 +1440,8 @@ export function createShellTools(params: { max_timeout_ms: timeoutPolicy.maxTimeoutMs, provider_id: params.providerId, run_id, + sandbox: sandboxEnabled || undefined, + sandbox_allow_network: sandboxEnabled ? sandboxAllowNetwork : undefined, } as any); const header = [ @@ -1431,6 +1450,7 @@ export function createShellTools(params: { res.platform ? `platform: ${res.platform}` : null, res.profile ? `profile: ${res.profile}` : null, res.shell_family ? `shell_family: ${res.shell_family}` : null, + res.sandbox ? `sandbox: ${res.sandbox}` : null, `cwd: ${formatResolvedTarget(cwdResolved)}`, `exit_code: ${res.exit_code}`, res.timed_out ? `timed_out: true` : null, diff --git a/crates/agent-gui/src/pages/ChatPage.tsx b/crates/agent-gui/src/pages/ChatPage.tsx index dc03e8fea..58a4c5dad 100644 --- a/crates/agent-gui/src/pages/ChatPage.tsx +++ b/crates/agent-gui/src/pages/ChatPage.tsx @@ -75,6 +75,7 @@ import { resolveWorkspaceResources, type SelectedModel, updateExecutionModeFromChatSelection, + updateSystem, updateWorkspaceResourceSettings, type WorkspaceProject, workspaceProjectPathKey, @@ -1890,6 +1891,14 @@ export function ChatPage(props: ChatPageProps) { modelOptions={modelOptions} selectedValue={selectedValue} chatRuntimeControls={chatRuntimeControlsForCurrentProvider} + commandSafetyMode={settings.system.commandSafetyMode} + onCommandSafetyModeChange={(mode) => + setSettings((prev) => + prev.system.commandSafetyMode === mode + ? prev + : updateSystem(prev, { commandSafetyMode: mode }), + ) + } reasoningOptions={chatRuntimeReasoningOptions} thinkingAlwaysOn={chatRuntimeThinkingAlwaysOn} contextUsageTokensSource={contextUsageTokensSource} diff --git a/crates/agent-gui/src/pages/chat/gateway/gatewayBridgeTypes.ts b/crates/agent-gui/src/pages/chat/gateway/gatewayBridgeTypes.ts index 9272a0315..49cadb1ca 100644 --- a/crates/agent-gui/src/pages/chat/gateway/gatewayBridgeTypes.ts +++ b/crates/agent-gui/src/pages/chat/gateway/gatewayBridgeTypes.ts @@ -2,7 +2,12 @@ import type { MentionComposerDraft } from "@liveagent/ui/components/chat/Mention import type { PendingUploadedFile } from "@liveagent/ui/lib/chat/uploadedFiles"; import type { MutableRefObject } from "react"; import type { HistoryMessageRef } from "../../../lib/chat/conversation/conversationState"; -import type { ChatRuntimeControls, ExecutionMode, ProviderId } from "../../../lib/settings"; +import type { + ChatRuntimeControls, + CommandSafetyMode, + ExecutionMode, + ProviderId, +} from "../../../lib/settings"; import type { ConversationRuntimeEntry } from "../runtime/chatPageRuntime"; export type GatewaySelectedModelEvent = { @@ -27,6 +32,7 @@ export type GatewayChatRequestEvent = { runtimeControls?: GatewayChatRuntimeControlsEvent; executionMode?: string; workdir?: string; + commandSafetyMode?: string; uploadedFiles?: PendingUploadedFile[]; queuePolicy?: "auto" | "append" | "interrupt" | string; }; @@ -65,6 +71,7 @@ export type ActiveGatewayBridgeRequest = { runtimeControlsOverride?: ChatRuntimeControls; executionModeOverride?: ExecutionMode; workdirOverride?: string; + commandSafetyModeOverride?: CommandSafetyMode; }; export type SendChatAction = (overrides?: { @@ -74,6 +81,7 @@ export type SendChatAction = (overrides?: { conversationIdOverride?: string; executionModeOverride?: ExecutionMode; workdirOverride?: string; + commandSafetyModeOverride?: CommandSafetyMode; runtimeControlsOverride?: ChatRuntimeControls; gatewayBridgeRequestOverride?: ActiveGatewayBridgeRequest | null; preserveComposerOnStart?: boolean; @@ -121,6 +129,23 @@ export function normalizeGatewayExecutionMode( } } +// 命令安全模式的网关归一化。返回 undefined(而非某个默认模式)表示"远端未指定", +// 让 useSendChatTurn 的优先级链回落到本地 settings.system.commandSafetyMode;绝不 +// 默认成 "auto",以免静默下调桌面端已选的更严格模式(fail-closed)。 +export function normalizeGatewayCommandSafetyMode( + value: string | null | undefined, +): CommandSafetyMode | undefined { + switch (value?.trim()) { + case "ask": + case "auto": + case "sandbox": + case "sandboxOffline": + return value.trim() as CommandSafetyMode; + default: + return undefined; + } +} + export function normalizeGatewayWorkdir(value: string | null | undefined): string | undefined { const normalized = value?.trim() ?? ""; return normalized || undefined; diff --git a/crates/agent-gui/src/pages/chat/gateway/useGatewayBridgeListeners.ts b/crates/agent-gui/src/pages/chat/gateway/useGatewayBridgeListeners.ts index 393bef7c3..e0630abc5 100644 --- a/crates/agent-gui/src/pages/chat/gateway/useGatewayBridgeListeners.ts +++ b/crates/agent-gui/src/pages/chat/gateway/useGatewayBridgeListeners.ts @@ -10,6 +10,7 @@ import { type GatewayChatCancelEvent, type GatewayChatClaimedRequest, type GatewayChatRequestReadyEvent, + normalizeGatewayCommandSafetyMode, normalizeGatewayExecutionMode, normalizeGatewayWorkdir, } from "./gatewayBridgeTypes"; @@ -482,6 +483,7 @@ export function useGatewayBridgeListeners(params: UseGatewayBridgeListenersParam : undefined, executionModeOverride: normalizeGatewayExecutionMode(payload.executionMode), workdirOverride: normalizeGatewayWorkdir(payload.workdir), + commandSafetyModeOverride: normalizeGatewayCommandSafetyMode(payload.commandSafetyMode), }); const markRuntimeStarted = async () => { await invoke("gateway_chat_mark_started", { @@ -496,6 +498,7 @@ export function useGatewayBridgeListeners(params: UseGatewayBridgeListenersParam conversationIdOverride: resolvedConversationId, executionModeOverride: gatewayBridgeRequest.executionModeOverride, workdirOverride: gatewayBridgeRequest.workdirOverride, + commandSafetyModeOverride: gatewayBridgeRequest.commandSafetyModeOverride, runtimeControlsOverride: gatewayBridgeRequest.runtimeControlsOverride, gatewayBridgeRequestOverride: gatewayBridgeRequest, editResendBaseMessageRef: baseMessageRef, diff --git a/crates/agent-gui/src/pages/chat/queue/chatTurnQueue.ts b/crates/agent-gui/src/pages/chat/queue/chatTurnQueue.ts index c92d583e9..77c7fddfc 100644 --- a/crates/agent-gui/src/pages/chat/queue/chatTurnQueue.ts +++ b/crates/agent-gui/src/pages/chat/queue/chatTurnQueue.ts @@ -7,7 +7,7 @@ export type { } from "@liveagent/ui/contracts/chatQueue"; import type { PendingUploadedFile } from "@liveagent/ui/lib/chat/uploadedFiles"; -import type { ChatRuntimeControls, ExecutionMode } from "../../../lib/settings"; +import type { ChatRuntimeControls, CommandSafetyMode, ExecutionMode } from "../../../lib/settings"; import type { GatewayChatRuntimeControlsEvent, GatewaySelectedModelEvent, @@ -31,6 +31,7 @@ export type QueuedChatTurn = { uploadedFiles: PendingUploadedFile[]; executionMode: ExecutionMode; workdir: string; + commandSafetyMode: CommandSafetyMode; runtimeControls: ChatRuntimeControls; createdAt: number; gatewayRequest?: QueuedGatewayChatRequest; @@ -57,6 +58,7 @@ export function createQueuedChatTurn(input: QueuedChatTurnInput): QueuedChatTurn uploadedFiles: input.uploadedFiles.slice(), executionMode: input.executionMode, workdir: input.workdir.trim(), + commandSafetyMode: input.commandSafetyMode, runtimeControls: { ...input.runtimeControls }, createdAt, gatewayRequest: input.gatewayRequest ? { ...input.gatewayRequest } : undefined, diff --git a/crates/agent-gui/src/pages/chat/queue/useChatTurnQueue.ts b/crates/agent-gui/src/pages/chat/queue/useChatTurnQueue.ts index 8f47de2cc..3cf2babea 100644 --- a/crates/agent-gui/src/pages/chat/queue/useChatTurnQueue.ts +++ b/crates/agent-gui/src/pages/chat/queue/useChatTurnQueue.ts @@ -11,6 +11,7 @@ import type { LiveTranscriptStore } from "../../../lib/chat/conversation/liveTra import { type AppSettings, type ChatRuntimeControls, + type CommandSafetyMode, type ExecutionMode, isAgentExecutionMode, normalizeChatRuntimeControls, @@ -21,6 +22,7 @@ import { createTextComposerDraft } from "../composer/composerDraftText"; import type { ActiveGatewayBridgeRequest, SendChatAction } from "../gateway/gatewayBridgeTypes"; import { type GatewayChatClaimedRequest, + normalizeGatewayCommandSafetyMode, normalizeGatewayExecutionMode, normalizeGatewayWorkdir, } from "../gateway/gatewayBridgeTypes"; @@ -147,6 +149,7 @@ export function useChatTurnQueue(params: UseChatTurnQueueParams) { createdAt: number; executionMode: ExecutionMode; workdir: string; + commandSafetyMode: CommandSafetyMode; runtimeControls: ChatRuntimeControls; gatewayRequest?: QueuedChatTurn["gatewayRequest"]; }) @@ -399,6 +402,7 @@ export function useChatTurnQueue(params: UseChatTurnQueueParams) { ? queuedChatTurnEditSlotRef.current : null; const executionMode = editSlot?.executionMode ?? settings.system.executionMode; + const commandSafetyMode = editSlot?.commandSafetyMode ?? settings.system.commandSafetyMode; const workdirForTurn = isAgentExecutionMode(executionMode) ? ( editSlot?.workdir ?? @@ -414,6 +418,7 @@ export function useChatTurnQueue(params: UseChatTurnQueueParams) { uploadedFiles, executionMode, workdir: workdirForTurn, + commandSafetyMode, runtimeControls: editSlot?.runtimeControls ?? settings.chatRuntimeControls, createdAt: editSlot?.createdAt, gatewayRequest: editSlot?.gatewayRequest, @@ -500,6 +505,7 @@ export function useChatTurnQueue(params: UseChatTurnQueueParams) { : queuedTurn.runtimeControls, executionModeOverride: queuedTurn.executionMode, workdirOverride: queuedTurn.workdir, + commandSafetyModeOverride: queuedTurn.commandSafetyMode, } : null; const markGatewayStarted = @@ -518,6 +524,7 @@ export function useChatTurnQueue(params: UseChatTurnQueueParams) { conversationIdOverride: targetConversationId, executionModeOverride: queuedTurn.executionMode, workdirOverride: queuedTurn.workdir, + commandSafetyModeOverride: queuedTurn.commandSafetyMode, runtimeControlsOverride: queuedTurn.runtimeControls, gatewayBridgeRequestOverride: gatewayBridgeRequest, preserveComposerOnStart: true, @@ -677,6 +684,7 @@ export function useChatTurnQueue(params: UseChatTurnQueueParams) { createdAt: queuedTurn.createdAt, executionMode: queuedTurn.executionMode, workdir: queuedTurn.workdir, + commandSafetyMode: queuedTurn.commandSafetyMode, runtimeControls: { ...queuedTurn.runtimeControls }, gatewayRequest: queuedTurn.gatewayRequest ? { ...queuedTurn.gatewayRequest } : undefined, }; @@ -722,6 +730,9 @@ export function useChatTurnQueue(params: UseChatTurnQueueParams) { const executionMode = normalizeGatewayExecutionMode(payload.executionMode) ?? settings.system.executionMode; + const commandSafetyMode = + normalizeGatewayCommandSafetyMode(payload.commandSafetyMode) ?? + settings.system.commandSafetyMode; const workdir = normalizeGatewayWorkdir(payload.workdir) ?? conversationRuntimeCacheRef.current.get(targetConversationId)?.workdir ?? @@ -737,6 +748,7 @@ export function useChatTurnQueue(params: UseChatTurnQueueParams) { uploadedFiles, executionMode, workdir: isAgentExecutionMode(executionMode) ? workdir : "", + commandSafetyMode, runtimeControls, gatewayRequest: { requestId, diff --git a/crates/agent-gui/src/pages/chat/runtime/useSendChatTurn.ts b/crates/agent-gui/src/pages/chat/runtime/useSendChatTurn.ts index d42b154a5..ec632f839 100644 --- a/crates/agent-gui/src/pages/chat/runtime/useSendChatTurn.ts +++ b/crates/agent-gui/src/pages/chat/runtime/useSendChatTurn.ts @@ -59,6 +59,7 @@ import { type AppSettings, applyMcpOpsToAppSettings, type ChatRuntimeControls, + type CommandSafetyMode, type ExecutionMode, filterMcpSettingsForWorkspace, getSshProjectHostIds, @@ -309,6 +310,7 @@ export function useSendChatTurn(params: UseSendChatTurnParams) { conversationIdOverride?: string; executionModeOverride?: ExecutionMode; workdirOverride?: string; + commandSafetyModeOverride?: CommandSafetyMode; runtimeControlsOverride?: ChatRuntimeControls; gatewayBridgeRequestOverride?: ActiveGatewayBridgeRequest | null; preserveComposerOnStart?: boolean; @@ -333,6 +335,12 @@ export function useSendChatTurn(params: UseSendChatTurnParams) { overrides?.executionModeOverride ?? gatewayBridgeRequest?.executionModeOverride ?? settings.system.executionMode; + // 命令安全模式同样遵循 override → 网关直带 → 本地设置 的优先级链;远端 WebUI + // 选定的安全模式经此生效(P1#1),未指定时回落本地 settings.system。 + const effectiveCommandSafetyMode = + overrides?.commandSafetyModeOverride ?? + gatewayBridgeRequest?.commandSafetyModeOverride ?? + settings.system.commandSafetyMode; const effectiveIsAgentMode = isAgentExecutionMode(effectiveExecutionMode); const effectiveWorkdir = resolveEffectiveConversationWorkdir({ isAgentMode: effectiveIsAgentMode, @@ -1595,6 +1603,7 @@ export function useSendChatTurn(params: UseSendChatTurnParams) { agentTemplates: settings.agents, getMcpSettings: getEffectiveMcpSettings, getToolPolicies, + commandSafetyMode: effectiveCommandSafetyMode, applyMcpOps: (ops) => { const removedIds = ops.filter((op) => op.kind === "remove").map((op) => op.serverId); setSettings((prev) => diff --git a/crates/agent-gui/src/pages/chat/turns/runAgentConversationTurn.ts b/crates/agent-gui/src/pages/chat/turns/runAgentConversationTurn.ts index 1814f143c..573db8a0e 100644 --- a/crates/agent-gui/src/pages/chat/turns/runAgentConversationTurn.ts +++ b/crates/agent-gui/src/pages/chat/turns/runAgentConversationTurn.ts @@ -235,6 +235,8 @@ export type RunAgentConversationTurnParams = { getMcpSettings: () => AppSettings["mcp"]; /** 工具审批策略的实时读取(权威 settingsRef,非 turn 级快照),缺省视为空表。 */ getToolPolicies?: () => AppSettings["system"]["toolPolicies"]; + /** 命令执行方式(turn 级快照):ask 全量审批 / auto 按策略 / sandbox(±断网)。 */ + commandSafetyMode?: AppSettings["system"]["commandSafetyMode"]; applyMcpOps?: (ops: McpSettingsOp[]) => void; remoteWebTunnelsEnabled?: boolean; tunnelPublicBaseUrl?: string; @@ -307,6 +309,7 @@ export async function runAgentConversationTurn(params: RunAgentConversationTurnP agentTemplates, getMcpSettings, getToolPolicies, + commandSafetyMode, applyMcpOps, remoteWebTunnelsEnabled, tunnelPublicBaseUrl, @@ -491,12 +494,17 @@ export async function runAgentConversationTurn(params: RunAgentConversationTurnP const subagentScheduler = createSubagentScheduler(); const runtimePlatform = await resolveRuntimePlatform(); const buildRegistryStartedAt = perfNowMs(); + const safetyMode = commandSafetyMode ?? "auto"; + const sandboxActive = safetyMode === "sandbox" || safetyMode === "sandboxOffline"; const builtinRegistry = await buildBuiltinToolRegistry({ workdir: effectiveWorkdir, additionalRoots, providerId, runtimePlatform, fileState, + sandbox: sandboxActive + ? { enabled: true, allowNetwork: safetyMode !== "sandboxOffline" } + : undefined, taskStateStore, askUserQuestionConversationId: conversationId, checkpoint: { @@ -584,22 +592,21 @@ export async function runAgentConversationTurn(params: RunAgentConversationTurnP // 工具审批门:按实时策略裁决每次调用。deny → 直接拦;ask → 挂起等用户在 // 聊天审批卡片作决定(本会话已“记住”的工具免审);allow → 放行。 + // 命令执行方式为 ask 时,非只读工具无论策略如何都升级为 ask(deny 仍拦)。 const resolveToolGate = async ( toolCall: ToolCall, signal?: AbortSignal, ): Promise<{ allow: true } | { allow: false; reason: string }> => { - const policy = resolveToolPolicy( - toolCall.name, - builtinRegistry.metadataByName.get(toolCall.name), - getToolPolicies?.(), - ); + const metadata = builtinRegistry.metadataByName.get(toolCall.name); + const policy = resolveToolPolicy(toolCall.name, metadata, getToolPolicies?.()); if (policy === "deny") { return { allow: false, reason: `工具 ${toolCall.name} 已被用户的权限策略禁止(deny)。不要重试;如确需使用,请让用户在设置的工具权限中放行。`, }; } - if (policy !== "ask") { + const effectivePolicy = safetyMode === "ask" && !metadata?.isReadOnly ? "ask" : policy; + if (effectivePolicy !== "ask") { return { allow: true }; } if (isSessionApproved(conversationId, toolCall.name)) { diff --git a/crates/agent-ui/src/components/IconSet.tsx b/crates/agent-ui/src/components/IconSet.tsx index d017c6cc4..b9ad66cb9 100644 --- a/crates/agent-ui/src/components/IconSet.tsx +++ b/crates/agent-ui/src/components/IconSet.tsx @@ -54,6 +54,7 @@ import GithubSource from "~icons/lucide/github"; import GlobeSource from "~icons/lucide/globe"; import GlobeOffSource from "~icons/lucide/globe-off"; import GripVerticalSource from "~icons/lucide/grip-vertical"; +import HandSource from "~icons/lucide/hand"; import HardDriveSource from "~icons/lucide/hard-drive"; import HistorySource from "~icons/lucide/history"; import HouseSource from "~icons/lucide/house"; @@ -111,6 +112,7 @@ import SettingsSource from "~icons/lucide/settings"; import Settings2Source from "~icons/lucide/settings-2"; import Share2Source from "~icons/lucide/share-2"; import ShieldSource from "~icons/lucide/shield"; +import ShieldOffSource from "~icons/lucide/shield-off"; import SparkleSource from "~icons/lucide/sparkle"; import SparklesSource from "~icons/lucide/sparkles"; import SquareSource from "~icons/lucide/square"; @@ -520,6 +522,7 @@ export const GitBranch = createIcon(GitBranchSource); export const GitCommitHorizontal = createIcon(GitCommitHorizontalSource); export const Github = createIcon(GithubSource); export const GripVertical = createIcon(GripVerticalSource); +export const Hand = createIcon(HandSource); export const HardDrive = createIcon(HardDriveSource); export const History = createIcon(HistorySource); export const House = createIcon(HouseSource); @@ -580,6 +583,7 @@ export const Settings = createIcon(SettingsSource); export const Settings2 = createIcon(Settings2Source); export const Share2 = createIcon(Share2Source); export const Shield = createIcon(ShieldSource); +export const ShieldOff = createIcon(ShieldOffSource); export const SkillIcon = createIcon(SkillIconSource); export const Sparkle = createIcon(SparkleSource); export const Sparkles = createIcon(SparklesSource); diff --git a/crates/agent-ui/src/components/chat/CommandSafetyModeSelector.tsx b/crates/agent-ui/src/components/chat/CommandSafetyModeSelector.tsx new file mode 100644 index 000000000..e3acbaa9b --- /dev/null +++ b/crates/agent-ui/src/components/chat/CommandSafetyModeSelector.tsx @@ -0,0 +1,138 @@ +import { inferSandboxPlatform, useSandboxCapability } from "@liveagent/adapters/sandboxCapability"; +import type { CommandSafetyMode } from "@liveagent/app/lib/settings"; +import { Hand, Shield, ShieldOff, Zap } from "@liveagent/ui/components/IconSet"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@liveagent/ui/components/ui/select"; +import { useLocale } from "@liveagent/ui/i18n/index"; +import { cn } from "@liveagent/ui/lib/shared/utils"; +import { useMemo } from "react"; + +const MODE_I18N_KEYS: Record = { + ask: "chat.safety.ask", + auto: "chat.safety.auto", + sandbox: "chat.safety.sandbox", + sandboxOffline: "chat.safety.sandboxOffline", +}; + +const MODE_DESC_I18N_KEYS: Record = { + ask: "chat.safety.askDesc", + auto: "chat.safety.autoDesc", + sandbox: "chat.safety.sandboxDesc", + sandboxOffline: "chat.safety.sandboxOfflineDesc", +}; + +function isCommandSafetyMode(value: unknown): value is CommandSafetyMode { + return value === "ask" || value === "auto" || value === "sandbox" || value === "sandboxOffline"; +} + +export function CommandSafetyModeSelector(props: { + surface: "desktop" | "web"; + value: CommandSafetyMode; + disabled?: boolean; + onChange: (mode: CommandSafetyMode) => void; +}) { + const { surface, value, disabled, onChange } = props; + const { t } = useLocale(); + const capability = useSandboxCapability(); + // 桌面端平台同步可知,Windows 立即禁用沙箱两项;WebUI 为 null(执行端平台 + // 未知),不禁用,由桌面端执行层 fail-closed 兜底。探测结果进一步覆盖。 + const platform = useMemo(() => inferSandboxPlatform(), []); + const sandboxUnavailable = + platform === "windows" || (capability !== null && !capability.supported); + const sandboxUnavailableHint = + platform === "windows" + ? t("chat.safety.sandboxUnavailableWindows") + : t("chat.safety.sandboxUnavailable"); + // 当前值本身不可用(如设置同步自 macOS,本机是 Windows)时仍显示,但标红提示 + // 由执行层报错兜底;这里不做静默改写,避免设置回写抖动。 + const selected = isCommandSafetyMode(value) ? value : "auto"; + const sandboxSelected = selected === "sandbox" || selected === "sandboxOffline"; + + const modeIcon = (mode: CommandSafetyMode, className: string) => { + if (mode === "ask") return ; + if (mode === "auto") return ; + if (mode === "sandboxOffline") return ; + return ; + }; + + return ( + + ); +} diff --git a/crates/agent-ui/src/i18n/translations/enUSCommon.ts b/crates/agent-ui/src/i18n/translations/enUSCommon.ts index c776d13bc..405a14d03 100644 --- a/crates/agent-ui/src/i18n/translations/enUSCommon.ts +++ b/crates/agent-ui/src/i18n/translations/enUSCommon.ts @@ -267,6 +267,19 @@ export const EN_US_COMMON_TRANSLATIONS = { "chat.runtime.tunnelRemoteOffline": "Remote Gateway is offline", "chat.runtime.tunnelSettingsSyncing": "Syncing desktop settings", "chat.runtime.reasoning": "Thinking effort", + "chat.safety.label": "Command execution mode", + "chat.safety.ask": "Approve each", + "chat.safety.askDesc": "Ask before every command or write; read-only tools run freely", + "chat.safety.auto": "Auto", + "chat.safety.autoDesc": "Run directly per the tool permission policies in Settings", + "chat.safety.sandbox": "Sandbox", + "chat.safety.sandboxDesc": + "Commands run in an OS sandbox: writes limited to the project folder, sensitive dirs like ~/.ssh unreadable", + "chat.safety.sandboxOffline": "Sandbox · offline", + "chat.safety.sandboxOfflineDesc": "Sandbox plus no network access for commands", + "chat.safety.sandboxUnavailable": "Sandbox mechanism unavailable on this platform", + "chat.safety.sandboxUnavailableWindows": + "Not supported on Windows yet (restricted-token backend in progress)", "chat.emptyRound": "(No reply)", "chat.inputHint": "Type a message, @ to reference files, prompts can be queued...", "chat.inputHintWithSkills": diff --git a/crates/agent-ui/src/i18n/translations/zhCNCommon.ts b/crates/agent-ui/src/i18n/translations/zhCNCommon.ts index 918816664..3b2428b2c 100644 --- a/crates/agent-ui/src/i18n/translations/zhCNCommon.ts +++ b/crates/agent-ui/src/i18n/translations/zhCNCommon.ts @@ -241,6 +241,17 @@ export const ZH_CN_COMMON_TRANSLATIONS = { "chat.runtime.tunnelRemoteOffline": "Remote Gateway 未连接", "chat.runtime.tunnelSettingsSyncing": "正在同步桌面端设置", "chat.runtime.reasoning": "思考程度", + "chat.safety.label": "命令执行方式", + "chat.safety.ask": "逐条审批", + "chat.safety.askDesc": "每次执行命令或写入前都请求批准,只读操作不打断", + "chat.safety.auto": "自动执行", + "chat.safety.autoDesc": "按设置中的工具权限策略直接执行", + "chat.safety.sandbox": "沙箱", + "chat.safety.sandboxDesc": "命令在 OS 沙箱内执行:写入限项目文件夹,~/.ssh 等敏感目录不可读", + "chat.safety.sandboxOffline": "沙箱·断网", + "chat.safety.sandboxOfflineDesc": "在沙箱基础上进一步禁止命令联网", + "chat.safety.sandboxUnavailable": "当前平台沙箱机制不可用", + "chat.safety.sandboxUnavailableWindows": "Windows 暂不支持(受限令牌方案开发中)", "chat.emptyRound": "(无回复)", "chat.inputHint": "输入消息,@ 引用文件,提示词可队列发送...", "chat.inputHintWithSkills": "输入消息,@ 引用文件,/ 引用Skills,提示词可队列发送...", diff --git a/crates/agent-ui/src/lib/settings/index.ts b/crates/agent-ui/src/lib/settings/index.ts index 980440749..d2d71b0e4 100644 --- a/crates/agent-ui/src/lib/settings/index.ts +++ b/crates/agent-ui/src/lib/settings/index.ts @@ -66,6 +66,7 @@ import type { ChatRuntimeReasoningProviderKey, CloseWindowBehavior, CodexRequestFormat, + CommandSafetyMode, CustomProvider, CustomSettings, EffectiveWorkspaceResources, @@ -104,6 +105,7 @@ import type { WorkspaceResourceSettings, } from "./types"; import { + COMMAND_SAFETY_MODES, DEFAULT_CHAT_RUNTIME_CONTROLS, getDefaultUsageQueryConfig, PROMPT_CACHE_HINT_MODES, @@ -647,11 +649,15 @@ export function createProviderModelConfig( }; } -const VALID_LIMITS_SOURCES: readonly ModelLimitsSource[] = ["catalog", "provider", "fallback", "user"]; +const VALID_LIMITS_SOURCES: readonly ModelLimitsSource[] = [ + "catalog", + "provider", + "fallback", + "user", +]; function normalizeLimitsSource(value: unknown): ModelLimitsSource | undefined { - return typeof value === "string" && - (VALID_LIMITS_SOURCES as readonly string[]).includes(value) + return typeof value === "string" && (VALID_LIMITS_SOURCES as readonly string[]).includes(value) ? (value as ModelLimitsSource) : undefined; } @@ -723,7 +729,10 @@ export function normalizeProviderModelConfig( if (resolvedSource === "catalog" || resolvedSource === "fallback") { // 加载时按当前目录/兜底重新解析,让目录更新自动传导。 - limits = { contextWindow: catalogDefaults.contextWindow, maxOutputToken: catalogDefaults.maxOutputToken }; + limits = { + contextWindow: catalogDefaults.contextWindow, + maxOutputToken: catalogDefaults.maxOutputToken, + }; limitsSource = catalogDefaults.source; } else { // provider:供应商实时数据只在“刷新模型列表”那次抓取时存在,加载阶段 @@ -1169,12 +1178,19 @@ export function normalizeSystemProxyConfig(input: unknown): SystemProxyConfig { }; } +export function normalizeCommandSafetyMode(input: unknown): CommandSafetyMode { + return typeof input === "string" && (COMMAND_SAFETY_MODES as readonly string[]).includes(input) + ? (input as CommandSafetyMode) + : "auto"; +} + export function normalizeSystemSettings(input: unknown): SystemSettings { const obj = (input && typeof input === "object" ? input : {}) as Record; return { executionMode: normalizeExecutionMode(obj.executionMode), workdir: normalizeWorkdir(obj.workdir), toolPolicies: normalizeToolPolicies(obj.toolPolicies), + commandSafetyMode: normalizeCommandSafetyMode(obj.commandSafetyMode), workspaceProjects: normalizeWorkspaceProjects(obj.workspaceProjects), workspaceProjectGroups: normalizeWorkspaceProjectGroups(obj.workspaceProjectGroups), activeWorkspaceProjectId: @@ -1377,6 +1393,7 @@ export function getDefaultSettings(): AppSettings { system: { executionMode: "tools", workdir: "", + commandSafetyMode: "auto", workspaceProjects: [], workspaceProjectGroups: [], activeWorkspaceProjectId: undefined, diff --git a/crates/agent-ui/src/lib/settings/types.ts b/crates/agent-ui/src/lib/settings/types.ts index 3326f0f2a..6cbb92f6a 100644 --- a/crates/agent-ui/src/lib/settings/types.ts +++ b/crates/agent-ui/src/lib/settings/types.ts @@ -232,6 +232,21 @@ export type SystemProxyConfig = { /** 工具审批策略:allow 直接执行、ask 执行前请求用户批准、deny 直接拒绝。 */ export type ToolPolicy = "allow" | "ask" | "deny"; +// 命令执行方式(对话框内切换,单一互斥维度): +// - ask:每次带副作用的工具调用都请求用户批准(只读工具不拦)。 +// - auto:按工具审批策略直接执行(既有默认行为)。 +// - sandbox / sandboxOffline:Bash 与常驻进程在 OS 级沙箱内执行(macOS +// Seatbelt / Linux bubblewrap;Windows 暂不支持),写入限工作区+临时目录, +// 敏感目录掩蔽;offline 变体额外断网。 +export type CommandSafetyMode = "ask" | "auto" | "sandbox" | "sandboxOffline"; + +export const COMMAND_SAFETY_MODES: readonly CommandSafetyMode[] = [ + "ask", + "auto", + "sandbox", + "sandboxOffline", +]; + export type SystemSettings = { executionMode: ExecutionMode; workdir: string; @@ -241,6 +256,7 @@ export type SystemSettings = { * 可选:旧快照缺失该字段时视为空表(全部走默认),保证零回归。 */ toolPolicies?: Record; + commandSafetyMode: CommandSafetyMode; workspaceProjects: WorkspaceProject[]; workspaceProjectGroups: WorkspaceProjectGroup[]; activeWorkspaceProjectId?: string; diff --git a/crates/agent-ui/src/pages/chat/ChatComposerBar.tsx b/crates/agent-ui/src/pages/chat/ChatComposerBar.tsx index 864853f67..456258b5d 100644 --- a/crates/agent-ui/src/pages/chat/ChatComposerBar.tsx +++ b/crates/agent-ui/src/pages/chat/ChatComposerBar.tsx @@ -1,11 +1,13 @@ import { type ChatRuntimeControls, + type CommandSafetyMode, type ExecutionMode, isAgentExecutionMode, type ProviderId, type ReasoningLevel, type SelectedModel, } from "@liveagent/app/lib/settings"; +import { CommandSafetyModeSelector } from "@liveagent/ui/components/chat/CommandSafetyModeSelector"; import { ComposerAttachmentCard } from "@liveagent/ui/components/chat/ComposerAttachmentCard"; import { ComposerModelControls } from "@liveagent/ui/components/chat/ComposerModelControls"; import { ContextUsageRing } from "@liveagent/ui/components/chat/ContextUsageRing"; @@ -225,6 +227,9 @@ export type ChatComposerBarProps = { modelOptions: SharedModelOption[]; selectedValue?: string; chatRuntimeControls: ChatRuntimeControls; + /** 命令执行方式(ask/auto/sandbox/sandboxOffline);缺省不渲染选择器。 */ + commandSafetyMode?: CommandSafetyMode; + onCommandSafetyModeChange?: (mode: CommandSafetyMode) => void; reasoningOptions: ReasoningLevel[]; thinkingAlwaysOn: boolean; gitClient?: GitClient | null; @@ -292,6 +297,8 @@ export const ChatComposerBar = memo(function ChatComposerBar(props: ChatComposer modelOptions, selectedValue, chatRuntimeControls, + commandSafetyMode, + onCommandSafetyModeChange, reasoningOptions, thinkingAlwaysOn, gitClient, @@ -997,6 +1004,15 @@ export const ChatComposerBar = memo(function ChatComposerBar(props: ChatComposer onChatRuntimeControlsChange={onChatRuntimeControlsChange} /> + {isAgentMode && commandSafetyMode && onCommandSafetyModeChange ? ( + + ) : null} +