diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b65482fc3..ec4f05bce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2267,6 +2267,20 @@ jobs: -f tools/toolchain/gate-tools.nix env --arg attrs "$attrs") echo "$out/bin" >>"$GITHUB_PATH" + - name: Stage the secretspec cdylib and seed a throwaway master key + # T5 resolves the master key unconditionally at boot, so the server now + # fails closed without both the cdylib its read path dlopens (the pgtest + # job's recipe) and a resolvable key. The dotenv is the only declared + # name here: e2e configures no forge, and every name is required=true. + run: | + libsecretspec=$(nix build --no-link --print-out-paths \ + -f tools/toolchain/secretspec-env.nix libsecretspec) + echo "SECRETSPEC_FFI_LIB=$libsecretspec/lib/libsecretspec.so" >>"$GITHUB_ENV" + secrets_env=/tmp/compass-e2e-secrets.env + umask 077 + echo "COMPASS_MASTER_KEY=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" >"$secrets_env" + echo "COMPASS_SECRET_PROVIDER=dotenv://$secrets_env" >>"$GITHUB_ENV" + - name: Detect whether this PR changes the compass-agent image id: image_affected # PRs only. Whether the e2e gate must test a LOCALLY-BUILT image or the diff --git a/go/cmd/compass-server/main.go b/go/cmd/compass-server/main.go index 5486a3f13..1c7a70f30 100644 --- a/go/cmd/compass-server/main.go +++ b/go/cmd/compass-server/main.go @@ -227,6 +227,7 @@ func buildServeConfig(args []string) (server.ServeConfig, bool, error) { StateDir: *f.stateDir, AdminHandle: *f.adminHandle, CORSAllowedOrigin: *f.corsAllowedOrigin, + SecretProvider: firstNonEmpty(*f.secretProvider, os.Getenv("COMPASS_SECRET_PROVIDER")), PublicURL: firstNonEmpty(*f.publicURL, os.Getenv("COMPASS_PUBLIC_URL")), // ENV-ONLY knob (Matt 2026-08-28): the OTLP exporter and the enable-gate // read one source, so no --otel-endpoint flag. Empty = tracing off. @@ -267,6 +268,7 @@ type serveFlags struct { s3Region *string s3UseTLS *bool stateDir *string + secretProvider *string adminHandle *string corsAllowedOrigin *string publicURL *string @@ -317,6 +319,10 @@ func registerServeFlags(fs *flag.FlagSet) serveFlags { stateDir: fs.String("state-dir", "", "Directory the bootstrap-admin token file is written under (0600). "+ "Defaults to the socket's parent directory."), + secretProvider: fs.String("secret-provider", "", + "SecretSpec provider URI both secret resolvers read (e.g. "+ + "\"keyring://\", \"dotenv:///path/.env\"). Empty = the SDK's "+ + "default chain. Defaults to $COMPASS_SECRET_PROVIDER."), adminHandle: fs.String("admin-handle", "", "Handle of the bootstrap-admin account created (or found) at startup. "+ "Defaults to \"admin\". A handle that already names a non-admin "+ diff --git a/go/cmd/compass/secret.go b/go/cmd/compass/secret.go index 899a88bf0..c40a90487 100644 --- a/go/cmd/compass/secret.go +++ b/go/cmd/compass/secret.go @@ -27,6 +27,9 @@ const ( kindGeneric = "generic" kindProvider = "provider" kindGH = "gh" + + scopeUser = "user" + scopeTenant = "tenant" ) // maxSecretBytes caps the stdin read in `secret set`. A secret value (an API @@ -53,7 +56,7 @@ func newSecretCmd() *cobra.Command { // read from stdin, never a flag or positional, so it cannot leak into the // process table (the load-bearing convention shared with the bearer token). func newSecretSetCmd() *cobra.Command { - var delivery, kind, provider, host string + var delivery, kind, provider, host, scope string cmd := &cobra.Command{ Use: "set ", Short: "Declare a secret and write its value (value read from stdin, admin)", @@ -69,6 +72,7 @@ func newSecretSetCmd() *cobra.Command { kind: kind, provider: provider, host: host, + scope: scope, }, cmd.InOrStdin(), cmd.OutOrStdout()) }, } @@ -80,6 +84,8 @@ func newSecretSetCmd() *cobra.Command { "LLM provider id (required when --kind provider).") cmd.Flags().StringVar(&host, "host", "", "gh host (required when --kind gh).") + cmd.Flags().StringVar(&scope, "scope", scopeUser, + "Scope the write targets: user (private, default) or tenant (shared, admin-only).") return cmd } @@ -102,7 +108,8 @@ func newSecretListCmd() *cobra.Command { // newSecretDeleteCmd builds `secret delete `: DeleteSecret and confirm. func newSecretDeleteCmd() *cobra.Command { - return &cobra.Command{ + var scope string + cmd := &cobra.Command{ Use: "delete ", Short: "Delete a declared secret (admin)", Args: cobra.ExactArgs(1), @@ -111,9 +118,12 @@ func newSecretDeleteCmd() *cobra.Command { if err != nil { return err } - return runSecretDelete(cmd.Context(), client, args[0], cmd.OutOrStdout()) + return runSecretDelete(cmd.Context(), client, args[0], scope, cmd.OutOrStdout()) }, } + cmd.Flags().StringVar(&scope, "scope", scopeUser, + "Scope the delete targets: user (private, default) or tenant (shared, admin-only).") + return cmd } // errEmptySecretValue names the empty-stdin rejection: a secret value is @@ -150,6 +160,7 @@ type secretSetArgs struct { kind string provider string host string + scope string } // parseDelivery maps the --delivery flag to its enum. It is required, so an @@ -206,6 +217,21 @@ func parseKind(kind, provider, host string) (compassv1.SecretKind, error) { } } +// parseScope maps the --scope flag to its proto enum. Only user and tenant are +// reachable: an agent scope names a coordinate no CLI caller can write. An empty +// or unknown value is a clear error naming the valid choices. +func parseScope(s string) (compassv1.SecretScope, error) { + switch s { + case scopeUser: + return compassv1.SecretScope_SECRET_SCOPE_USER, nil + case scopeTenant: + return compassv1.SecretScope_SECRET_SCOPE_TENANT, nil + default: + return compassv1.SecretScope_SECRET_SCOPE_UNSPECIFIED, + fmt.Errorf("unknown scope %q: pass --scope user or --scope tenant", s) + } +} + // runSecretSet validates the routing flags, reads the value from in (trimming a // single trailing newline and rejecting an empty value), and calls SetSecret. // The value is never taken from argv, so it cannot leak into the process table. @@ -218,6 +244,10 @@ func runSecretSet(ctx context.Context, client compassv1connect.SecretsServiceCli if err != nil { return err } + scope, err := parseScope(args.scope) + if err != nil { + return err + } value, err := readSecretValue(in) if err != nil { return err @@ -232,6 +262,7 @@ func runSecretSet(ctx context.Context, client compassv1connect.SecretsServiceCli Kind: kind, Provider: args.provider, Host: args.host, + Scope: scope, })); err != nil { return fmt.Errorf("setting secret %s: %w", args.name, err) } @@ -310,12 +341,16 @@ func kindLabel(k compassv1.SecretKind) string { } // runSecretDelete calls DeleteSecret and confirms. -func runSecretDelete(ctx context.Context, client compassv1connect.SecretsServiceClient, name string, out io.Writer) error { +func runSecretDelete(ctx context.Context, client compassv1connect.SecretsServiceClient, name, scopeFlag string, out io.Writer) error { + scope, err := parseScope(scopeFlag) + if err != nil { + return err + } ctx, cancel := context.WithTimeout(ctx, rpcTimeout) defer cancel() - if _, err := client.DeleteSecret(ctx, connect.NewRequest(&compassv1.DeleteSecretRequest{Name: name})); err != nil { + if _, err := client.DeleteSecret(ctx, connect.NewRequest(&compassv1.DeleteSecretRequest{Name: name, Scope: scope})); err != nil { return fmt.Errorf("deleting secret %s: %w", name, err) } - _, err := fmt.Fprintf(out, "deleted secret %s\n", name) + _, err = fmt.Fprintf(out, "deleted secret %s\n", name) return err } diff --git a/go/cmd/compass/secret_test.go b/go/cmd/compass/secret_test.go index ed060c8c1..b5033d382 100644 --- a/go/cmd/compass/secret_test.go +++ b/go/cmd/compass/secret_test.go @@ -73,7 +73,7 @@ func TestRunSecretSet(t *testing.T) { var out strings.Builder in := strings.NewReader("s3cr3t\n") - args := secretSetArgs{name: "OPENAI_KEY", delivery: "env", kind: "generic"} + args := secretSetArgs{name: "OPENAI_KEY", delivery: "env", kind: "generic", scope: "user"} if err := runSecretSet(context.Background(), client, args, in, &out); err != nil { t.Fatalf("runSecretSet: %v", err) } @@ -104,7 +104,7 @@ func TestRunSecretSetProviderKind(t *testing.T) { client := startFakeSecretsServer(t, fake) var out strings.Builder - args := secretSetArgs{name: "ANTHROPIC", delivery: "file", kind: "provider", provider: "anthropic"} + args := secretSetArgs{name: "ANTHROPIC", delivery: "file", kind: "provider", provider: "anthropic", scope: "user"} if err := runSecretSet(context.Background(), client, args, strings.NewReader("v"), &out); err != nil { t.Fatalf("runSecretSet: %v", err) } @@ -126,7 +126,7 @@ func TestRunSecretSetGhKind(t *testing.T) { client := startFakeSecretsServer(t, fake) var out strings.Builder - args := secretSetArgs{name: "GH", delivery: "env", kind: "gh", host: "github.com"} + args := secretSetArgs{name: "GH", delivery: "env", kind: "gh", host: "github.com", scope: "user"} if err := runSecretSet(context.Background(), client, args, strings.NewReader("tok"), &out); err != nil { t.Fatalf("runSecretSet: %v", err) } @@ -180,7 +180,7 @@ func TestRunSecretSetRejections(t *testing.T) { }, { name: "empty stdin value", - args: secretSetArgs{name: "X", delivery: "env", kind: "generic"}, + args: secretSetArgs{name: "X", delivery: "env", kind: "generic", scope: "user"}, in: "\n", want: "value is required", }, @@ -262,7 +262,7 @@ func TestRunSecretDelete(t *testing.T) { fake := &fakeSecrets{} client := startFakeSecretsServer(t, fake) var out strings.Builder - if err := runSecretDelete(context.Background(), client, "OPENAI_KEY", &out); err != nil { + if err := runSecretDelete(context.Background(), client, "OPENAI_KEY", "user", &out); err != nil { t.Fatalf("runSecretDelete: %v", err) } if fake.deleteCalls != 1 { @@ -339,7 +339,7 @@ func TestRunSecretSetBound(t *testing.T) { client := startFakeSecretsServer(t, fake) var out strings.Builder in := strings.NewReader(strings.Repeat("a", maxSecretBytes+1)) - err := runSecretSet(context.Background(), client, secretSetArgs{name: "X", delivery: "env", kind: "generic"}, in, &out) + err := runSecretSet(context.Background(), client, secretSetArgs{name: "X", delivery: "env", kind: "generic", scope: "user"}, in, &out) if err == nil { t.Fatal("runSecretSet with oversized stdin = nil error, want rejection") } @@ -356,7 +356,7 @@ func TestRunSecretSetBound(t *testing.T) { client := startFakeSecretsServer(t, fake) var out strings.Builder in := strings.NewReader(strings.Repeat("a", maxSecretBytes)) - if err := runSecretSet(context.Background(), client, secretSetArgs{name: "X", delivery: "env", kind: "generic"}, in, &out); err != nil { + if err := runSecretSet(context.Background(), client, secretSetArgs{name: "X", delivery: "env", kind: "generic", scope: "user"}, in, &out); err != nil { t.Fatalf("runSecretSet at cap: %v", err) } if fake.gotSet == nil { @@ -372,7 +372,7 @@ func TestRunSecretSetBound(t *testing.T) { client := startFakeSecretsServer(t, fake) var out strings.Builder in := strings.NewReader(strings.Repeat("a", maxSecretBytes) + "\n") - if err := runSecretSet(context.Background(), client, secretSetArgs{name: "X", delivery: "env", kind: "generic"}, in, &out); err != nil { + if err := runSecretSet(context.Background(), client, secretSetArgs{name: "X", delivery: "env", kind: "generic", scope: "user"}, in, &out); err != nil { t.Fatalf("runSecretSet at cap with trailing newline: %v", err) } if fake.gotSet == nil { diff --git a/go/gen/compass/v1/compass.pb.go b/go/gen/compass/v1/compass.pb.go index b0cde22c8..88585df81 100644 --- a/go/gen/compass/v1/compass.pb.go +++ b/go/gen/compass/v1/compass.pb.go @@ -136,6 +136,61 @@ func (SecretKind) EnumDescriptor() ([]byte, []int) { return file_compass_v1_compass_proto_rawDescGZIP(), []int{1} } +// The tier a user-secret write targets. Names the same tiers as the store's +// SecretScope* constants but NOT the same numbers: proto reserves 0 for +// unspecified, so tenant is 2 here but 0 in the store — the handler maps +// explicitly, never casts. The unspecified default is USER: a client that omits +// the field writes its own private coordinate, never a tenant-wide value every +// other user's agents resolve. +type SecretScope int32 + +const ( + SecretScope_SECRET_SCOPE_UNSPECIFIED SecretScope = 0 // treated as USER — private by default + SecretScope_SECRET_SCOPE_USER SecretScope = 1 + SecretScope_SECRET_SCOPE_TENANT SecretScope = 2 // admin-only +) + +// Enum value maps for SecretScope. +var ( + SecretScope_name = map[int32]string{ + 0: "SECRET_SCOPE_UNSPECIFIED", + 1: "SECRET_SCOPE_USER", + 2: "SECRET_SCOPE_TENANT", + } + SecretScope_value = map[string]int32{ + "SECRET_SCOPE_UNSPECIFIED": 0, + "SECRET_SCOPE_USER": 1, + "SECRET_SCOPE_TENANT": 2, + } +) + +func (x SecretScope) Enum() *SecretScope { + p := new(SecretScope) + *p = x + return p +} + +func (x SecretScope) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SecretScope) Descriptor() protoreflect.EnumDescriptor { + return file_compass_v1_compass_proto_enumTypes[2].Descriptor() +} + +func (SecretScope) Type() protoreflect.EnumType { + return &file_compass_v1_compass_proto_enumTypes[2] +} + +func (x SecretScope) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SecretScope.Descriptor instead. +func (SecretScope) EnumDescriptor() ([]byte, []int) { + return file_compass_v1_compass_proto_rawDescGZIP(), []int{2} +} + type ServerState int32 const ( @@ -166,11 +221,11 @@ func (x ServerState) String() string { } func (ServerState) Descriptor() protoreflect.EnumDescriptor { - return file_compass_v1_compass_proto_enumTypes[2].Descriptor() + return file_compass_v1_compass_proto_enumTypes[3].Descriptor() } func (ServerState) Type() protoreflect.EnumType { - return &file_compass_v1_compass_proto_enumTypes[2] + return &file_compass_v1_compass_proto_enumTypes[3] } func (x ServerState) Number() protoreflect.EnumNumber { @@ -179,7 +234,7 @@ func (x ServerState) Number() protoreflect.EnumNumber { // Deprecated: Use ServerState.Descriptor instead. func (ServerState) EnumDescriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{2} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{3} } // The runtime tier an agent workload runs on. `HOST` runs agents as direct @@ -223,11 +278,11 @@ func (x RuntimeTier) String() string { } func (RuntimeTier) Descriptor() protoreflect.EnumDescriptor { - return file_compass_v1_compass_proto_enumTypes[3].Descriptor() + return file_compass_v1_compass_proto_enumTypes[4].Descriptor() } func (RuntimeTier) Type() protoreflect.EnumType { - return &file_compass_v1_compass_proto_enumTypes[3] + return &file_compass_v1_compass_proto_enumTypes[4] } func (x RuntimeTier) Number() protoreflect.EnumNumber { @@ -236,7 +291,7 @@ func (x RuntimeTier) Number() protoreflect.EnumNumber { // Deprecated: Use RuntimeTier.Descriptor instead. func (RuntimeTier) EnumDescriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{3} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{4} } // How an agent's egress is constrained. `UNENFORCED` means the tier cannot @@ -276,11 +331,11 @@ func (x EgressPosture) String() string { } func (EgressPosture) Descriptor() protoreflect.EnumDescriptor { - return file_compass_v1_compass_proto_enumTypes[4].Descriptor() + return file_compass_v1_compass_proto_enumTypes[5].Descriptor() } func (EgressPosture) Type() protoreflect.EnumType { - return &file_compass_v1_compass_proto_enumTypes[4] + return &file_compass_v1_compass_proto_enumTypes[5] } func (x EgressPosture) Number() protoreflect.EnumNumber { @@ -289,7 +344,7 @@ func (x EgressPosture) Number() protoreflect.EnumNumber { // Deprecated: Use EgressPosture.Descriptor instead. func (EgressPosture) EnumDescriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{4} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{5} } // Agent-session lifecycle states. `ERRORED` is an unexpected agent exit (OOM, @@ -354,11 +409,11 @@ func (x AgentSessionState) String() string { } func (AgentSessionState) Descriptor() protoreflect.EnumDescriptor { - return file_compass_v1_compass_proto_enumTypes[5].Descriptor() + return file_compass_v1_compass_proto_enumTypes[6].Descriptor() } func (AgentSessionState) Type() protoreflect.EnumType { - return &file_compass_v1_compass_proto_enumTypes[5] + return &file_compass_v1_compass_proto_enumTypes[6] } func (x AgentSessionState) Number() protoreflect.EnumNumber { @@ -367,7 +422,7 @@ func (x AgentSessionState) Number() protoreflect.EnumNumber { // Deprecated: Use AgentSessionState.Descriptor instead. func (AgentSessionState) EnumDescriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{5} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{6} } type AgentToolCallStatus int32 @@ -409,11 +464,11 @@ func (x AgentToolCallStatus) String() string { } func (AgentToolCallStatus) Descriptor() protoreflect.EnumDescriptor { - return file_compass_v1_compass_proto_enumTypes[6].Descriptor() + return file_compass_v1_compass_proto_enumTypes[7].Descriptor() } func (AgentToolCallStatus) Type() protoreflect.EnumType { - return &file_compass_v1_compass_proto_enumTypes[6] + return &file_compass_v1_compass_proto_enumTypes[7] } func (x AgentToolCallStatus) Number() protoreflect.EnumNumber { @@ -422,7 +477,7 @@ func (x AgentToolCallStatus) Number() protoreflect.EnumNumber { // Deprecated: Use AgentToolCallStatus.Descriptor instead. func (AgentToolCallStatus) EnumDescriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{6} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{7} } type AgentPlanEntryStatus int32 @@ -461,11 +516,11 @@ func (x AgentPlanEntryStatus) String() string { } func (AgentPlanEntryStatus) Descriptor() protoreflect.EnumDescriptor { - return file_compass_v1_compass_proto_enumTypes[7].Descriptor() + return file_compass_v1_compass_proto_enumTypes[8].Descriptor() } func (AgentPlanEntryStatus) Type() protoreflect.EnumType { - return &file_compass_v1_compass_proto_enumTypes[7] + return &file_compass_v1_compass_proto_enumTypes[8] } func (x AgentPlanEntryStatus) Number() protoreflect.EnumNumber { @@ -474,7 +529,7 @@ func (x AgentPlanEntryStatus) Number() protoreflect.EnumNumber { // Deprecated: Use AgentPlanEntryStatus.Descriptor instead. func (AgentPlanEntryStatus) EnumDescriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{7} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{8} } // The control op-kind a SessionInjection records. Mirrors the internal @@ -515,11 +570,11 @@ func (x SessionInjectionKind) String() string { } func (SessionInjectionKind) Descriptor() protoreflect.EnumDescriptor { - return file_compass_v1_compass_proto_enumTypes[8].Descriptor() + return file_compass_v1_compass_proto_enumTypes[9].Descriptor() } func (SessionInjectionKind) Type() protoreflect.EnumType { - return &file_compass_v1_compass_proto_enumTypes[8] + return &file_compass_v1_compass_proto_enumTypes[9] } func (x SessionInjectionKind) Number() protoreflect.EnumNumber { @@ -528,7 +583,7 @@ func (x SessionInjectionKind) Number() protoreflect.EnumNumber { // Deprecated: Use SessionInjectionKind.Descriptor instead. func (SessionInjectionKind) EnumDescriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{8} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{9} } // The class of a SessionError. ERROR pairs with the ERRORED lifecycle @@ -567,11 +622,11 @@ func (x SessionErrorKind) String() string { } func (SessionErrorKind) Descriptor() protoreflect.EnumDescriptor { - return file_compass_v1_compass_proto_enumTypes[9].Descriptor() + return file_compass_v1_compass_proto_enumTypes[10].Descriptor() } func (SessionErrorKind) Type() protoreflect.EnumType { - return &file_compass_v1_compass_proto_enumTypes[9] + return &file_compass_v1_compass_proto_enumTypes[10] } func (x SessionErrorKind) Number() protoreflect.EnumNumber { @@ -580,7 +635,7 @@ func (x SessionErrorKind) Number() protoreflect.EnumNumber { // Deprecated: Use SessionErrorKind.Descriptor instead. func (SessionErrorKind) EnumDescriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{9} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{10} } // The Compass issue lifecycle, server-owned (DL-032/DL-033 + terminal ARCHIVED, @@ -639,11 +694,11 @@ func (x IssueState) String() string { } func (IssueState) Descriptor() protoreflect.EnumDescriptor { - return file_compass_v1_compass_proto_enumTypes[10].Descriptor() + return file_compass_v1_compass_proto_enumTypes[11].Descriptor() } func (IssueState) Type() protoreflect.EnumType { - return &file_compass_v1_compass_proto_enumTypes[10] + return &file_compass_v1_compass_proto_enumTypes[11] } func (x IssueState) Number() protoreflect.EnumNumber { @@ -652,7 +707,7 @@ func (x IssueState) Number() protoreflect.EnumNumber { // Deprecated: Use IssueState.Descriptor instead. func (IssueState) EnumDescriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{10} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{11} } // Which forge (and which host, for self-hosted instances) an artifact lives on. @@ -697,11 +752,11 @@ func (x ForgeProvider) String() string { } func (ForgeProvider) Descriptor() protoreflect.EnumDescriptor { - return file_compass_v1_compass_proto_enumTypes[11].Descriptor() + return file_compass_v1_compass_proto_enumTypes[12].Descriptor() } func (ForgeProvider) Type() protoreflect.EnumType { - return &file_compass_v1_compass_proto_enumTypes[11] + return &file_compass_v1_compass_proto_enumTypes[12] } func (x ForgeProvider) Number() protoreflect.EnumNumber { @@ -710,7 +765,7 @@ func (x ForgeProvider) Number() protoreflect.EnumNumber { // Deprecated: Use ForgeProvider.Descriptor instead. func (ForgeProvider) EnumDescriptor() ([]byte, []int) { - return file_compass_v1_compass_proto_rawDescGZIP(), []int{11} + return file_compass_v1_compass_proto_rawDescGZIP(), []int{12} } type SetSecretRequest struct { @@ -719,8 +774,9 @@ type SetSecretRequest struct { Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` Delivery SecretDelivery `protobuf:"varint,3,opt,name=delivery,proto3,enum=compass.v1.SecretDelivery" json:"delivery,omitempty"` Kind SecretKind `protobuf:"varint,4,opt,name=kind,proto3,enum=compass.v1.SecretKind" json:"kind,omitempty"` - Provider string `protobuf:"bytes,5,opt,name=provider,proto3" json:"provider,omitempty"` // set for SECRET_KIND_PROVIDER - Host string `protobuf:"bytes,6,opt,name=host,proto3" json:"host,omitempty"` // set for SECRET_KIND_GH + Provider string `protobuf:"bytes,5,opt,name=provider,proto3" json:"provider,omitempty"` // set for SECRET_KIND_PROVIDER + Host string `protobuf:"bytes,6,opt,name=host,proto3" json:"host,omitempty"` // set for SECRET_KIND_GH + Scope SecretScope `protobuf:"varint,7,opt,name=scope,proto3,enum=compass.v1.SecretScope" json:"scope,omitempty"` // tier the write targets; unspecified == user unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -797,6 +853,13 @@ func (x *SetSecretRequest) GetHost() string { return "" } +func (x *SetSecretRequest) GetScope() SecretScope { + if x != nil { + return x.Scope + } + return SecretScope_SECRET_SCOPE_UNSPECIFIED +} + type SetSecretResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -1001,6 +1064,7 @@ func (x *SecretStatus) GetHost() string { type DeleteSecretRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Scope SecretScope `protobuf:"varint,2,opt,name=scope,proto3,enum=compass.v1.SecretScope" json:"scope,omitempty"` // tier the delete targets; unspecified == user unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1042,6 +1106,13 @@ func (x *DeleteSecretRequest) GetName() string { return "" } +func (x *DeleteSecretRequest) GetScope() SecretScope { + if x != nil { + return x.Scope + } + return SecretScope_SECRET_SCOPE_UNSPECIFIED +} + type DeleteSecretResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -5933,14 +6004,15 @@ var File_compass_v1_compass_proto protoreflect.FileDescriptor const file_compass_v1_compass_proto_rawDesc = "" + "\n" + "\x18compass/v1/compass.proto\x12\n" + - "compass.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xd5\x01\n" + + "compass.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\x84\x02\n" + "\x10SetSecretRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x19\n" + "\x05value\x18\x02 \x01(\tB\x03\x80\x01\x01R\x05value\x126\n" + "\bdelivery\x18\x03 \x01(\x0e2\x1a.compass.v1.SecretDeliveryR\bdelivery\x12*\n" + "\x04kind\x18\x04 \x01(\x0e2\x16.compass.v1.SecretKindR\x04kind\x12\x1a\n" + "\bprovider\x18\x05 \x01(\tR\bprovider\x12\x12\n" + - "\x04host\x18\x06 \x01(\tR\x04host\"\x13\n" + + "\x04host\x18\x06 \x01(\tR\x04host\x12-\n" + + "\x05scope\x18\a \x01(\x0e2\x17.compass.v1.SecretScopeR\x05scope\"\x13\n" + "\x11SetSecretResponse\"\x14\n" + "\x12ListSecretsRequest\"I\n" + "\x13ListSecretsResponse\x122\n" + @@ -5951,9 +6023,10 @@ const file_compass_v1_compass_proto_rawDesc = "" + "\bdelivery\x18\x03 \x01(\x0e2\x1a.compass.v1.SecretDeliveryR\bdelivery\x12*\n" + "\x04kind\x18\x04 \x01(\x0e2\x16.compass.v1.SecretKindR\x04kind\x12\x1a\n" + "\bprovider\x18\x05 \x01(\tR\bprovider\x12\x12\n" + - "\x04host\x18\x06 \x01(\tR\x04host\")\n" + + "\x04host\x18\x06 \x01(\tR\x04host\"X\n" + "\x13DeleteSecretRequest\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\"\x16\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12-\n" + + "\x05scope\x18\x02 \x01(\x0e2\x17.compass.v1.SecretScopeR\x05scope\"\x16\n" + "\x14DeleteSecretResponse\"G\n" + "\x16SetServerSecretRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x19\n" + @@ -6281,7 +6354,11 @@ const file_compass_v1_compass_proto_rawDesc = "" + "\x17SECRET_KIND_UNSPECIFIED\x10\x00\x12\x17\n" + "\x13SECRET_KIND_GENERIC\x10\x01\x12\x18\n" + "\x14SECRET_KIND_PROVIDER\x10\x02\x12\x12\n" + - "\x0eSECRET_KIND_GH\x10\x03*C\n" + + "\x0eSECRET_KIND_GH\x10\x03*[\n" + + "\vSecretScope\x12\x1c\n" + + "\x18SECRET_SCOPE_UNSPECIFIED\x10\x00\x12\x15\n" + + "\x11SECRET_SCOPE_USER\x10\x01\x12\x17\n" + + "\x13SECRET_SCOPE_TENANT\x10\x02*C\n" + "\vServerState\x12\x1c\n" + "\x18SERVER_STATE_UNSPECIFIED\x10\x00\x12\x16\n" + "\x12SERVER_STATE_READY\x10\x01*\x97\x01\n" + @@ -6382,227 +6459,230 @@ func file_compass_v1_compass_proto_rawDescGZIP() []byte { return file_compass_v1_compass_proto_rawDescData } -var file_compass_v1_compass_proto_enumTypes = make([]protoimpl.EnumInfo, 12) +var file_compass_v1_compass_proto_enumTypes = make([]protoimpl.EnumInfo, 13) var file_compass_v1_compass_proto_msgTypes = make([]protoimpl.MessageInfo, 87) var file_compass_v1_compass_proto_goTypes = []any{ (SecretDelivery)(0), // 0: compass.v1.SecretDelivery (SecretKind)(0), // 1: compass.v1.SecretKind - (ServerState)(0), // 2: compass.v1.ServerState - (RuntimeTier)(0), // 3: compass.v1.RuntimeTier - (EgressPosture)(0), // 4: compass.v1.EgressPosture - (AgentSessionState)(0), // 5: compass.v1.AgentSessionState - (AgentToolCallStatus)(0), // 6: compass.v1.AgentToolCallStatus - (AgentPlanEntryStatus)(0), // 7: compass.v1.AgentPlanEntryStatus - (SessionInjectionKind)(0), // 8: compass.v1.SessionInjectionKind - (SessionErrorKind)(0), // 9: compass.v1.SessionErrorKind - (IssueState)(0), // 10: compass.v1.IssueState - (ForgeProvider)(0), // 11: compass.v1.ForgeProvider - (*SetSecretRequest)(nil), // 12: compass.v1.SetSecretRequest - (*SetSecretResponse)(nil), // 13: compass.v1.SetSecretResponse - (*ListSecretsRequest)(nil), // 14: compass.v1.ListSecretsRequest - (*ListSecretsResponse)(nil), // 15: compass.v1.ListSecretsResponse - (*SecretStatus)(nil), // 16: compass.v1.SecretStatus - (*DeleteSecretRequest)(nil), // 17: compass.v1.DeleteSecretRequest - (*DeleteSecretResponse)(nil), // 18: compass.v1.DeleteSecretResponse - (*SetServerSecretRequest)(nil), // 19: compass.v1.SetServerSecretRequest - (*SetServerSecretResponse)(nil), // 20: compass.v1.SetServerSecretResponse - (*DeleteServerSecretRequest)(nil), // 21: compass.v1.DeleteServerSecretRequest - (*DeleteServerSecretResponse)(nil), // 22: compass.v1.DeleteServerSecretResponse - (*ListServerSecretsRequest)(nil), // 23: compass.v1.ListServerSecretsRequest - (*ListServerSecretsResponse)(nil), // 24: compass.v1.ListServerSecretsResponse - (*ServerSecretStatus)(nil), // 25: compass.v1.ServerSecretStatus - (*GetServerInfoRequest)(nil), // 26: compass.v1.GetServerInfoRequest - (*GetServerInfoResponse)(nil), // 27: compass.v1.GetServerInfoResponse - (*WhoAmIRequest)(nil), // 28: compass.v1.WhoAmIRequest - (*WhoAmIResponse)(nil), // 29: compass.v1.WhoAmIResponse - (*SubscribeEventsRequest)(nil), // 30: compass.v1.SubscribeEventsRequest - (*SubscribeEventsResponse)(nil), // 31: compass.v1.SubscribeEventsResponse - (*ListBoardIssuesRequest)(nil), // 32: compass.v1.ListBoardIssuesRequest - (*ListBoardIssuesResponse)(nil), // 33: compass.v1.ListBoardIssuesResponse - (*ServerStatus)(nil), // 34: compass.v1.ServerStatus - (*ResyncRequired)(nil), // 35: compass.v1.ResyncRequired - (*AgentSessionStatus)(nil), // 36: compass.v1.AgentSessionStatus - (*AgentMessageChunk)(nil), // 37: compass.v1.AgentMessageChunk - (*AgentToolCall)(nil), // 38: compass.v1.AgentToolCall - (*AgentPlan)(nil), // 39: compass.v1.AgentPlan - (*AgentPlanEntry)(nil), // 40: compass.v1.AgentPlanEntry - (*SessionEvent)(nil), // 41: compass.v1.SessionEvent - (*SessionAssistantText)(nil), // 42: compass.v1.SessionAssistantText - (*SessionThinking)(nil), // 43: compass.v1.SessionThinking - (*SessionToolCall)(nil), // 44: compass.v1.SessionToolCall - (*SessionToolCallUpdate)(nil), // 45: compass.v1.SessionToolCallUpdate - (*SessionFileDiff)(nil), // 46: compass.v1.SessionFileDiff - (*SessionPlan)(nil), // 47: compass.v1.SessionPlan - (*SessionNotice)(nil), // 48: compass.v1.SessionNotice - (*SessionInjection)(nil), // 49: compass.v1.SessionInjection - (*SessionError)(nil), // 50: compass.v1.SessionError - (*SubscribeAgentSessionRequest)(nil), // 51: compass.v1.SubscribeAgentSessionRequest - (*AgentSessionFrame)(nil), // 52: compass.v1.AgentSessionFrame - (*ProvisionAgentWorkspaceRequest)(nil), // 53: compass.v1.ProvisionAgentWorkspaceRequest - (*ProvisionAgentWorkspaceResponse)(nil), // 54: compass.v1.ProvisionAgentWorkspaceResponse - (*RemoveAgentWorkspaceRequest)(nil), // 55: compass.v1.RemoveAgentWorkspaceRequest - (*RemoveAgentWorkspaceResponse)(nil), // 56: compass.v1.RemoveAgentWorkspaceResponse - (*StartAgentSessionRequest)(nil), // 57: compass.v1.StartAgentSessionRequest - (*StartAgentSessionResponse)(nil), // 58: compass.v1.StartAgentSessionResponse - (*SpawnAgentRequest)(nil), // 59: compass.v1.SpawnAgentRequest - (*SpawnAgentResponse)(nil), // 60: compass.v1.SpawnAgentResponse - (*StopAgentSessionRequest)(nil), // 61: compass.v1.StopAgentSessionRequest - (*StopAgentSessionResponse)(nil), // 62: compass.v1.StopAgentSessionResponse - (*ReloadAgentSessionRequest)(nil), // 63: compass.v1.ReloadAgentSessionRequest - (*ReloadAgentSessionResponse)(nil), // 64: compass.v1.ReloadAgentSessionResponse - (*GetAgentStatusRequest)(nil), // 65: compass.v1.GetAgentStatusRequest - (*GetAgentStatusResponse)(nil), // 66: compass.v1.GetAgentStatusResponse - (*IssueTokenRequest)(nil), // 67: compass.v1.IssueTokenRequest - (*IssueTokenResponse)(nil), // 68: compass.v1.IssueTokenResponse - (*RevokeTokenRequest)(nil), // 69: compass.v1.RevokeTokenRequest - (*RevokeTokenResponse)(nil), // 70: compass.v1.RevokeTokenResponse - (*PutAgentConfigRequest)(nil), // 71: compass.v1.PutAgentConfigRequest - (*PutAgentConfigResponse)(nil), // 72: compass.v1.PutAgentConfigResponse - (*GetAgentConfigInfoRequest)(nil), // 73: compass.v1.GetAgentConfigInfoRequest - (*GetAgentConfigInfoResponse)(nil), // 74: compass.v1.GetAgentConfigInfoResponse - (*DeleteAgentConfigRequest)(nil), // 75: compass.v1.DeleteAgentConfigRequest - (*DeleteAgentConfigResponse)(nil), // 76: compass.v1.DeleteAgentConfigResponse - (*ModelCandidate)(nil), // 77: compass.v1.ModelCandidate - (*ModelMetadata)(nil), // 78: compass.v1.ModelMetadata - (*ModelRegistryEntry)(nil), // 79: compass.v1.ModelRegistryEntry - (*ModelRegistry)(nil), // 80: compass.v1.ModelRegistry - (*PutModelRegistryRequest)(nil), // 81: compass.v1.PutModelRegistryRequest - (*PutModelRegistryResponse)(nil), // 82: compass.v1.PutModelRegistryResponse - (*GetModelRegistryRequest)(nil), // 83: compass.v1.GetModelRegistryRequest - (*GetModelRegistryResponse)(nil), // 84: compass.v1.GetModelRegistryResponse - (*DeleteModelRegistryRequest)(nil), // 85: compass.v1.DeleteModelRegistryRequest - (*DeleteModelRegistryResponse)(nil), // 86: compass.v1.DeleteModelRegistryResponse - (*AgentAttribution)(nil), // 87: compass.v1.AgentAttribution - (*ForgeRef)(nil), // 88: compass.v1.ForgeRef - (*Issue)(nil), // 89: compass.v1.Issue - (*PullRequest)(nil), // 90: compass.v1.PullRequest - (*ChecksSummary)(nil), // 91: compass.v1.ChecksSummary - (*Check)(nil), // 92: compass.v1.Check - (*ChangedStats)(nil), // 93: compass.v1.ChangedStats - (*TrackerRef)(nil), // 94: compass.v1.TrackerRef - (*Review)(nil), // 95: compass.v1.Review - (*ReviewThread)(nil), // 96: compass.v1.ReviewThread - (*Comment)(nil), // 97: compass.v1.Comment - nil, // 98: compass.v1.ModelRegistry.EntriesEntry - (*timestamppb.Timestamp)(nil), // 99: google.protobuf.Timestamp + (SecretScope)(0), // 2: compass.v1.SecretScope + (ServerState)(0), // 3: compass.v1.ServerState + (RuntimeTier)(0), // 4: compass.v1.RuntimeTier + (EgressPosture)(0), // 5: compass.v1.EgressPosture + (AgentSessionState)(0), // 6: compass.v1.AgentSessionState + (AgentToolCallStatus)(0), // 7: compass.v1.AgentToolCallStatus + (AgentPlanEntryStatus)(0), // 8: compass.v1.AgentPlanEntryStatus + (SessionInjectionKind)(0), // 9: compass.v1.SessionInjectionKind + (SessionErrorKind)(0), // 10: compass.v1.SessionErrorKind + (IssueState)(0), // 11: compass.v1.IssueState + (ForgeProvider)(0), // 12: compass.v1.ForgeProvider + (*SetSecretRequest)(nil), // 13: compass.v1.SetSecretRequest + (*SetSecretResponse)(nil), // 14: compass.v1.SetSecretResponse + (*ListSecretsRequest)(nil), // 15: compass.v1.ListSecretsRequest + (*ListSecretsResponse)(nil), // 16: compass.v1.ListSecretsResponse + (*SecretStatus)(nil), // 17: compass.v1.SecretStatus + (*DeleteSecretRequest)(nil), // 18: compass.v1.DeleteSecretRequest + (*DeleteSecretResponse)(nil), // 19: compass.v1.DeleteSecretResponse + (*SetServerSecretRequest)(nil), // 20: compass.v1.SetServerSecretRequest + (*SetServerSecretResponse)(nil), // 21: compass.v1.SetServerSecretResponse + (*DeleteServerSecretRequest)(nil), // 22: compass.v1.DeleteServerSecretRequest + (*DeleteServerSecretResponse)(nil), // 23: compass.v1.DeleteServerSecretResponse + (*ListServerSecretsRequest)(nil), // 24: compass.v1.ListServerSecretsRequest + (*ListServerSecretsResponse)(nil), // 25: compass.v1.ListServerSecretsResponse + (*ServerSecretStatus)(nil), // 26: compass.v1.ServerSecretStatus + (*GetServerInfoRequest)(nil), // 27: compass.v1.GetServerInfoRequest + (*GetServerInfoResponse)(nil), // 28: compass.v1.GetServerInfoResponse + (*WhoAmIRequest)(nil), // 29: compass.v1.WhoAmIRequest + (*WhoAmIResponse)(nil), // 30: compass.v1.WhoAmIResponse + (*SubscribeEventsRequest)(nil), // 31: compass.v1.SubscribeEventsRequest + (*SubscribeEventsResponse)(nil), // 32: compass.v1.SubscribeEventsResponse + (*ListBoardIssuesRequest)(nil), // 33: compass.v1.ListBoardIssuesRequest + (*ListBoardIssuesResponse)(nil), // 34: compass.v1.ListBoardIssuesResponse + (*ServerStatus)(nil), // 35: compass.v1.ServerStatus + (*ResyncRequired)(nil), // 36: compass.v1.ResyncRequired + (*AgentSessionStatus)(nil), // 37: compass.v1.AgentSessionStatus + (*AgentMessageChunk)(nil), // 38: compass.v1.AgentMessageChunk + (*AgentToolCall)(nil), // 39: compass.v1.AgentToolCall + (*AgentPlan)(nil), // 40: compass.v1.AgentPlan + (*AgentPlanEntry)(nil), // 41: compass.v1.AgentPlanEntry + (*SessionEvent)(nil), // 42: compass.v1.SessionEvent + (*SessionAssistantText)(nil), // 43: compass.v1.SessionAssistantText + (*SessionThinking)(nil), // 44: compass.v1.SessionThinking + (*SessionToolCall)(nil), // 45: compass.v1.SessionToolCall + (*SessionToolCallUpdate)(nil), // 46: compass.v1.SessionToolCallUpdate + (*SessionFileDiff)(nil), // 47: compass.v1.SessionFileDiff + (*SessionPlan)(nil), // 48: compass.v1.SessionPlan + (*SessionNotice)(nil), // 49: compass.v1.SessionNotice + (*SessionInjection)(nil), // 50: compass.v1.SessionInjection + (*SessionError)(nil), // 51: compass.v1.SessionError + (*SubscribeAgentSessionRequest)(nil), // 52: compass.v1.SubscribeAgentSessionRequest + (*AgentSessionFrame)(nil), // 53: compass.v1.AgentSessionFrame + (*ProvisionAgentWorkspaceRequest)(nil), // 54: compass.v1.ProvisionAgentWorkspaceRequest + (*ProvisionAgentWorkspaceResponse)(nil), // 55: compass.v1.ProvisionAgentWorkspaceResponse + (*RemoveAgentWorkspaceRequest)(nil), // 56: compass.v1.RemoveAgentWorkspaceRequest + (*RemoveAgentWorkspaceResponse)(nil), // 57: compass.v1.RemoveAgentWorkspaceResponse + (*StartAgentSessionRequest)(nil), // 58: compass.v1.StartAgentSessionRequest + (*StartAgentSessionResponse)(nil), // 59: compass.v1.StartAgentSessionResponse + (*SpawnAgentRequest)(nil), // 60: compass.v1.SpawnAgentRequest + (*SpawnAgentResponse)(nil), // 61: compass.v1.SpawnAgentResponse + (*StopAgentSessionRequest)(nil), // 62: compass.v1.StopAgentSessionRequest + (*StopAgentSessionResponse)(nil), // 63: compass.v1.StopAgentSessionResponse + (*ReloadAgentSessionRequest)(nil), // 64: compass.v1.ReloadAgentSessionRequest + (*ReloadAgentSessionResponse)(nil), // 65: compass.v1.ReloadAgentSessionResponse + (*GetAgentStatusRequest)(nil), // 66: compass.v1.GetAgentStatusRequest + (*GetAgentStatusResponse)(nil), // 67: compass.v1.GetAgentStatusResponse + (*IssueTokenRequest)(nil), // 68: compass.v1.IssueTokenRequest + (*IssueTokenResponse)(nil), // 69: compass.v1.IssueTokenResponse + (*RevokeTokenRequest)(nil), // 70: compass.v1.RevokeTokenRequest + (*RevokeTokenResponse)(nil), // 71: compass.v1.RevokeTokenResponse + (*PutAgentConfigRequest)(nil), // 72: compass.v1.PutAgentConfigRequest + (*PutAgentConfigResponse)(nil), // 73: compass.v1.PutAgentConfigResponse + (*GetAgentConfigInfoRequest)(nil), // 74: compass.v1.GetAgentConfigInfoRequest + (*GetAgentConfigInfoResponse)(nil), // 75: compass.v1.GetAgentConfigInfoResponse + (*DeleteAgentConfigRequest)(nil), // 76: compass.v1.DeleteAgentConfigRequest + (*DeleteAgentConfigResponse)(nil), // 77: compass.v1.DeleteAgentConfigResponse + (*ModelCandidate)(nil), // 78: compass.v1.ModelCandidate + (*ModelMetadata)(nil), // 79: compass.v1.ModelMetadata + (*ModelRegistryEntry)(nil), // 80: compass.v1.ModelRegistryEntry + (*ModelRegistry)(nil), // 81: compass.v1.ModelRegistry + (*PutModelRegistryRequest)(nil), // 82: compass.v1.PutModelRegistryRequest + (*PutModelRegistryResponse)(nil), // 83: compass.v1.PutModelRegistryResponse + (*GetModelRegistryRequest)(nil), // 84: compass.v1.GetModelRegistryRequest + (*GetModelRegistryResponse)(nil), // 85: compass.v1.GetModelRegistryResponse + (*DeleteModelRegistryRequest)(nil), // 86: compass.v1.DeleteModelRegistryRequest + (*DeleteModelRegistryResponse)(nil), // 87: compass.v1.DeleteModelRegistryResponse + (*AgentAttribution)(nil), // 88: compass.v1.AgentAttribution + (*ForgeRef)(nil), // 89: compass.v1.ForgeRef + (*Issue)(nil), // 90: compass.v1.Issue + (*PullRequest)(nil), // 91: compass.v1.PullRequest + (*ChecksSummary)(nil), // 92: compass.v1.ChecksSummary + (*Check)(nil), // 93: compass.v1.Check + (*ChangedStats)(nil), // 94: compass.v1.ChangedStats + (*TrackerRef)(nil), // 95: compass.v1.TrackerRef + (*Review)(nil), // 96: compass.v1.Review + (*ReviewThread)(nil), // 97: compass.v1.ReviewThread + (*Comment)(nil), // 98: compass.v1.Comment + nil, // 99: compass.v1.ModelRegistry.EntriesEntry + (*timestamppb.Timestamp)(nil), // 100: google.protobuf.Timestamp } var file_compass_v1_compass_proto_depIdxs = []int32{ - 0, // 0: compass.v1.SetSecretRequest.delivery:type_name -> compass.v1.SecretDelivery - 1, // 1: compass.v1.SetSecretRequest.kind:type_name -> compass.v1.SecretKind - 16, // 2: compass.v1.ListSecretsResponse.secrets:type_name -> compass.v1.SecretStatus - 0, // 3: compass.v1.SecretStatus.delivery:type_name -> compass.v1.SecretDelivery - 1, // 4: compass.v1.SecretStatus.kind:type_name -> compass.v1.SecretKind - 25, // 5: compass.v1.ListServerSecretsResponse.server_secrets:type_name -> compass.v1.ServerSecretStatus - 34, // 6: compass.v1.SubscribeEventsResponse.server_status:type_name -> compass.v1.ServerStatus - 35, // 7: compass.v1.SubscribeEventsResponse.resync_required:type_name -> compass.v1.ResyncRequired - 36, // 8: compass.v1.SubscribeEventsResponse.agent_session_status:type_name -> compass.v1.AgentSessionStatus - 37, // 9: compass.v1.SubscribeEventsResponse.agent_message_chunk:type_name -> compass.v1.AgentMessageChunk - 38, // 10: compass.v1.SubscribeEventsResponse.agent_tool_call:type_name -> compass.v1.AgentToolCall - 39, // 11: compass.v1.SubscribeEventsResponse.agent_plan:type_name -> compass.v1.AgentPlan - 89, // 12: compass.v1.SubscribeEventsResponse.issue:type_name -> compass.v1.Issue - 89, // 13: compass.v1.ListBoardIssuesResponse.issues:type_name -> compass.v1.Issue - 2, // 14: compass.v1.ServerStatus.state:type_name -> compass.v1.ServerState - 5, // 15: compass.v1.AgentSessionStatus.state:type_name -> compass.v1.AgentSessionState - 3, // 16: compass.v1.AgentSessionStatus.runtime_tier:type_name -> compass.v1.RuntimeTier - 4, // 17: compass.v1.AgentSessionStatus.egress_posture:type_name -> compass.v1.EgressPosture - 6, // 18: compass.v1.AgentToolCall.status:type_name -> compass.v1.AgentToolCallStatus - 40, // 19: compass.v1.AgentPlan.entries:type_name -> compass.v1.AgentPlanEntry - 7, // 20: compass.v1.AgentPlanEntry.status:type_name -> compass.v1.AgentPlanEntryStatus - 42, // 21: compass.v1.SessionEvent.assistant_text:type_name -> compass.v1.SessionAssistantText - 43, // 22: compass.v1.SessionEvent.thinking:type_name -> compass.v1.SessionThinking - 44, // 23: compass.v1.SessionEvent.tool_call:type_name -> compass.v1.SessionToolCall - 45, // 24: compass.v1.SessionEvent.tool_call_update:type_name -> compass.v1.SessionToolCallUpdate - 47, // 25: compass.v1.SessionEvent.plan:type_name -> compass.v1.SessionPlan - 48, // 26: compass.v1.SessionEvent.notice:type_name -> compass.v1.SessionNotice - 49, // 27: compass.v1.SessionEvent.session_injection:type_name -> compass.v1.SessionInjection - 50, // 28: compass.v1.SessionEvent.session_error:type_name -> compass.v1.SessionError - 6, // 29: compass.v1.SessionToolCall.status:type_name -> compass.v1.AgentToolCallStatus - 6, // 30: compass.v1.SessionToolCallUpdate.status:type_name -> compass.v1.AgentToolCallStatus - 46, // 31: compass.v1.SessionToolCallUpdate.diffs:type_name -> compass.v1.SessionFileDiff - 40, // 32: compass.v1.SessionPlan.entries:type_name -> compass.v1.AgentPlanEntry - 8, // 33: compass.v1.SessionInjection.op_kind:type_name -> compass.v1.SessionInjectionKind - 9, // 34: compass.v1.SessionError.kind:type_name -> compass.v1.SessionErrorKind - 41, // 35: compass.v1.AgentSessionFrame.event:type_name -> compass.v1.SessionEvent - 5, // 36: compass.v1.AgentSessionFrame.state:type_name -> compass.v1.AgentSessionState - 36, // 37: compass.v1.GetAgentStatusResponse.statuses:type_name -> compass.v1.AgentSessionStatus - 77, // 38: compass.v1.ModelRegistryEntry.candidates:type_name -> compass.v1.ModelCandidate - 78, // 39: compass.v1.ModelRegistryEntry.metadata:type_name -> compass.v1.ModelMetadata - 98, // 40: compass.v1.ModelRegistry.entries:type_name -> compass.v1.ModelRegistry.EntriesEntry - 80, // 41: compass.v1.PutModelRegistryRequest.registry:type_name -> compass.v1.ModelRegistry - 80, // 42: compass.v1.GetModelRegistryResponse.registry:type_name -> compass.v1.ModelRegistry - 11, // 43: compass.v1.ForgeRef.provider:type_name -> compass.v1.ForgeProvider - 88, // 44: compass.v1.Issue.forge:type_name -> compass.v1.ForgeRef - 87, // 45: compass.v1.Issue.agent:type_name -> compass.v1.AgentAttribution - 99, // 46: compass.v1.Issue.updated_at:type_name -> google.protobuf.Timestamp - 10, // 47: compass.v1.Issue.state:type_name -> compass.v1.IssueState - 90, // 48: compass.v1.Issue.prs:type_name -> compass.v1.PullRequest - 94, // 49: compass.v1.Issue.tracker:type_name -> compass.v1.TrackerRef - 88, // 50: compass.v1.PullRequest.forge:type_name -> compass.v1.ForgeRef - 87, // 51: compass.v1.PullRequest.agent:type_name -> compass.v1.AgentAttribution - 93, // 52: compass.v1.PullRequest.changed:type_name -> compass.v1.ChangedStats - 91, // 53: compass.v1.PullRequest.checks:type_name -> compass.v1.ChecksSummary - 95, // 54: compass.v1.PullRequest.reviews:type_name -> compass.v1.Review - 96, // 55: compass.v1.PullRequest.threads:type_name -> compass.v1.ReviewThread - 92, // 56: compass.v1.ChecksSummary.checks:type_name -> compass.v1.Check - 97, // 57: compass.v1.ReviewThread.comments:type_name -> compass.v1.Comment - 79, // 58: compass.v1.ModelRegistry.EntriesEntry.value:type_name -> compass.v1.ModelRegistryEntry - 26, // 59: compass.v1.CompassService.GetServerInfo:input_type -> compass.v1.GetServerInfoRequest - 28, // 60: compass.v1.CompassService.WhoAmI:input_type -> compass.v1.WhoAmIRequest - 30, // 61: compass.v1.CompassService.SubscribeEvents:input_type -> compass.v1.SubscribeEventsRequest - 32, // 62: compass.v1.CompassService.ListBoardIssues:input_type -> compass.v1.ListBoardIssuesRequest - 53, // 63: compass.v1.CompassService.ProvisionAgentWorkspace:input_type -> compass.v1.ProvisionAgentWorkspaceRequest - 57, // 64: compass.v1.CompassService.StartAgentSession:input_type -> compass.v1.StartAgentSessionRequest - 59, // 65: compass.v1.CompassService.SpawnAgent:input_type -> compass.v1.SpawnAgentRequest - 61, // 66: compass.v1.CompassService.StopAgentSession:input_type -> compass.v1.StopAgentSessionRequest - 55, // 67: compass.v1.CompassService.RemoveAgentWorkspace:input_type -> compass.v1.RemoveAgentWorkspaceRequest - 63, // 68: compass.v1.CompassService.ReloadAgentSession:input_type -> compass.v1.ReloadAgentSessionRequest - 65, // 69: compass.v1.CompassService.GetAgentStatus:input_type -> compass.v1.GetAgentStatusRequest - 51, // 70: compass.v1.CompassService.SubscribeAgentSession:input_type -> compass.v1.SubscribeAgentSessionRequest - 67, // 71: compass.v1.CompassService.IssueToken:input_type -> compass.v1.IssueTokenRequest - 69, // 72: compass.v1.CompassService.RevokeToken:input_type -> compass.v1.RevokeTokenRequest - 71, // 73: compass.v1.CompassService.PutAgentConfig:input_type -> compass.v1.PutAgentConfigRequest - 73, // 74: compass.v1.CompassService.GetAgentConfigInfo:input_type -> compass.v1.GetAgentConfigInfoRequest - 75, // 75: compass.v1.CompassService.DeleteAgentConfig:input_type -> compass.v1.DeleteAgentConfigRequest - 81, // 76: compass.v1.CompassService.PutModelRegistry:input_type -> compass.v1.PutModelRegistryRequest - 83, // 77: compass.v1.CompassService.GetModelRegistry:input_type -> compass.v1.GetModelRegistryRequest - 85, // 78: compass.v1.CompassService.DeleteModelRegistry:input_type -> compass.v1.DeleteModelRegistryRequest - 12, // 79: compass.v1.SecretsService.SetSecret:input_type -> compass.v1.SetSecretRequest - 14, // 80: compass.v1.SecretsService.ListSecrets:input_type -> compass.v1.ListSecretsRequest - 17, // 81: compass.v1.SecretsService.DeleteSecret:input_type -> compass.v1.DeleteSecretRequest - 19, // 82: compass.v1.SecretsService.SetServerSecret:input_type -> compass.v1.SetServerSecretRequest - 21, // 83: compass.v1.SecretsService.DeleteServerSecret:input_type -> compass.v1.DeleteServerSecretRequest - 23, // 84: compass.v1.SecretsService.ListServerSecrets:input_type -> compass.v1.ListServerSecretsRequest - 27, // 85: compass.v1.CompassService.GetServerInfo:output_type -> compass.v1.GetServerInfoResponse - 29, // 86: compass.v1.CompassService.WhoAmI:output_type -> compass.v1.WhoAmIResponse - 31, // 87: compass.v1.CompassService.SubscribeEvents:output_type -> compass.v1.SubscribeEventsResponse - 33, // 88: compass.v1.CompassService.ListBoardIssues:output_type -> compass.v1.ListBoardIssuesResponse - 54, // 89: compass.v1.CompassService.ProvisionAgentWorkspace:output_type -> compass.v1.ProvisionAgentWorkspaceResponse - 58, // 90: compass.v1.CompassService.StartAgentSession:output_type -> compass.v1.StartAgentSessionResponse - 60, // 91: compass.v1.CompassService.SpawnAgent:output_type -> compass.v1.SpawnAgentResponse - 62, // 92: compass.v1.CompassService.StopAgentSession:output_type -> compass.v1.StopAgentSessionResponse - 56, // 93: compass.v1.CompassService.RemoveAgentWorkspace:output_type -> compass.v1.RemoveAgentWorkspaceResponse - 64, // 94: compass.v1.CompassService.ReloadAgentSession:output_type -> compass.v1.ReloadAgentSessionResponse - 66, // 95: compass.v1.CompassService.GetAgentStatus:output_type -> compass.v1.GetAgentStatusResponse - 52, // 96: compass.v1.CompassService.SubscribeAgentSession:output_type -> compass.v1.AgentSessionFrame - 68, // 97: compass.v1.CompassService.IssueToken:output_type -> compass.v1.IssueTokenResponse - 70, // 98: compass.v1.CompassService.RevokeToken:output_type -> compass.v1.RevokeTokenResponse - 72, // 99: compass.v1.CompassService.PutAgentConfig:output_type -> compass.v1.PutAgentConfigResponse - 74, // 100: compass.v1.CompassService.GetAgentConfigInfo:output_type -> compass.v1.GetAgentConfigInfoResponse - 76, // 101: compass.v1.CompassService.DeleteAgentConfig:output_type -> compass.v1.DeleteAgentConfigResponse - 82, // 102: compass.v1.CompassService.PutModelRegistry:output_type -> compass.v1.PutModelRegistryResponse - 84, // 103: compass.v1.CompassService.GetModelRegistry:output_type -> compass.v1.GetModelRegistryResponse - 86, // 104: compass.v1.CompassService.DeleteModelRegistry:output_type -> compass.v1.DeleteModelRegistryResponse - 13, // 105: compass.v1.SecretsService.SetSecret:output_type -> compass.v1.SetSecretResponse - 15, // 106: compass.v1.SecretsService.ListSecrets:output_type -> compass.v1.ListSecretsResponse - 18, // 107: compass.v1.SecretsService.DeleteSecret:output_type -> compass.v1.DeleteSecretResponse - 20, // 108: compass.v1.SecretsService.SetServerSecret:output_type -> compass.v1.SetServerSecretResponse - 22, // 109: compass.v1.SecretsService.DeleteServerSecret:output_type -> compass.v1.DeleteServerSecretResponse - 24, // 110: compass.v1.SecretsService.ListServerSecrets:output_type -> compass.v1.ListServerSecretsResponse - 85, // [85:111] is the sub-list for method output_type - 59, // [59:85] is the sub-list for method input_type - 59, // [59:59] is the sub-list for extension type_name - 59, // [59:59] is the sub-list for extension extendee - 0, // [0:59] is the sub-list for field type_name + 0, // 0: compass.v1.SetSecretRequest.delivery:type_name -> compass.v1.SecretDelivery + 1, // 1: compass.v1.SetSecretRequest.kind:type_name -> compass.v1.SecretKind + 2, // 2: compass.v1.SetSecretRequest.scope:type_name -> compass.v1.SecretScope + 17, // 3: compass.v1.ListSecretsResponse.secrets:type_name -> compass.v1.SecretStatus + 0, // 4: compass.v1.SecretStatus.delivery:type_name -> compass.v1.SecretDelivery + 1, // 5: compass.v1.SecretStatus.kind:type_name -> compass.v1.SecretKind + 2, // 6: compass.v1.DeleteSecretRequest.scope:type_name -> compass.v1.SecretScope + 26, // 7: compass.v1.ListServerSecretsResponse.server_secrets:type_name -> compass.v1.ServerSecretStatus + 35, // 8: compass.v1.SubscribeEventsResponse.server_status:type_name -> compass.v1.ServerStatus + 36, // 9: compass.v1.SubscribeEventsResponse.resync_required:type_name -> compass.v1.ResyncRequired + 37, // 10: compass.v1.SubscribeEventsResponse.agent_session_status:type_name -> compass.v1.AgentSessionStatus + 38, // 11: compass.v1.SubscribeEventsResponse.agent_message_chunk:type_name -> compass.v1.AgentMessageChunk + 39, // 12: compass.v1.SubscribeEventsResponse.agent_tool_call:type_name -> compass.v1.AgentToolCall + 40, // 13: compass.v1.SubscribeEventsResponse.agent_plan:type_name -> compass.v1.AgentPlan + 90, // 14: compass.v1.SubscribeEventsResponse.issue:type_name -> compass.v1.Issue + 90, // 15: compass.v1.ListBoardIssuesResponse.issues:type_name -> compass.v1.Issue + 3, // 16: compass.v1.ServerStatus.state:type_name -> compass.v1.ServerState + 6, // 17: compass.v1.AgentSessionStatus.state:type_name -> compass.v1.AgentSessionState + 4, // 18: compass.v1.AgentSessionStatus.runtime_tier:type_name -> compass.v1.RuntimeTier + 5, // 19: compass.v1.AgentSessionStatus.egress_posture:type_name -> compass.v1.EgressPosture + 7, // 20: compass.v1.AgentToolCall.status:type_name -> compass.v1.AgentToolCallStatus + 41, // 21: compass.v1.AgentPlan.entries:type_name -> compass.v1.AgentPlanEntry + 8, // 22: compass.v1.AgentPlanEntry.status:type_name -> compass.v1.AgentPlanEntryStatus + 43, // 23: compass.v1.SessionEvent.assistant_text:type_name -> compass.v1.SessionAssistantText + 44, // 24: compass.v1.SessionEvent.thinking:type_name -> compass.v1.SessionThinking + 45, // 25: compass.v1.SessionEvent.tool_call:type_name -> compass.v1.SessionToolCall + 46, // 26: compass.v1.SessionEvent.tool_call_update:type_name -> compass.v1.SessionToolCallUpdate + 48, // 27: compass.v1.SessionEvent.plan:type_name -> compass.v1.SessionPlan + 49, // 28: compass.v1.SessionEvent.notice:type_name -> compass.v1.SessionNotice + 50, // 29: compass.v1.SessionEvent.session_injection:type_name -> compass.v1.SessionInjection + 51, // 30: compass.v1.SessionEvent.session_error:type_name -> compass.v1.SessionError + 7, // 31: compass.v1.SessionToolCall.status:type_name -> compass.v1.AgentToolCallStatus + 7, // 32: compass.v1.SessionToolCallUpdate.status:type_name -> compass.v1.AgentToolCallStatus + 47, // 33: compass.v1.SessionToolCallUpdate.diffs:type_name -> compass.v1.SessionFileDiff + 41, // 34: compass.v1.SessionPlan.entries:type_name -> compass.v1.AgentPlanEntry + 9, // 35: compass.v1.SessionInjection.op_kind:type_name -> compass.v1.SessionInjectionKind + 10, // 36: compass.v1.SessionError.kind:type_name -> compass.v1.SessionErrorKind + 42, // 37: compass.v1.AgentSessionFrame.event:type_name -> compass.v1.SessionEvent + 6, // 38: compass.v1.AgentSessionFrame.state:type_name -> compass.v1.AgentSessionState + 37, // 39: compass.v1.GetAgentStatusResponse.statuses:type_name -> compass.v1.AgentSessionStatus + 78, // 40: compass.v1.ModelRegistryEntry.candidates:type_name -> compass.v1.ModelCandidate + 79, // 41: compass.v1.ModelRegistryEntry.metadata:type_name -> compass.v1.ModelMetadata + 99, // 42: compass.v1.ModelRegistry.entries:type_name -> compass.v1.ModelRegistry.EntriesEntry + 81, // 43: compass.v1.PutModelRegistryRequest.registry:type_name -> compass.v1.ModelRegistry + 81, // 44: compass.v1.GetModelRegistryResponse.registry:type_name -> compass.v1.ModelRegistry + 12, // 45: compass.v1.ForgeRef.provider:type_name -> compass.v1.ForgeProvider + 89, // 46: compass.v1.Issue.forge:type_name -> compass.v1.ForgeRef + 88, // 47: compass.v1.Issue.agent:type_name -> compass.v1.AgentAttribution + 100, // 48: compass.v1.Issue.updated_at:type_name -> google.protobuf.Timestamp + 11, // 49: compass.v1.Issue.state:type_name -> compass.v1.IssueState + 91, // 50: compass.v1.Issue.prs:type_name -> compass.v1.PullRequest + 95, // 51: compass.v1.Issue.tracker:type_name -> compass.v1.TrackerRef + 89, // 52: compass.v1.PullRequest.forge:type_name -> compass.v1.ForgeRef + 88, // 53: compass.v1.PullRequest.agent:type_name -> compass.v1.AgentAttribution + 94, // 54: compass.v1.PullRequest.changed:type_name -> compass.v1.ChangedStats + 92, // 55: compass.v1.PullRequest.checks:type_name -> compass.v1.ChecksSummary + 96, // 56: compass.v1.PullRequest.reviews:type_name -> compass.v1.Review + 97, // 57: compass.v1.PullRequest.threads:type_name -> compass.v1.ReviewThread + 93, // 58: compass.v1.ChecksSummary.checks:type_name -> compass.v1.Check + 98, // 59: compass.v1.ReviewThread.comments:type_name -> compass.v1.Comment + 80, // 60: compass.v1.ModelRegistry.EntriesEntry.value:type_name -> compass.v1.ModelRegistryEntry + 27, // 61: compass.v1.CompassService.GetServerInfo:input_type -> compass.v1.GetServerInfoRequest + 29, // 62: compass.v1.CompassService.WhoAmI:input_type -> compass.v1.WhoAmIRequest + 31, // 63: compass.v1.CompassService.SubscribeEvents:input_type -> compass.v1.SubscribeEventsRequest + 33, // 64: compass.v1.CompassService.ListBoardIssues:input_type -> compass.v1.ListBoardIssuesRequest + 54, // 65: compass.v1.CompassService.ProvisionAgentWorkspace:input_type -> compass.v1.ProvisionAgentWorkspaceRequest + 58, // 66: compass.v1.CompassService.StartAgentSession:input_type -> compass.v1.StartAgentSessionRequest + 60, // 67: compass.v1.CompassService.SpawnAgent:input_type -> compass.v1.SpawnAgentRequest + 62, // 68: compass.v1.CompassService.StopAgentSession:input_type -> compass.v1.StopAgentSessionRequest + 56, // 69: compass.v1.CompassService.RemoveAgentWorkspace:input_type -> compass.v1.RemoveAgentWorkspaceRequest + 64, // 70: compass.v1.CompassService.ReloadAgentSession:input_type -> compass.v1.ReloadAgentSessionRequest + 66, // 71: compass.v1.CompassService.GetAgentStatus:input_type -> compass.v1.GetAgentStatusRequest + 52, // 72: compass.v1.CompassService.SubscribeAgentSession:input_type -> compass.v1.SubscribeAgentSessionRequest + 68, // 73: compass.v1.CompassService.IssueToken:input_type -> compass.v1.IssueTokenRequest + 70, // 74: compass.v1.CompassService.RevokeToken:input_type -> compass.v1.RevokeTokenRequest + 72, // 75: compass.v1.CompassService.PutAgentConfig:input_type -> compass.v1.PutAgentConfigRequest + 74, // 76: compass.v1.CompassService.GetAgentConfigInfo:input_type -> compass.v1.GetAgentConfigInfoRequest + 76, // 77: compass.v1.CompassService.DeleteAgentConfig:input_type -> compass.v1.DeleteAgentConfigRequest + 82, // 78: compass.v1.CompassService.PutModelRegistry:input_type -> compass.v1.PutModelRegistryRequest + 84, // 79: compass.v1.CompassService.GetModelRegistry:input_type -> compass.v1.GetModelRegistryRequest + 86, // 80: compass.v1.CompassService.DeleteModelRegistry:input_type -> compass.v1.DeleteModelRegistryRequest + 13, // 81: compass.v1.SecretsService.SetSecret:input_type -> compass.v1.SetSecretRequest + 15, // 82: compass.v1.SecretsService.ListSecrets:input_type -> compass.v1.ListSecretsRequest + 18, // 83: compass.v1.SecretsService.DeleteSecret:input_type -> compass.v1.DeleteSecretRequest + 20, // 84: compass.v1.SecretsService.SetServerSecret:input_type -> compass.v1.SetServerSecretRequest + 22, // 85: compass.v1.SecretsService.DeleteServerSecret:input_type -> compass.v1.DeleteServerSecretRequest + 24, // 86: compass.v1.SecretsService.ListServerSecrets:input_type -> compass.v1.ListServerSecretsRequest + 28, // 87: compass.v1.CompassService.GetServerInfo:output_type -> compass.v1.GetServerInfoResponse + 30, // 88: compass.v1.CompassService.WhoAmI:output_type -> compass.v1.WhoAmIResponse + 32, // 89: compass.v1.CompassService.SubscribeEvents:output_type -> compass.v1.SubscribeEventsResponse + 34, // 90: compass.v1.CompassService.ListBoardIssues:output_type -> compass.v1.ListBoardIssuesResponse + 55, // 91: compass.v1.CompassService.ProvisionAgentWorkspace:output_type -> compass.v1.ProvisionAgentWorkspaceResponse + 59, // 92: compass.v1.CompassService.StartAgentSession:output_type -> compass.v1.StartAgentSessionResponse + 61, // 93: compass.v1.CompassService.SpawnAgent:output_type -> compass.v1.SpawnAgentResponse + 63, // 94: compass.v1.CompassService.StopAgentSession:output_type -> compass.v1.StopAgentSessionResponse + 57, // 95: compass.v1.CompassService.RemoveAgentWorkspace:output_type -> compass.v1.RemoveAgentWorkspaceResponse + 65, // 96: compass.v1.CompassService.ReloadAgentSession:output_type -> compass.v1.ReloadAgentSessionResponse + 67, // 97: compass.v1.CompassService.GetAgentStatus:output_type -> compass.v1.GetAgentStatusResponse + 53, // 98: compass.v1.CompassService.SubscribeAgentSession:output_type -> compass.v1.AgentSessionFrame + 69, // 99: compass.v1.CompassService.IssueToken:output_type -> compass.v1.IssueTokenResponse + 71, // 100: compass.v1.CompassService.RevokeToken:output_type -> compass.v1.RevokeTokenResponse + 73, // 101: compass.v1.CompassService.PutAgentConfig:output_type -> compass.v1.PutAgentConfigResponse + 75, // 102: compass.v1.CompassService.GetAgentConfigInfo:output_type -> compass.v1.GetAgentConfigInfoResponse + 77, // 103: compass.v1.CompassService.DeleteAgentConfig:output_type -> compass.v1.DeleteAgentConfigResponse + 83, // 104: compass.v1.CompassService.PutModelRegistry:output_type -> compass.v1.PutModelRegistryResponse + 85, // 105: compass.v1.CompassService.GetModelRegistry:output_type -> compass.v1.GetModelRegistryResponse + 87, // 106: compass.v1.CompassService.DeleteModelRegistry:output_type -> compass.v1.DeleteModelRegistryResponse + 14, // 107: compass.v1.SecretsService.SetSecret:output_type -> compass.v1.SetSecretResponse + 16, // 108: compass.v1.SecretsService.ListSecrets:output_type -> compass.v1.ListSecretsResponse + 19, // 109: compass.v1.SecretsService.DeleteSecret:output_type -> compass.v1.DeleteSecretResponse + 21, // 110: compass.v1.SecretsService.SetServerSecret:output_type -> compass.v1.SetServerSecretResponse + 23, // 111: compass.v1.SecretsService.DeleteServerSecret:output_type -> compass.v1.DeleteServerSecretResponse + 25, // 112: compass.v1.SecretsService.ListServerSecrets:output_type -> compass.v1.ListServerSecretsResponse + 87, // [87:113] is the sub-list for method output_type + 61, // [61:87] is the sub-list for method input_type + 61, // [61:61] is the sub-list for extension type_name + 61, // [61:61] is the sub-list for extension extendee + 0, // [0:61] is the sub-list for field type_name } func init() { file_compass_v1_compass_proto_init() } @@ -6637,7 +6717,7 @@ func file_compass_v1_compass_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_compass_v1_compass_proto_rawDesc), len(file_compass_v1_compass_proto_rawDesc)), - NumEnums: 12, + NumEnums: 13, NumMessages: 87, NumExtensions: 0, NumServices: 2, diff --git a/go/internal/runnerhub/commands_test.go b/go/internal/runnerhub/commands_test.go index fc90ae9ad..2fd84a48b 100644 --- a/go/internal/runnerhub/commands_test.go +++ b/go/internal/runnerhub/commands_test.go @@ -216,16 +216,16 @@ func TestRemoveRelayReturnsResponseOnSuccess(t *testing.T) { // Remove clears the container's provisioned account binding — the teardown // counterpart to Provision's bindContainer. On a Provision->Remove path that // never reached Start (promoteSession clears it there), a lingering binding would -// keep authorizing a pre-exec FetchSecrets materialize (HasContainerBinding) for +// keep authorizing a pre-exec FetchSecrets materialize (AccountForContainer) for // a container that no longer exists. // -// Mutation: dropping the unbindContainer call in Remove leaves HasContainerBinding +// Mutation: dropping the unbindContainer call in Remove leaves AccountForContainer // true after teardown and reddens this. func TestRemoveClearsContainerBinding(t *testing.T) { hub := newHubOnly() hub.enroll(context.Background(), "runner-1", store.Subject{Kind: store.SubjectRunner, ID: "runner-1"}, compassv1.RuntimeTier_RUNTIME_TIER_UNSPECIFIED, compassv1.EgressPosture_EGRESS_POSTURE_UNSPECIFIED) hub.bindContainer("c1", testAgentAccount) - if !hub.HasContainerBinding("c1") { + if _, ok := hub.AccountForContainer("c1"); !ok { t.Fatal("precondition: container c1 should be bound after bindContainer") } router, _, _ := hub.routerFor("any") @@ -240,7 +240,7 @@ func TestRemoveClearsContainerBinding(t *testing.T) { if _, err := hub.Remove(context.Background(), "req-rm", &compassv1.RemoveAgentWorkspaceRequest{ContainerName: "c1"}); err != nil { t.Fatalf("Remove = %v, want success", err) } - if hub.HasContainerBinding("c1") { + if _, ok := hub.AccountForContainer("c1"); ok { t.Fatal("container c1 still bound after Remove, want the binding cleared (stale binding authorizes pre-exec secrets materialize)") } } diff --git a/go/internal/runnerhub/handler.go b/go/internal/runnerhub/handler.go index 2c26ed405..7669d54c3 100644 --- a/go/internal/runnerhub/handler.go +++ b/go/internal/runnerhub/handler.go @@ -37,15 +37,28 @@ type AgentConfigStore interface { CurrentAgentConfig(ctx context.Context) (version string, bundle []byte, err error) } +// secretResolver is the Server-side per-agent secret resolve surface FetchSecrets +// delegates to — the A9 scoped read. Narrow by design, the AgentConfigStore +// pattern: the handler depends on this one method, not the whole resolver, so the +// concrete *secrets.StoreResolver satisfies it and a server built with no secrets +// surface passes nil. It replaces the old value-free secrets.Resolver the handler +// carried: FetchSecrets no longer injects the whole declared set, it resolves the +// most-specific row per name for one agent account. +type secretResolver interface { + // ResolveFor resolves the one most-specific row per name visible to agent + // (A9: agent > user > tenant), decrypted. reason is audit context. + ResolveFor(ctx context.Context, agent store.AccountID, reason string) ([]secrets.ResolvedSecret, error) +} + // Handler implements compassv1internalconnect.RunnerServiceHandler over the hub. // The hub owns the registry, router, and Deliver seam; the resolver is the -// Server-side secret resolve surface FetchSecrets delegates to, and configStore -// the fleet config-bundle surface FetchAgentConfig delegates to. The handler is -// the wire-termination shell that drives them. +// Server-side per-agent secret resolve surface FetchSecrets delegates to, and +// configStore the fleet config-bundle surface FetchAgentConfig delegates to. The +// handler is the wire-termination shell that drives them. type Handler struct { compassv1internalconnect.UnimplementedRunnerServiceHandler hub *Hub - resolver secrets.Resolver + resolver secretResolver configStore AgentConfigStore } @@ -57,7 +70,7 @@ type Handler struct { // CodeUnavailable connect-go synthesizes for transport faults, so the Runner can // tolerate a genuine no-surface server without also tolerating a transient // outage. -func NewHandler(hub *Hub, resolver secrets.Resolver, configStore AgentConfigStore) *Handler { +func NewHandler(hub *Hub, resolver secretResolver, configStore AgentConfigStore) *Handler { return &Handler{hub: hub, resolver: resolver, configStore: configStore} } @@ -256,20 +269,22 @@ func (h *Handler) CommitConversationFrame(ctx context.Context, req *connect.Requ return connect.NewResponse(resp), nil } -// FetchSecrets resolves the whole declared secret set for a live session and -// returns it to the Runner. Auth is already at the door (the Runner-subject -// bearer interceptor Kind-gates every RunnerService RPC — an account token is -// Unauthenticated here, the OQ7 cross-door rule); the runnerSubjectFrom check is -// defense in depth, mirroring the other handlers. +// FetchSecrets resolves the secret set for a live session (or a provisioned +// container) and returns it to the Runner. Auth is already at the door (the +// Runner-subject bearer interceptor Kind-gates every RunnerService RPC — an +// account token is Unauthenticated here, the OQ7 cross-door rule); the +// runnerSubjectFrom check is defense in depth, mirroring the other handlers. // // Binding authz (record §756-762): the request selects the binding to authorize -// against. A session_id must be a live session bound to this Runner (rotation -// re-fetch); a container_name must have a recorded container→account binding -// (the PROVISION-time initial materialize, before any session exists). Under -// inject-all + single-Runner, "bound to this Runner" == "present in the hub" -// (there is exactly one Runner); HasLiveSession / HasContainerBinding are those -// checks. A foreign/unknown selector is rejected CodePermissionDenied; a missing -// selector is CodeInvalidArgument. +// against, and that binding RESOLVES the agent account the read is scoped to (A9). +// A session_id must be a live session bound to this Runner (rotation re-fetch); a +// container_name must have a recorded container→account binding (the PROVISION- +// time initial materialize, before any session exists). Under inject-all + +// single-Runner, "bound to this Runner" == "present in the hub" (there is exactly +// one Runner); AccountForLiveSession / AccountForContainer are those checks AND +// yield the account. The agent identity comes from the hub binding, NEVER a +// request field. A foreign/unknown selector is rejected CodePermissionDenied; a +// missing selector is CodeInvalidArgument. // // NO-LOG posture (record §770-772): the response carries live secret values // (ResolvedSecret.value/.version are [debug_redact] on the wire). This handler @@ -281,27 +296,32 @@ func (h *Handler) FetchSecrets(ctx context.Context, req *connect.Request[compass if h.resolver == nil { return nil, connect.NewError(connect.CodeFailedPrecondition, errNoResolver) } - // Authorize against whichever binding the selector names, then resolve the - // same inject-all set for either (no per-agent differentiation in the MVP). - // A container_name authorizes the PROVISION-time initial materialize (bound - // from Provision, before any session); a session_id authorizes the - // post-Start rotation re-fetch. A foreign/unknown selector — or none — is - // rejected CodePermissionDenied, never a silent empty set. + // Authorize against whichever binding the selector names AND take the agent + // account it resolves to — the read is scoped to that agent (A9). A + // container_name authorizes the PROVISION-time initial materialize (bound from + // Provision, before any session); a session_id authorizes the post-Start + // rotation re-fetch. A foreign/unknown selector — or none — is rejected + // CodePermissionDenied, never a silent empty set. + var agent store.AccountID switch sessionID, containerName := req.Msg.GetSessionId(), req.Msg.GetContainerName(); { case sessionID != "" && containerName != "": return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("FetchSecrets accepts a session_id or a container_name, not both")) case sessionID != "": - if !h.hub.HasLiveSession(sessionID) { + account, ok := h.hub.AccountForLiveSession(sessionID) + if !ok { return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("session %q is not a live session bound to this runner", sessionID)) } + agent = account case containerName != "": - if !h.hub.HasContainerBinding(containerName) { + account, ok := h.hub.AccountForContainer(containerName) + if !ok { return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("container %q has no provisioned binding on this runner", containerName)) } + agent = account default: return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("FetchSecrets requires a session_id or container_name selector")) } - resolved, err := h.resolver.Resolve(ctx, "runner fetch") + resolved, err := h.resolver.ResolveFor(ctx, agent, "runner fetch") if err != nil { return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("resolving secrets: %w", err)) } @@ -447,7 +467,7 @@ func kindToProto(k secrets.SecretKind) compassv1.SecretKind { func NewMountedHandler( hub *Hub, resolve TokenResolver, - resolver secrets.Resolver, + resolver secretResolver, configStore AgentConfigStore, otelIC *otelconnect.Interceptor, ) (string, http.Handler) { diff --git a/go/internal/runnerhub/helpers_test.go b/go/internal/runnerhub/helpers_test.go index 6e36fb6e5..7b44c2f7d 100644 --- a/go/internal/runnerhub/helpers_test.go +++ b/go/internal/runnerhub/helpers_test.go @@ -28,7 +28,6 @@ import ( compassv1 "github.com/RigelBuild/compass/go/gen/compass/v1" compassv1internal "github.com/RigelBuild/compass/go/internal/gen/compass/v1" "github.com/RigelBuild/compass/go/internal/gen/compass/v1/compassv1internalconnect" - "github.com/RigelBuild/compass/go/internal/secrets" "github.com/RigelBuild/compass/go/internal/store" ) @@ -581,7 +580,7 @@ func newMountedH2CServer(t *testing.T, hub *Hub, resolve TokenResolver) string { // newMountedH2CServerWithResolver is newMountedH2CServer with a secret resolver // threaded into the handler, so a FetchSecrets test drives the resolve path over // the real wire. -func newMountedH2CServerWithResolver(t *testing.T, hub *Hub, resolve TokenResolver, resolver secrets.Resolver) string { +func newMountedH2CServerWithResolver(t *testing.T, hub *Hub, resolve TokenResolver, resolver secretResolver) string { t.Helper() return newMountedH2CServerWith(t, hub, resolve, resolver, nil) } @@ -598,7 +597,7 @@ func newMountedH2CServerWithConfig(t *testing.T, hub *Hub, resolve TokenResolver // may be nil) on an httptest h2c server and returns its base URL. The // otelconnect interceptor is real (NewMountedHandler forbids nil) but inert: // these tests install no tracer provider, so it reads the no-op global. -func newMountedH2CServerWith(t *testing.T, hub *Hub, resolve TokenResolver, resolver secrets.Resolver, configStore AgentConfigStore) string { +func newMountedH2CServerWith(t *testing.T, hub *Hub, resolve TokenResolver, resolver secretResolver, configStore AgentConfigStore) string { t.Helper() otelIC, err := otelconnect.NewInterceptor() if err != nil { diff --git a/go/internal/runnerhub/relay_comms.go b/go/internal/runnerhub/relay_comms.go index 19d8bfdba..d8fb970ec 100644 --- a/go/internal/runnerhub/relay_comms.go +++ b/go/internal/runnerhub/relay_comms.go @@ -441,36 +441,36 @@ func (h *Hub) OnBindingChange(change fabric.BindingChange) { } } -// HasLiveSession reports whether sessionID names a live session bound in the -// hub. It mirrors accountForSession's lock discipline but discards the account — -// the FetchSecrets authz check only needs "is this a session bound to the (one) -// enrolled Runner", not whose session it is. Under the inject-all + single-Runner -// MVP, a live binding in the hub IS a session bound to this Runner (there is -// exactly one), so this is the whole session-binding authz. The per-Runner -// differentiation — verifying the session belongs to THIS Runner among several — -// is the future multi-Runner seam (record §761-762); today there is one Runner, -// so membership in sessionAccounts is that check. -func (h *Hub) HasLiveSession(sessionID string) bool { +// AccountForLiveSession returns the agent account bound to sessionID in the hub, +// with false when no live binding exists. It mirrors accountForSession's lock +// discipline but skips the durable read-through: FetchSecrets authorizes a +// re-fetch for a session the hub currently holds, and the returned account is +// the identity the A9 scoped resolve reads. Under the inject-all + single-Runner +// MVP a live binding in the hub IS a session bound to this Runner (there is +// exactly one), so membership in sessionAccounts is the whole session-binding +// authz; the per-Runner differentiation is the future multi-Runner seam +// (record §761-762). +func (h *Hub) AccountForLiveSession(sessionID string) (store.AccountID, bool) { h.mu.Lock() defer h.mu.Unlock() - _, ok := h.sessionAccounts[sessionID] - return ok + account, ok := h.sessionAccounts[sessionID] + return account, ok } -// HasContainerBinding reports whether containerName has a recorded -// container→account binding — the Provision..Start window binding (bindContainer, -// cleared by promoteSession at Start and by clear() on re-enroll). It is the -// PROVISION-time analogue of HasLiveSession: FetchSecrets authorizes an initial -// pre-exec materialize against it, because no live session exists until Start. -// Under the inject-all + single-Runner MVP a recorded binding IS a container -// provisioned on the one enrolled Runner, so membership is the whole authz check -// (the per-Runner differentiation is the same future multi-Runner seam, -// record §761-762). -func (h *Hub) HasContainerBinding(containerName string) bool { +// AccountForContainer returns the agent account bound to containerName in the +// Provision..Start window (bindContainer, cleared by promoteSession at Start and +// by clear() on re-enroll), with false when none is recorded. It is the +// PROVISION-time analogue of AccountForLiveSession: FetchSecrets authorizes an +// initial pre-exec materialize against it (no live session exists until Start) +// and reads the returned account for the A9 scoped resolve. Under the inject-all +// + single-Runner MVP a recorded binding IS a container provisioned on the one +// enrolled Runner, so membership is the whole authz check (the per-Runner +// differentiation is the same future multi-Runner seam, record §761-762). +func (h *Hub) AccountForContainer(containerName string) (store.AccountID, bool) { h.mu.Lock() defer h.mu.Unlock() - _, ok := h.containerAccounts[containerName] - return ok + account, ok := h.containerAccounts[containerName] + return account, ok } // errCommsUnavailable is the fail-closed cause when a hub with no CommsCaller diff --git a/go/internal/runnerhub/secrets_test.go b/go/internal/runnerhub/secrets_test.go index 35e96512b..cf6d1143e 100644 --- a/go/internal/runnerhub/secrets_test.go +++ b/go/internal/runnerhub/secrets_test.go @@ -26,34 +26,28 @@ import ( "github.com/RigelBuild/compass/go/internal/store" ) -// fakeResolverSecrets is a hand-written secrets.Resolver: Resolve returns a fixed -// set (and records that it was called), Set/Delete are no-op successes. It lets -// the FetchSecrets seam test drive the resolve path without a real SecretSpec -// provider. resolveErr, when set, makes Resolve fail. +// fakeResolverSecrets is a hand-written secretResolver: ResolveFor returns a +// fixed set (and records that it was called AND for which agent), letting the +// FetchSecrets seam test drive the scoped resolve path without a real store or +// crypto. resolveErr, when set, makes ResolveFor fail. The fixed set ignores the +// agent arg — these tests exercise the handler's authz+binding→account→resolve +// wiring, not the A9 precedence (that is the store/StoreResolver pgtest's job). type fakeResolverSecrets struct { set []secrets.ResolvedSecret resolveErr error resolveCalls int + gotAgent store.AccountID } -func (f *fakeResolverSecrets) Resolve(_ context.Context, _ string) ([]secrets.ResolvedSecret, error) { +func (f *fakeResolverSecrets) ResolveFor(_ context.Context, agent store.AccountID, _ string) ([]secrets.ResolvedSecret, error) { f.resolveCalls++ + f.gotAgent = agent if f.resolveErr != nil { return nil, f.resolveErr } return f.set, nil } -func (f *fakeResolverSecrets) Set(_ context.Context, _, _, _ string) error { return nil } -func (f *fakeResolverSecrets) Delete(_ context.Context, _ string) error { return nil } - -// Statuses satisfies the Resolver interface. These tests exercise the container -// secrets-delivery seam, which never lists set/unset state, so it returns -// nothing rather than modelling a provider probe. -func (f *fakeResolverSecrets) Statuses(_ context.Context, _ string) ([]secrets.SecretStatus, error) { - return nil, nil -} - // runnerResolverForFetch is the token resolver the FetchSecrets door uses: it // accepts a single Runner token and rejects everything else, modelling the real // kind-gate contract the seam tests already rely on. diff --git a/go/internal/secrets/secrets.go b/go/internal/secrets/secrets.go index eb82695e4..b63061e92 100644 --- a/go/internal/secrets/secrets.go +++ b/go/internal/secrets/secrets.go @@ -63,7 +63,7 @@ const ( // nameGrammar is SecretSpec's env-var-name grammar. A declared secret name must // match it: it becomes both a manifest key and, downstream, a path segment // under $HOME/.compass/secrets/ and a token in a root-adjacent setup script -// (T5). Validated at the store door (store.DeclareSecret) and re-checked here as +// (T5). Validated at the store door (store.UpsertSecret) and re-checked here as // defense in depth before a name is ever emitted into a generated manifest. var nameGrammar = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) diff --git a/go/internal/store/db/querier.go b/go/internal/store/db/querier.go index 3c80e184b..57d80ec19 100644 --- a/go/internal/store/db/querier.go +++ b/go/internal/store/db/querier.go @@ -266,18 +266,6 @@ type Querier interface { // RETURNING) rather than clobbering the winner. The seeded version is 1. InsertModelRegistry(ctx context.Context, registry []byte) (int64, error) InsertOwnerDMGroup(ctx context.Context, arg InsertOwnerDMGroupParams) error - // Secrets-registry queries (sqlc adoption T6, RIG-3034). These back the - // hand-written Store methods, which keep their signatures, the door-side - // validation (name grammar, kind/routing, A9 scope shape), the - // ErrConflict/ErrInvalidArgument/ErrNotFound mapping, and the RowsAffected - // branch (DeleteSecretDeclaration is :execrows). - // - // InsertSecret/DeclaredSecrets are the retained value-free path (T5 caller); the - // scoped, encrypted path is UpsertSecret + SecretRecordsForAgent (A1/A9). - // InsertSecret writes the value-free declaration at the tenant coordinate - // (scope_kind 0, empty scope_id); the value columns stay NULL. Retained for the T5 - // SetSecret caller, removed with it in T5. - InsertSecret(ctx context.Context, arg InsertSecretParams) error InsertServerKeyState(ctx context.Context, arg InsertServerKeyStateParams) error // Server-secrets registry queries (design record T0, mechanism C1/D6). The // SERVER-owned half of the names-only secret registry, physically separate from @@ -307,6 +295,15 @@ type Querier interface { InsertUserAccount(ctx context.Context, arg InsertUserAccountParams) error IsAgentAccount(ctx context.Context, accountID string) (bool, error) IsEnabledForgeRepo(ctx context.Context, repo string) (bool, error) + // Secrets-registry queries (sqlc adoption T6, RIG-3034). These back the + // hand-written Store methods, which keep their signatures, the door-side + // validation (name grammar, kind/routing, A9 scope shape), the + // ErrConflict/ErrInvalidArgument/ErrNotFound mapping, and the RowsAffected + // branch (DeleteSecretDeclaration is :execrows). + // + // DeclaredSecrets is the retained value-free READ path (the SERVER SpecResolver's + // declarations view); the scoped, encrypted path is UpsertSecret + + // SecretRecordsForAgent (A1/A9), now the sole writer. // IsUserAccount reports whether an id names a human account — the user-scope // (scope_kind 1) referential check the UpsertSecret door runs in lieu of an FK // (A9). The agent-scope check reuses IsAgentAccount. diff --git a/go/internal/store/db/secrets.sql.go b/go/internal/store/db/secrets.sql.go index fe12441bf..ff66b8ddf 100644 --- a/go/internal/store/db/secrets.sql.go +++ b/go/internal/store/db/secrets.sql.go @@ -78,48 +78,20 @@ func (q *Queries) DeleteSecret(ctx context.Context, arg DeleteSecretParams) (int return result.RowsAffected(), nil } -const insertSecret = `-- name: InsertSecret :exec +const isUserAccount = `-- name: IsUserAccount :one -INSERT INTO secrets (name, scope_kind, delivery, kind, provider, host, declared_by) -VALUES ($1, 0, $2, $3, $4, $5, $6) +SELECT EXISTS (SELECT 1 FROM user_accounts WHERE account_id = $1) ` -type InsertSecretParams struct { - Name string - Delivery int16 - Kind int16 - Provider string - Host string - DeclaredBy string -} - // Secrets-registry queries (sqlc adoption T6, RIG-3034). These back the // hand-written Store methods, which keep their signatures, the door-side // validation (name grammar, kind/routing, A9 scope shape), the // ErrConflict/ErrInvalidArgument/ErrNotFound mapping, and the RowsAffected // branch (DeleteSecretDeclaration is :execrows). // -// InsertSecret/DeclaredSecrets are the retained value-free path (T5 caller); the -// scoped, encrypted path is UpsertSecret + SecretRecordsForAgent (A1/A9). -// InsertSecret writes the value-free declaration at the tenant coordinate -// (scope_kind 0, empty scope_id); the value columns stay NULL. Retained for the T5 -// SetSecret caller, removed with it in T5. -func (q *Queries) InsertSecret(ctx context.Context, arg InsertSecretParams) error { - _, err := q.db.Exec(ctx, insertSecret, - arg.Name, - arg.Delivery, - arg.Kind, - arg.Provider, - arg.Host, - arg.DeclaredBy, - ) - return err -} - -const isUserAccount = `-- name: IsUserAccount :one -SELECT EXISTS (SELECT 1 FROM user_accounts WHERE account_id = $1) -` - +// DeclaredSecrets is the retained value-free READ path (the SERVER SpecResolver's +// declarations view); the scoped, encrypted path is UpsertSecret + +// SecretRecordsForAgent (A1/A9), now the sole writer. // IsUserAccount reports whether an id names a human account — the user-scope // (scope_kind 1) referential check the UpsertSecret door runs in lieu of an FK // (A9). The agent-scope check reuses IsAgentAccount. diff --git a/go/internal/store/migrations/0001_init.sql b/go/internal/store/migrations/0001_init.sql index c73b20221..a34793e1c 100644 --- a/go/internal/store/migrations/0001_init.sql +++ b/go/internal/store/migrations/0001_init.sql @@ -412,11 +412,10 @@ CREATE TABLE secrets ( -- host: the forge host for a gh secret. Empty for non-gh kinds. host TEXT NOT NULL DEFAULT '', -- value_ciphertext/value_nonce: the AES-256-GCM ciphertext and its fresh - -- 96-bit nonce. NULLABLE in T2 only — the retained value-free - -- InsertSecret/DeclareSecret path writes no value through T5, which then - -- tightens both to NOT NULL once the upsert is the sole writer (A1). - value_ciphertext BYTEA, - value_nonce BYTEA, + -- 96-bit nonce. NOT NULL in the final schema (T5): the upsert is the sole + -- writer and always writes both, so a value-free row can no longer exist (A1). + value_ciphertext BYTEA NOT NULL, + value_nonce BYTEA NOT NULL, -- key_version: which master-key generation encrypted this row (A3, reserved -- for the deferred rotation record). key_version SMALLINT NOT NULL DEFAULT 1, @@ -433,7 +432,7 @@ CREATE TABLE secrets ( -- row (kind=1) carries a non-empty provider and no host; a gh row (kind=2) a -- non-empty host and no provider; a generic row (kind=0) neither. Without -- this a malformed row persists silently and misroutes at the T5 materializer; - -- the CHECK fails it at write time. DeclareSecret guards the same invariant so + -- the CHECK fails it at write time. UpsertSecret guards the same invariant so -- a caller gets ErrInvalidArgument, not a raw constraint violation. CONSTRAINT secrets_kind_routing CHECK ( (kind = 0 AND provider = '' AND host = '') diff --git a/go/internal/store/queries/secrets.sql b/go/internal/store/queries/secrets.sql index 81ff0a439..3f93c426c 100644 --- a/go/internal/store/queries/secrets.sql +++ b/go/internal/store/queries/secrets.sql @@ -4,15 +4,9 @@ -- ErrConflict/ErrInvalidArgument/ErrNotFound mapping, and the RowsAffected -- branch (DeleteSecretDeclaration is :execrows). -- --- InsertSecret/DeclaredSecrets are the retained value-free path (T5 caller); the --- scoped, encrypted path is UpsertSecret + SecretRecordsForAgent (A1/A9). - --- InsertSecret writes the value-free declaration at the tenant coordinate --- (scope_kind 0, empty scope_id); the value columns stay NULL. Retained for the T5 --- SetSecret caller, removed with it in T5. --- name: InsertSecret :exec -INSERT INTO secrets (name, scope_kind, delivery, kind, provider, host, declared_by) -VALUES ($1, 0, $2, $3, $4, $5, $6); +-- DeclaredSecrets is the retained value-free READ path (the SERVER SpecResolver's +-- declarations view); the scoped, encrypted path is UpsertSecret + +-- SecretRecordsForAgent (A1/A9), now the sole writer. -- IsUserAccount reports whether an id names a human account — the user-scope -- (scope_kind 1) referential check the UpsertSecret door runs in lieu of an FK diff --git a/go/internal/store/secrets.go b/go/internal/store/secrets.go index 78d34da6b..6c514b388 100644 --- a/go/internal/store/secrets.go +++ b/go/internal/store/secrets.go @@ -42,7 +42,7 @@ const ( ) // secretNamePattern is SecretSpec's env-var-name grammar. A declared name is -// validated against it at the store door (DeclareSecret) — before it can reach +// validated against it at the store door (UpsertSecret) — before it can reach // a row — because it later becomes a path segment under $HOME/.compass/secrets/ // and a line in a root-adjacent setup script (T5): constrained at the door, not // escaped downstream. The identical grammar is re-exported and re-checked by @@ -71,52 +71,6 @@ type SecretDeclaration struct { UpdatedAt time.Time } -// DeclareSecret adds a names-only registry row (RIG-1327 T3). It stores NO -// value — the value lives in the SecretSpec provider. name is validated against -// SecretSpec's env-var-name grammar at the door (a bad name is -// ErrInvalidArgument before touching Postgres, since the name becomes a -// filesystem path and script token downstream). A duplicate name is -// ErrConflict; an unknown actor account is ErrInvalidArgument (the declared_by -// FK). provider is meaningful only for a provider kind and host only for a gh -// kind; callers pass "" otherwise. -func (s *Store) DeclareSecret(ctx context.Context, actor AccountID, name string, delivery SecretDelivery, kind SecretKind, provider, host string) error { - if !secretNamePattern.MatchString(name) { - return fmt.Errorf("%w: secret name %q must match %s", ErrInvalidArgument, name, secretNamePattern.String()) - } - // F1 (design record D6): the user keyspace REJECTS reserved server-secret - // prefixes. Without this a user-path declare could mint a shadow `secrets` - // row under a server-secret name, which the inject-all delivery path then - // hands to every agent container. With it, the two doors partition the - // keyspace by name: a reserved-prefix name can only live in - // `server_secrets`, an unprefixed one only in `secrets`. - if HasServerSecretPrefix(name) { - return fmt.Errorf("%w: secret name %q uses a reserved server-secret prefix", ErrInvalidArgument, name) - } - if actor == "" { - return fmt.Errorf("%w: declaring account id is required", ErrInvalidArgument) - } - if err := validateKindRouting(kind, provider, host); err != nil { - return err - } - if err := s.q.InsertSecret(ctx, db.InsertSecretParams{ - Name: name, - Delivery: int16(delivery), //nolint:gosec // G115: SecretDelivery is a CHECK-constrained 0/1 enum (secrets.delivery), always within int16 - Kind: int16(kind), //nolint:gosec // G115: SecretKind is a CHECK-constrained 0/1/2 enum (secrets.kind), always within int16 - Provider: provider, - Host: host, - DeclaredBy: string(actor), - }); err != nil { - if pgErrIs(err, pgUniqueViolation) { - return fmt.Errorf("%w: secret %q already declared", ErrConflict, name) - } - if pgErrIs(err, pgForeignKeyViolation) { - return fmt.Errorf("%w: declaring account %q does not exist", ErrInvalidArgument, actor) - } - return fmt.Errorf("store: declare secret: %w", err) - } - return nil -} - // validateKindRouting enforces the kind↔provider/host invariant at the store // door, mirroring the secrets_kind_routing CHECK: a provider row (kind=1) // carries a non-empty provider and no host, a gh row (kind=2) a non-empty host diff --git a/go/internal/store/secrets_test.go b/go/internal/store/secrets_test.go index 4098ceb62..301a3b525 100644 --- a/go/internal/store/secrets_test.go +++ b/go/internal/store/secrets_test.go @@ -2,34 +2,47 @@ package store -// Secret names-registry contracts (RIG-1327 T3): the round-trip of a declared -// row with its delivery/kind/provider/host/actor intact and name-ordered, the -// UNIQUE conflict on a duplicate name, the door name-validation that rejects a -// bad name before any row is written, the declared_by FK on an unknown actor, -// and the delete path (found → gone, unknown → ErrNotFound). NEVER a value: -// the registry stores names only. +// Secret registry door contracts on the UpsertSecret write path (T5 cutover): +// the round-trip of a written row with its delivery/kind/provider/host/actor +// intact and name-ordered, the door name-validation that rejects a bad name +// before any row is written, the declared_by FK on an unknown actor, the +// kind↔provider/host routing guard, and the delete path (found → gone, unknown → +// ErrNotFound). UpsertSecret carries CIPHERTEXT; these door checks all run before +// the store touches Postgres, so a dummy ciphertext/nonce pair is enough to reach +// them (the encrypt+decrypt round-trip is the StoreResolver's own pgtest). The +// scope model, value columns, and ciphertext-at-rest are proven in +// secrets_scope_pgtest_test.go. import ( "context" "testing" ) -func TestDeclareSecretRoundTrip(t *testing.T) { +// dummyCT / dummyNonce are placeholder value bytes: UpsertSecret stores them +// verbatim (the store never decrypts), and every door check these tests exercise +// runs before the row is written, so their content is irrelevant. +var ( + dummyCT = []byte("ciphertext") + dummyNonce = []byte("nonce") +) + +func TestUpsertSecretRoundTrip(t *testing.T) { ctx := context.Background() s := newTestStore(t) actor := mustUser(t, s, "declarer") // A generic file secret, a provider secret carrying a Provider id, and a gh - // secret carrying a Host — the three routing classes, declared out of name - // order to prove the read orders them. - if err := s.DeclareSecret(ctx, actor.ID, "ZED_TOKEN", SecretDeliveryFile, SecretKindGeneric, "", ""); err != nil { - t.Fatalf("declare generic: %v", err) + // secret carrying a Host — the three routing classes, written out of name + // order to prove the read orders them. Tenant scope so DeclaredSecrets (the + // value-free view ListSecrets and the server resolver read) lists them. + if err := s.UpsertSecret(ctx, actor.ID, "ZED_TOKEN", SecretScopeTenant, "", SecretDeliveryFile, SecretKindGeneric, "", "", dummyCT, dummyNonce, 1); err != nil { + t.Fatalf("upsert generic: %v", err) } - if err := s.DeclareSecret(ctx, actor.ID, "ANTHROPIC_KEY", SecretDeliveryEnv, SecretKindProvider, "anthropic", ""); err != nil { - t.Fatalf("declare provider: %v", err) + if err := s.UpsertSecret(ctx, actor.ID, "ANTHROPIC_KEY", SecretScopeTenant, "", SecretDeliveryEnv, SecretKindProvider, "anthropic", "", dummyCT, dummyNonce, 1); err != nil { + t.Fatalf("upsert provider: %v", err) } - if err := s.DeclareSecret(ctx, actor.ID, "GH_TOKEN", SecretDeliveryFile, SecretKindGH, "", "github.com"); err != nil { - t.Fatalf("declare gh: %v", err) + if err := s.UpsertSecret(ctx, actor.ID, "GH_TOKEN", SecretScopeTenant, "", SecretDeliveryFile, SecretKindGH, "", "github.com", dummyCT, dummyNonce, 1); err != nil { + t.Fatalf("upsert gh: %v", err) } got, err := s.DeclaredSecrets(ctx) @@ -44,7 +57,7 @@ func TestDeclareSecretRoundTrip(t *testing.T) { wantOrder := []string{"ANTHROPIC_KEY", "GH_TOKEN", "ZED_TOKEN"} for i, w := range wantOrder { if got[i].Name != w { - t.Errorf("row %d name = %q, want %q (name-ordered)", i, got[i].Name, w) + t.Errorf("row %d name = %q, want %q (name order not preserved)", i, got[i].Name, w) } } @@ -62,31 +75,44 @@ func TestDeclareSecretRoundTrip(t *testing.T) { if d := byName["ZED_TOKEN"]; d.Delivery != SecretDeliveryFile || d.Kind != SecretKindGeneric || d.Provider != "" || d.Host != "" { t.Errorf("generic secret round-trip mismatch: %+v", d) } - // Never a value: the struct has no value field; timestamps are set. if byName["ZED_TOKEN"].CreatedAt.IsZero() { t.Error("CreatedAt not populated on round-trip") } } -func TestDeclareSecretDuplicateConflict(t *testing.T) { +func TestUpsertSecretReSetRewrites(t *testing.T) { ctx := context.Background() s := newTestStore(t) actor := mustUser(t, s, "declarer") - if err := s.DeclareSecret(ctx, actor.ID, "API_KEY", SecretDeliveryEnv, SecretKindGeneric, "", ""); err != nil { - t.Fatalf("first declare: %v", err) + // A re-write of an existing coordinate is the upsert's UPDATE arm, NOT an + // ErrConflict: declaration and value are one row, so a second Set is a value + // rewrite. The old declare-then-set ErrConflict branch is gone. + if err := s.UpsertSecret(ctx, actor.ID, "API_KEY", SecretScopeTenant, "", SecretDeliveryEnv, SecretKindGeneric, "", "", dummyCT, dummyNonce, 1); err != nil { + t.Fatalf("first upsert: %v", err) + } + if err := s.UpsertSecret(ctx, actor.ID, "API_KEY", SecretScopeTenant, "", SecretDeliveryFile, SecretKindGeneric, "", "", []byte("ct2"), []byte("nonce2"), 1); err != nil { + t.Fatalf("re-upsert (value rewrite) = %v, want success", err) + } + got, err := s.DeclaredSecrets(ctx) + if err != nil { + t.Fatalf("DeclaredSecrets: %v", err) + } + if len(got) != 1 { + t.Fatalf("re-upsert wrote %d rows, want 1 (a rewrite, not a second row)", len(got)) + } + if got[0].Delivery != SecretDeliveryFile { + t.Errorf("delivery = %v after rewrite, want File (the second write's routing)", got[0].Delivery) } - err := s.DeclareSecret(ctx, actor.ID, "API_KEY", SecretDeliveryFile, SecretKindGeneric, "", "") - sentinelIs(t, err, ErrConflict, "duplicate secret name") } -func TestDeclareSecretInvalidNameRejected(t *testing.T) { +func TestUpsertSecretInvalidNameRejected(t *testing.T) { ctx := context.Background() s := newTestStore(t) actor := mustUser(t, s, "declarer") for _, bad := range []string{"bad-name", "", "a/b", "1abc", "a b", "../x"} { - err := s.DeclareSecret(ctx, actor.ID, bad, SecretDeliveryEnv, SecretKindGeneric, "", "") + err := s.UpsertSecret(ctx, actor.ID, bad, SecretScopeTenant, "", SecretDeliveryEnv, SecretKindGeneric, "", "", dummyCT, dummyNonce, 1) sentinelIs(t, err, ErrInvalidArgument, "invalid secret name "+bad) } @@ -100,13 +126,13 @@ func TestDeclareSecretInvalidNameRejected(t *testing.T) { } } -func TestDeclareSecretUnknownActorInvalid(t *testing.T) { +func TestUpsertSecretUnknownActorInvalid(t *testing.T) { ctx := context.Background() s := newTestStore(t) // A well-formed name but an actor account that was never created → the // declared_by FK yields ErrInvalidArgument. - err := s.DeclareSecret(ctx, AccountID("acct-never-created"), "API_KEY", SecretDeliveryEnv, SecretKindGeneric, "", "") + err := s.UpsertSecret(ctx, AccountID("acct-never-created"), "API_KEY", SecretScopeTenant, "", SecretDeliveryEnv, SecretKindGeneric, "", "", dummyCT, dummyNonce, 1) sentinelIs(t, err, ErrInvalidArgument, "unknown declaring account") } @@ -115,8 +141,8 @@ func TestDeleteSecretDeclaration(t *testing.T) { s := newTestStore(t) actor := mustUser(t, s, "declarer") - if err := s.DeclareSecret(ctx, actor.ID, "API_KEY", SecretDeliveryEnv, SecretKindGeneric, "", ""); err != nil { - t.Fatalf("declare: %v", err) + if err := s.UpsertSecret(ctx, actor.ID, "API_KEY", SecretScopeTenant, "", SecretDeliveryEnv, SecretKindGeneric, "", "", dummyCT, dummyNonce, 1); err != nil { + t.Fatalf("upsert: %v", err) } if err := s.DeleteSecretDeclaration(ctx, actor.ID, "API_KEY", SecretScopeTenant, ""); err != nil { t.Fatalf("delete: %v", err) @@ -140,7 +166,7 @@ func TestDeleteUnknownSecretNotFound(t *testing.T) { sentinelIs(t, err, ErrNotFound, "delete unknown secret") } -func TestDeclareSecretKindRoutingRejected(t *testing.T) { +func TestUpsertSecretKindRoutingRejected(t *testing.T) { ctx := context.Background() s := newTestStore(t) actor := mustUser(t, s, "declarer") @@ -164,7 +190,7 @@ func TestDeclareSecretKindRoutingRejected(t *testing.T) { } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - err := s.DeclareSecret(ctx, actor.ID, "API_KEY", SecretDeliveryEnv, tc.kind, tc.provider, tc.host) + err := s.UpsertSecret(ctx, actor.ID, "API_KEY", SecretScopeTenant, "", SecretDeliveryEnv, tc.kind, tc.provider, tc.host, dummyCT, dummyNonce, 1) sentinelIs(t, err, ErrInvalidArgument, tc.name) }) } @@ -179,7 +205,7 @@ func TestDeclareSecretKindRoutingRejected(t *testing.T) { } } -func TestDeclareSecretKindRoutingAccepted(t *testing.T) { +func TestUpsertSecretKindRoutingAccepted(t *testing.T) { ctx := context.Background() s := newTestStore(t) actor := mustUser(t, s, "declarer") @@ -197,7 +223,7 @@ func TestDeclareSecretKindRoutingAccepted(t *testing.T) { {"GENERIC_KEY", SecretKindGeneric, "", ""}, } for _, tc := range cases { - if err := s.DeclareSecret(ctx, actor.ID, tc.name, SecretDeliveryEnv, tc.kind, tc.provider, tc.host); err != nil { + if err := s.UpsertSecret(ctx, actor.ID, tc.name, SecretScopeTenant, "", SecretDeliveryEnv, tc.kind, tc.provider, tc.host, dummyCT, dummyNonce, 1); err != nil { t.Errorf("%s: valid combo rejected: %v", tc.name, err) } } diff --git a/go/internal/store/server_secrets.go b/go/internal/store/server_secrets.go index aaa196719..1444de8a7 100644 --- a/go/internal/store/server_secrets.go +++ b/go/internal/store/server_secrets.go @@ -86,8 +86,8 @@ type ServerSecretDeclaration struct { } // DeclareServerSecret adds a names-only row to the SEPARATE server-secret -// registry, mirroring DeclareSecret minus delivery/kind/provider/host. It -// stores NO value. +// registry: like a user-secret row (UpsertSecret) minus delivery/kind/provider/ +// host and, being names-only, minus any value. // // name is validated against SecretSpec's env-var-name grammar AND is REQUIRED // to carry a reserved server-secret prefix — so no writer (the admin RPC, or diff --git a/go/internal/store/server_secrets_pgtest_test.go b/go/internal/store/server_secrets_pgtest_test.go index 63d38c74a..7103dc191 100644 --- a/go/internal/store/server_secrets_pgtest_test.go +++ b/go/internal/store/server_secrets_pgtest_test.go @@ -127,7 +127,7 @@ func TestT0KeyspacePartition(t *testing.T) { // User door REJECTS those same prefixes. for _, name := range []string{"SERVER_LINEAR_FORGE_CLIENT_SECRET", "GATEWAY_CREDENTIALS_MASTER_KEY"} { - err := s.DeclareSecret(ctx, actor, name, SecretDeliveryEnv, SecretKindGeneric, "", "") + err := s.UpsertSecret(ctx, actor, name, SecretScopeTenant, "", SecretDeliveryEnv, SecretKindGeneric, "", "", dummyCT, dummyNonce, 1) if !errors.Is(err, ErrInvalidArgument) { t.Fatalf("user declare of reserved name %s: want ErrInvalidArgument, got %v", name, err) } @@ -137,7 +137,7 @@ func TestT0KeyspacePartition(t *testing.T) { // A legitimate user secret is declared first so this check has something to // iterate: without it the loop ran zero times and would have passed even if // both registries shared one table. - if err := s.DeclareSecret(ctx, actor, "PLAIN_USER_TOKEN", SecretDeliveryEnv, SecretKindGeneric, "", ""); err != nil { + if err := s.UpsertSecret(ctx, actor, "PLAIN_USER_TOKEN", SecretScopeTenant, "", SecretDeliveryEnv, SecretKindGeneric, "", "", dummyCT, dummyNonce, 1); err != nil { t.Fatalf("declare user secret: %v", err) } userRows, err := s.DeclaredSecrets(ctx) diff --git a/go/internal/store/updated_at_pgtest_test.go b/go/internal/store/updated_at_pgtest_test.go index 0273676c4..27d605ce2 100644 --- a/go/internal/store/updated_at_pgtest_test.go +++ b/go/internal/store/updated_at_pgtest_test.go @@ -107,24 +107,22 @@ func TestUpdatedAtTriggerFiresOnUpsertConflict(t *testing.T) { } // TestSecretsUpdatedAtIsLive proves property 3 — the specific rot RIG-3495 -// fixes. secrets.updated_at is declared, read by DeclaredSecrets, and surfaced -// on SecretDeclaration.UpdatedAt, but queries/secrets.sql has only an INSERT and -// a DELETE: no write path ever set the column, so its value could never differ -// from created_at and every reader was reading a lie. +// fixes. secrets.updated_at is read by DeclaredSecrets and surfaced on +// SecretDeclaration.UpdatedAt. Before RIG-3495 queries/secrets.sql had only an +// INSERT and a DELETE: no write path ever set the column, so its value could +// never differ from created_at and every reader was reading a lie. // -// The store therefore still has no update method for a secret, and this test -// does NOT invent one. It asserts what the store's own surface can show (a -// freshly declared row has updated_at == created_at), and then drives a bare -// UPDATE on the table to prove the trigger is ARMED on secrets — so the column -// becomes correct for free the moment a re-declare/rotate path is added, rather -// than needing whoever adds it to remember. +// UpsertSecret's ON CONFLICT arm is now that write path, and this asserts the +// trigger is ARMED on secrets: a freshly upserted row has updated_at == +// created_at, and a bare UPDATE on the table then advances updated_at while +// leaving created_at fixed. func TestSecretsUpdatedAtIsLive(t *testing.T) { ctx := context.Background() s := newTestStore(t) actor := mustUser(t, s, "secrets-owner") - if err := s.DeclareSecret(ctx, actor.ID, "DATABASE_URL", SecretDeliveryEnv, SecretKindGeneric, "", ""); err != nil { - t.Fatalf("DeclareSecret: %v", err) + if err := s.UpsertSecret(ctx, actor.ID, "DATABASE_URL", SecretScopeTenant, "", SecretDeliveryEnv, SecretKindGeneric, "", "", dummyCT, dummyNonce, 1); err != nil { + t.Fatalf("UpsertSecret: %v", err) } createdBefore, updatedBefore := secretStamps(t, s, "DATABASE_URL") if !updatedBefore.Equal(createdBefore) { diff --git a/go/server/network_door.go b/go/server/network_door.go index a74a0d7ba..ba1489001 100644 --- a/go/server/network_door.go +++ b/go/server/network_door.go @@ -266,7 +266,7 @@ func buildNetworkServer( st *store.Store, adminID store.AccountID, netTLS *tls.Config, - resolver secrets.Resolver, + resolver *secrets.StoreResolver, otelIC *otelconnect.Interceptor, webhookSink ForgeEventSink, webhookSecret func(ctx context.Context) ([]byte, error), diff --git a/go/server/network_door_test.go b/go/server/network_door_test.go index 5ab7172d2..3eb5c2008 100644 --- a/go/server/network_door_test.go +++ b/go/server/network_door_test.go @@ -159,6 +159,7 @@ func newTLSClient(t *testing.T, addr string, pool *x509.CertPool) compassv1conne // waitServing. func serveInBackground(t *testing.T, cfg ServeConfig) { t.Helper() + provisionMasterKeyProvider(t, &cfg) ctx, cancel := context.WithCancel(context.Background()) errCh := make(chan error, 1) go func() { errCh <- Serve(ctx, cfg) }() @@ -175,6 +176,31 @@ func serveInBackground(t *testing.T, cfg ServeConfig) { }) } +// provisionMasterKeyProvider gives a full-Serve test a resolvable at-rest master +// key. Serve now calls resolveMasterKey at boot (T4/T5) and fails closed with a +// runbook when the key is absent, so a test that drives Serve MUST provision one. +// It writes a dotenv holding COMPASS_MASTER_KEY and points cfg.SecretProvider at +// it — the same production knob the CLI's --secret-provider flag sets — unless the +// test already chose a provider. testMasterKeyHex is a fixed valid 64-hex key; the +// value is irrelevant, only that boot resolves and pins it. +func provisionMasterKeyProvider(t *testing.T, cfg *ServeConfig) { + t.Helper() + if cfg.SecretProvider != "" { + return + } + path := filepath.Join(t.TempDir(), "master-key.env") + line := store.MasterKeyName + "=" + dotenvValue(testMasterKeyHex) + "\n" + if err := os.WriteFile(path, []byte(line), 0o600); err != nil { + t.Fatalf("write master-key dotenv: %v", err) + } + cfg.SecretProvider = "dotenv://" + path +} + +// testMasterKeyHex is a fixed, valid 64-hex-char (32-byte) master key for the +// full-Serve tests' boot-time resolveMasterKey. Distinct from keyHexA/keyHexB so +// a grep for those does not collide. +const testMasterKeyHex = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff" + // waitServing event-gates on the server actually serving RPCs: it waits for the // socket to bind (waitListening), then round-trips GetServerInfo over the UDS. // A successful RPC proves Serve has reached the serving stage — which, in Serve's diff --git a/go/server/otel_emission_pgtest_test.go b/go/server/otel_emission_pgtest_test.go index 0cc236b52..e5b24dd8d 100644 --- a/go/server/otel_emission_pgtest_test.go +++ b/go/server/otel_emission_pgtest_test.go @@ -379,8 +379,10 @@ func serveOTelSocket(t *testing.T, version, dsn string) string { path := filepath.Join(t.TempDir(), "compass.sock") ctx, cancel := context.WithCancel(context.Background()) // test root (rule://go-thread-context _test.go exemption) errCh := make(chan error, 1) + cfg := ServeConfig{SocketPath: path, Version: version, DatabaseDSN: dsn} + provisionMasterKeyProvider(t, &cfg) go func() { - errCh <- Serve(ctx, ServeConfig{SocketPath: path, Version: version, DatabaseDSN: dsn}) + errCh <- Serve(ctx, cfg) }() t.Cleanup(func() { cancel() diff --git a/go/server/secrets_service.go b/go/server/secrets_service.go index c054fb247..ddb397ee1 100644 --- a/go/server/secrets_service.go +++ b/go/server/secrets_service.go @@ -48,28 +48,32 @@ type secretsSignaler interface { } // secretsService implements compassv1connect.SecretsServiceHandler over the -// store's names registry and the secret resolver. The store owns the value-free -// declaration rows; the resolver is the provider write/resolve path; the signaler -// notifies live sessions on a write. signaler may be nil on a server with no -// Runner door (socket-only), in which case a write completes without a signal — -// there is no live session to notify. +// store's registry and the secret resolvers. The user path (Set/Delete) writes +// through the DB-backed StoreResolver — declaration and encrypted value are one +// atomic upsert; the serverResolver is the value-free server-secret READ surface +// the ListServerSecrets probe uses; the signaler notifies live sessions on a +// write. signaler may be nil on a server with no Runner door (socket-only), in +// which case a write completes without a signal — there is no live session to +// notify. type secretsService struct { compassv1connect.UnimplementedSecretsServiceHandler - store *store.Store - resolver secrets.Resolver - // serverResolver is the SECOND resolver instance, reading the separate - // server_secrets registry. It is a DISTINCT instance from resolver (whose - // manifest is the user registry the container-delivery path reads), so a - // server-secret write can never land in the container manifest. nil on a - // server with no server-secret wiring, in which case the admin RPCs - // fail closed with errNoServerResolver rather than silently writing to the - // user registry. + store *store.Store + // resolver is the DB-backed user-secret write path (StoreResolver.Upsert / + // Remove): declaration and encrypted value land in one atomic upsert, so the + // old declare-then-Set-then-rollback trio is gone. Concrete, not an interface + // (record A4): there is exactly one DB-backed store and nothing to swap in. + resolver *secrets.StoreResolver + // serverResolver reads the separate server_secrets registry — the value-free + // Statuses probe ListServerSecrets uses. It is a DISTINCT surface from the + // user write path, so a server secret can never land in the container- + // delivery table. nil on a server with no server-secret wiring, in which case + // the server-secret list RPC fails closed with errNoServerResolver. serverResolver secrets.Resolver signaler secretsSignaler } // newSecretsService constructs the SecretsService handler. -func newSecretsService(st *store.Store, resolver, serverResolver secrets.Resolver, signaler secretsSignaler) *secretsService { +func newSecretsService(st *store.Store, resolver *secrets.StoreResolver, serverResolver secrets.Resolver, signaler secretsSignaler) *secretsService { return &secretsService{store: st, resolver: resolver, serverResolver: serverResolver, signaler: signaler} } @@ -86,28 +90,22 @@ var errNoResolver = errors.New("no secret resolver configured on this server") // path reads, inverting the whole point of the separate store. var errNoServerResolver = errors.New("no server secret resolver configured on this server") -// SetSecret declares a secret's registry row and writes its value via the -// resolver. USER-ONLY (record §911-927): an agent-token caller is -// CodePermissionDenied. `value` is never logged. +// SetSecret writes a user secret's declaration and encrypted value in ONE atomic +// upsert at the caller-resolved scope coordinate. USER-ONLY (record §911-927): an +// agent-token caller is CodePermissionDenied. `value` is never logged. // -// Flow (declare-then-set): DeclareSecret records the value-free row, then -// resolver.Set writes the value to the provider. A re-Set of an already-declared -// name is a value REWRITE, not a failure: DeclareSecret returns ErrConflict for a -// duplicate name (store/secrets.go), so on ErrConflict this proceeds to -// resolver.Set anyway — the name already exists and we are rewriting its value. -// (Conflict policy per the driver brief; not invented here.) An empty value is -// rejected up front, before any row is declared. A failed FRESH write rolls back -// the declaration so no orphan survives (an orphaned declaration is required=true -// in the resolve manifest and would poison EVERY live session's FetchSecrets). On -// a successful write the secrets version is bumped so live sessions re-fetch. The -// declare/set/rollback trio is not atomic and assumes no concurrent same-name -// writer (the single-Runner MVP: SetSecret is user-driven CLI); a future -// multi-writer path must re-examine the race. +// The declaration and the value are the same row now (A1), so the write is a +// single StoreResolver.Upsert transaction — the old declare-then-Set-then-rollback +// trio and its ErrConflict re-set branch are gone: a re-set of an existing +// coordinate is just the upsert's UPDATE arm. An empty value is rejected up front, +// before any row is touched; the scope coordinate is resolved per D9 +// (unspecified/user → the caller's private coordinate, tenant → admin-gated). On a +// successful write the secrets version is bumped so live sessions re-fetch. func (s *secretsService) SetSecret( ctx context.Context, req *connect.Request[compassv1.SetSecretRequest], ) (*connect.Response[compassv1.SetSecretResponse], error) { - callerID, err := s.requireUser(ctx) + callerID, role, err := s.requireUser(ctx) if err != nil { return nil, err } @@ -123,48 +121,21 @@ func (s *secretsService) SetSecret( return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("secret value is empty")) } - declErr := s.store.DeclareSecret(ctx, callerID, msg.GetName(), delivery, kind, msg.GetProvider(), msg.GetHost()) - switch { - case declErr == nil: - // Fresh declaration. - case errors.Is(declErr, store.ErrConflict): - // Already declared: a re-Set rewrites the value (proceed to resolver.Set). - case errors.Is(declErr, store.ErrInvalidArgument): - return nil, connect.NewError(connect.CodeInvalidArgument, declErr) - default: - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("declaring secret: %w", declErr)) + scopeKind, scopeID, err := resolveSecretScope(msg.GetScope(), callerID, role) + if err != nil { + return nil, err } - // The audit reason carries the authenticated caller, so the provider's log - // distinguishes which operator wrote a secret rather than recording every - // write anonymously. The RPC is the only path that reaches this write, so - // the prefix also records that provenance. callerID is resolved from the - // bearer token (auth.CallerFrom -> the token subject), never a request - // field, and every account id is server-minted hex (store/ids.go), so it - // cannot carry a quote or newline into the reason; the CLI additionally - // JSON-escapes the reason into its audit record, so a forged log entry is - // doubly unreachable. - reason := fmt.Sprintf("compass: operator secret write via SetSecret RPC (caller %s)", callerID) - if err := s.resolver.Set(ctx, msg.GetName(), msg.GetValue(), reason); err != nil { - // The name was validated by DeclareSecret and the value was screened - // non-empty above, so a Set failure here is a provider/exec fault - // (CLI unreachable, non-zero exit) — retryable and operator-side, never - // the caller's argument, so CodeUnavailable, not CodeInvalidArgument. - // Roll back a FRESH declaration: an orphaned declaration is required=true - // in the resolve manifest and would fail EVERY live session's FetchSecrets - // (a global denial from one failed write). Leave an ErrConflict (re-Set) - // row alone — it legitimately pre-existed this call. The Set error wraps - // name/cli/stderr, never the value, so logging it server-side is safe; the - // client-facing error is value-free. - if declErr == nil { - // Tenant coordinate (scope 0, "") is a T5 placeholder: this handler still - // declares at tenant scope pending the write-surface scope ruling (A9 OQ). - if delErr := s.store.DeleteSecretDeclaration(ctx, callerID, msg.GetName(), 0, ""); delErr != nil { - slog.ErrorContext(ctx, "rolling back secret declaration after failed write", "err", delErr) - } + // One atomic upsert: the store validates name grammar, the reserved-prefix + // partition, kind routing, and the scope shape at its door, so an invalid + // argument maps to CodeInvalidArgument. The value is encrypted before the + // store door and never logged; the error wraps the name only, never the value. + if err := s.resolver.Upsert(ctx, callerID, msg.GetName(), scopeKind, scopeID, msg.GetValue(), delivery, kind, msg.GetProvider(), msg.GetHost()); err != nil { + if errors.Is(err, store.ErrInvalidArgument) { + return nil, connect.NewError(connect.CodeInvalidArgument, err) } slog.ErrorContext(ctx, "writing secret value", "err", err) - return nil, connect.NewError(connect.CodeUnavailable, errors.New("writing secret value failed")) + return nil, connect.NewError(connect.CodeInternal, errors.New("writing secret value failed")) } s.bumpSecretsVersion(ctx) return connect.NewResponse(&compassv1.SetSecretResponse{}), nil @@ -209,14 +180,18 @@ func (s *secretsService) ListSecrets( return connect.NewResponse(&compassv1.ListSecretsResponse{Secrets: out}), nil } -// DeleteSecret removes a secret's provider value and registry row, then bumps the -// secrets version. USER-ONLY (record §915-918): an agent-token caller is -// CodePermissionDenied. A name that was never declared is CodeNotFound. +// DeleteSecret removes a user secret's row (declaration and value are the same +// row post-A1) at the caller-resolved coordinate, then bumps the secrets version. +// USER-ONLY (record §915-918): an agent-token caller is CodePermissionDenied. A +// name that was never declared at that coordinate is CodeNotFound. A reserved- +// prefix name is rejected CodeInvalidArgument ahead of any store call — the F1 +// name partition keeps reserved names out of the user table, so a delete on one +// is a malformed request (defense in depth beside Upsert's own reject). func (s *secretsService) DeleteSecret( ctx context.Context, req *connect.Request[compassv1.DeleteSecretRequest], ) (*connect.Response[compassv1.DeleteSecretResponse], error) { - callerID, err := s.requireUser(ctx) + callerID, role, err := s.requireUser(ctx) if err != nil { return nil, err } @@ -224,24 +199,21 @@ func (s *secretsService) DeleteSecret( return nil, connect.NewError(connect.CodeUnavailable, errNoResolver) } name := req.Msg.GetName() - // Ordering note: resolver.Delete is a validate-only no-op today, so calling - // it before DeleteSecretDeclaration is inert. The provider verb it would - // shell EXISTS at this pin (`secretspec delete`, 0.18+); wiring it is a - // deferral (RIG-3436), not an upstream gap. When it lands, this MUST flip to - // declaration-first: the declaration is the source of truth Resolve reads, and - // deleting the provider value before the row would leave a required=true - // declaration pointing at a missing value — the same global resolve-poison as a - // failed Set, in reverse. - if err := s.resolver.Delete(ctx, name); err != nil { - return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("deleting secret value: %w", err)) - } - // Tenant coordinate (scope 0, "") is a T5 placeholder: this handler deletes at - // tenant scope pending the write-surface scope ruling (A9 OQ). - if err := s.store.DeleteSecretDeclaration(ctx, callerID, name, 0, ""); err != nil { + if store.ShadowsServerSecretPrefix(name) { + return nil, connect.NewError(connect.CodeInvalidArgument, + fmt.Errorf("secret name %q uses a reserved server-secret prefix", name)) + } + scopeKind, scopeID, err := resolveSecretScope(req.Msg.GetScope(), callerID, role) + if err != nil { + return nil, err + } + // One transaction: declaration and value are the same row, so there is no + // half-deleted state to order or recover from (record A5). + if err := s.resolver.Remove(ctx, callerID, name, scopeKind, scopeID); err != nil { if errors.Is(err, store.ErrNotFound) { return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("secret %q", name)) } - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("deleting secret declaration: %w", err)) + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("deleting secret: %w", err)) } s.bumpSecretsVersion(ctx) return connect.NewResponse(&compassv1.DeleteSecretResponse{}), nil @@ -419,29 +391,37 @@ func (s *secretsService) requireCaller(ctx context.Context) (store.AccountID, er return callerID, nil } -// requireUser returns the authenticated caller id only when it is a USER account; -// an agent account is CodePermissionDenied (the user-only write gate, record -// §919-927 — the same fail-closed posture as admin-gated IssueToken). No caller is -// CodeUnauthenticated (fail closed). The account kind is read from the store (an -// agent account has the Agent subtype set; a user does not — IsAgent). -func (s *secretsService) requireUser(ctx context.Context) (store.AccountID, error) { +// requireUser returns the authenticated caller id AND role only when the caller +// is a USER account; an agent account is CodePermissionDenied (the user-only +// write gate, record §919-927 — the same fail-closed posture as admin-gated +// IssueToken). No caller is CodeUnauthenticated (fail closed). The account kind +// is read from the store (an agent account has the Agent subtype set; a user +// does not — IsAgent). The role is returned so a handler can gate a tenant-scope +// write (D9) without a second GetAccount; a caller with no user payload (the +// reserved system account) is the least-privilege member, so it cannot pass the +// admin gate. +func (s *secretsService) requireUser(ctx context.Context) (store.AccountID, store.UserRole, error) { callerID, err := s.requireCaller(ctx) if err != nil { - return "", err + return "", 0, err } acct, err := s.store.GetAccount(ctx, callerID) if err != nil { if errors.Is(err, store.ErrNotFound) { // A caller the bearer door authenticated but whose account row is gone: // fail closed rather than admit a write under an unresolvable identity. - return "", connect.NewError(connect.CodePermissionDenied, fmt.Errorf("caller account %q not found", callerID)) + return "", 0, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("caller account %q not found", callerID)) } - return "", connect.NewError(connect.CodeInternal, fmt.Errorf("resolving caller account: %w", err)) + return "", 0, connect.NewError(connect.CodeInternal, fmt.Errorf("resolving caller account: %w", err)) } if acct.IsAgent() { - return "", connect.NewError(connect.CodePermissionDenied, errors.New("secret writes are user-only")) + return "", 0, connect.NewError(connect.CodePermissionDenied, errors.New("secret writes are user-only")) } - return callerID, nil + var role store.UserRole + if acct.User != nil { + role = acct.User.Role + } + return callerID, role, nil } // bumpSecretsVersion emits the SecretsVersion signal after a successful write. @@ -458,28 +438,29 @@ func (s *secretsService) bumpSecretsVersion(ctx context.Context) { } } -// secretRoutingFromProto maps the public proto delivery/kind enums to the store -// enums, rejecting an UNSPECIFIED value (the proto 0) as an invalid argument — a -// SetSecret must name a concrete delivery and kind. The store's DeclareSecret -// re-validates the kind↔provider/host routing invariant, so this only translates. -func secretRoutingFromProto(d compassv1.SecretDelivery, k compassv1.SecretKind) (store.SecretDelivery, store.SecretKind, error) { - var delivery store.SecretDelivery +// secretRoutingFromProto maps the public proto delivery/kind enums to the +// resolve-surface (secrets package) enums StoreResolver.Upsert takes, rejecting +// an UNSPECIFIED value (the proto 0) as an invalid argument — a SetSecret must +// name a concrete delivery and kind. The store door re-validates the +// kind↔provider/host routing invariant, so this only translates. +func secretRoutingFromProto(d compassv1.SecretDelivery, k compassv1.SecretKind) (secrets.DeliveryKind, secrets.SecretKind, error) { + var delivery secrets.DeliveryKind switch d { case compassv1.SecretDelivery_SECRET_DELIVERY_FILE: - delivery = store.SecretDeliveryFile + delivery = secrets.DeliveryFile case compassv1.SecretDelivery_SECRET_DELIVERY_ENV: - delivery = store.SecretDeliveryEnv + delivery = secrets.DeliveryEnv default: return 0, 0, errors.New("secret delivery is unspecified") } - var kind store.SecretKind + var kind secrets.SecretKind switch k { case compassv1.SecretKind_SECRET_KIND_GENERIC: - kind = store.SecretKindGeneric + kind = secrets.SecretGeneric case compassv1.SecretKind_SECRET_KIND_PROVIDER: - kind = store.SecretKindProvider + kind = secrets.SecretProvider case compassv1.SecretKind_SECRET_KIND_GH: - kind = store.SecretKindGH + kind = secrets.SecretGH default: return 0, 0, errors.New("secret kind is unspecified") } @@ -507,3 +488,27 @@ func kindToProto(k store.SecretKind) compassv1.SecretKind { return compassv1.SecretKind_SECRET_KIND_GENERIC } } + +// resolveSecretScope maps a wire SecretScope + caller identity to the store +// coordinate (scope_kind, scope_id) a user-secret write targets, implementing +// D9's matrix. UNSPECIFIED and USER both land at the caller's private user +// coordinate — the unspecified default is USER, so an omitted field never +// silently writes a tenant-wide value. TENANT lands at the shared coordinate +// (store 0, "") and requires UserRoleAdmin, else CodePermissionDenied. An +// unknown/out-of-range wire value is CodeInvalidArgument (fail closed). The wire +// numbers deliberately differ from the store's (tenant is 2 here, 0 there), so +// this MAPS explicitly rather than casting — a cast would turn an omitted field +// into a tenant write, the exact bug the default exists to prevent. +func resolveSecretScope(scope compassv1.SecretScope, callerID store.AccountID, role store.UserRole) (int16, string, error) { + switch scope { + case compassv1.SecretScope_SECRET_SCOPE_UNSPECIFIED, compassv1.SecretScope_SECRET_SCOPE_USER: + return store.SecretScopeUser, string(callerID), nil + case compassv1.SecretScope_SECRET_SCOPE_TENANT: + if role != store.UserRoleAdmin { + return 0, "", connect.NewError(connect.CodePermissionDenied, errors.New("tenant-scoped secret writes require an admin")) + } + return store.SecretScopeTenant, "", nil + default: + return 0, "", connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("unknown secret scope %d", scope)) + } +} diff --git a/go/server/secrets_service_pgtest_test.go b/go/server/secrets_service_pgtest_test.go index f5dd2d27b..a4376e3d4 100644 --- a/go/server/secrets_service_pgtest_test.go +++ b/go/server/secrets_service_pgtest_test.go @@ -4,23 +4,25 @@ package server // Store-gated SecretsService authz contracts: the user-only Set/Delete gate // (the load-bearing regression, record §927), the user-AND-agent ListSecrets, the -// is_set-without-resolve invariant, delete-not-found, and the SecretsVersion bump -// on a successful write. They need a real Postgres because the write path declares -// a registry row and the authz gate reads the caller's account KIND from the store +// is_set-without-resolve invariant, delete-not-found, the SecretsVersion bump +// on a successful write, and the D9 scope model end to end. They need a real +// Postgres because the write path now lands an encrypted row through the DB-backed +// StoreResolver and the authz gate reads the caller's account KIND from the store // (user vs agent). Driven through the production bearer + admin-gate interceptor // chain over a real connect client so the handler reads a genuine caller identity // the same way the shipped door supplies it. Behind `pgtest && unix` (SKIP when no // runtime). // -// The resolver is a fake (no real SecretSpec provider in a unit test): Set/Delete -// are recorded no-ops, and Resolve FAILS LOUDLY if ListSecrets ever calls it — the -// is_set-without-resolve invariant is asserted by that fake, not just by reading -// the code. +// The USER resolver is the real StoreResolver (T5): SetSecret/DeleteSecret write +// and delete encrypted rows, and a test observes the result through the same +// production read the Runner uses (ResolveFor / SecretRecordsForAgent), not a fake. +// The SERVER resolver is still a recording fake — the server-secret path keeps its +// SpecResolver seam (Set/Delete/Statuses) at this task, so its tests script and +// record that fake. import ( "context" "errors" - "strings" "testing" "connectrpc.com/connect" @@ -28,17 +30,19 @@ import ( compassv1 "github.com/RigelBuild/compass/go/gen/compass/v1" "github.com/RigelBuild/compass/go/gen/compass/v1/compassv1connect" "github.com/RigelBuild/compass/go/internal/auth" + "github.com/RigelBuild/compass/go/internal/envelope" "github.com/RigelBuild/compass/go/internal/pgtest" "github.com/RigelBuild/compass/go/internal/secrets" "github.com/RigelBuild/compass/go/internal/store" ) -// recordingResolver is a fake secrets.Resolver for the SecretsService tests. Set -// and Delete record their calls and succeed; Resolve fails loudly, so a test that -// wires this into ListSecrets proves the list path never resolves values to -// compute is_set (record §906-908 / brief item 7). Statuses returns a scripted -// value-free report — the server-secret list path's probe — and can be scripted -// to fail so a test proves a provider fault is not flattened into all-unset. +// recordingResolver is a fake secrets.Resolver standing in for the SERVER-secret +// resolver. Set and Delete record their calls and succeed; Resolve fails loudly, +// so a test proves no server-secret path ever resolves values. Statuses returns a +// scripted value-free report — the ListServerSecrets probe — and can be scripted +// to fail so a test proves a provider fault is not flattened into all-unset. The +// USER resolver is no longer a fake (it is the real StoreResolver); this type now +// serves only the server side. type recordingResolver struct { setErr error setNames []string @@ -97,12 +101,22 @@ type secretsFixture struct { userToken string agentToken string userID store.AccountID + agentID store.AccountID adminToken string - resolver *recordingResolver - // serverResolver is the SECOND fake, standing in for the server-secret - // resolver. Kept distinct from resolver so a test can prove a server-secret - // write lands ONLY on this one — the container-delivery registry must never - // see it. + // st is the live store, so a scope test can assert WHICH coordinate a write + // landed at rather than only that the RPC returned OK, and read a written + // value back through the production ResolveFor. + st *store.Store + // resolver is the REAL DB-backed user-secret resolver the service writes + // through — SetSecret/DeleteSecret land encrypted rows here, observed via the + // store (SecretRecordsForAgent / ResolveFor), never a fake. + resolver *secrets.StoreResolver + // key is the master key resolver was built with, so a test can ResolveFor a + // written value back (decrypt under the same key). + key envelope.Key + // serverResolver stands in for the server-secret resolver. Kept distinct from + // resolver so a test can prove a server-secret write lands ONLY on it — the + // container-delivery registry must never see it. serverResolver *recordingResolver signaler *recordingSignaler } @@ -137,7 +151,8 @@ func newSecretsFixture(t *testing.T) secretsFixture { t.Fatalf("IssueAccountToken(agent): %v", err) } - resolver := &recordingResolver{} + key := secretsFixtureKey(t) + resolver := secrets.NewStoreResolver(st, key, 1) serverResolver := &recordingResolver{} signaler := &recordingSignaler{} svc := newSecretsService(st, resolver, serverResolver, signaler) @@ -156,12 +171,31 @@ func newSecretsFixture(t *testing.T) secretsFixture { agentToken: agentTok, adminToken: adminTok, userID: user.ID, + agentID: agent.ID, + st: st, resolver: resolver, + key: key, serverResolver: serverResolver, signaler: signaler, } } +// secretsFixtureKey is a fixed 32-byte master key for the encrypted round-trips. +// The value is irrelevant — only that every Upsert and ResolveFor in one fixture +// use the same one, so a written value decrypts back. +func secretsFixtureKey(t *testing.T) envelope.Key { + t.Helper() + raw := make([]byte, 32) + for i := range raw { + raw[i] = byte(i) + } + k, err := envelope.NewKey(raw) + if err != nil { + t.Fatalf("NewKey: %v", err) + } + return k +} + func setReq(bearer, name, value string) *connect.Request[compassv1.SetSecretRequest] { req := connect.NewRequest(&compassv1.SetSecretRequest{ Name: name, @@ -179,6 +213,20 @@ func delReq(bearer, name string) *connect.Request[compassv1.DeleteSecretRequest] return req } +// scopedSetReq is setReq with an explicit D9 scope selector. setReq deliberately +// leaves scope unset, so it exercises the unspecified-means-user default. +func scopedSetReq(bearer, name, value string, scope compassv1.SecretScope) *connect.Request[compassv1.SetSecretRequest] { + req := setReq(bearer, name, value) + req.Msg.Scope = scope + return req +} + +func scopedDelReq(bearer, name string, scope compassv1.SecretScope) *connect.Request[compassv1.DeleteSecretRequest] { + req := delReq(bearer, name) + req.Msg.Scope = scope + return req +} + func listReq(bearer string) *connect.Request[compassv1.ListSecretsRequest] { req := connect.NewRequest(&compassv1.ListSecretsRequest{}) req.Header().Set("Authorization", "Bearer "+bearer) @@ -186,43 +234,30 @@ func listReq(bearer string) *connect.Request[compassv1.ListSecretsRequest] { } // TestSetSecretUserOnly is the load-bearing regression (record §927): an -// AGENT-token caller is CodePermissionDenied, a USER-token caller succeeds. The -// agent must never write a secret. +// AGENT-token caller is CodePermissionDenied, a USER-token caller succeeds and the +// value is written (resolvable back). The agent must never write a secret. func TestSetSecretUserOnly(t *testing.T) { f := newSecretsFixture(t) ctx := context.Background() - // Agent: rejected, and the resolver is never reached (no value written). + // Agent: rejected, and no row is written (nothing resolves for the agent). _, err := f.client.SetSecret(ctx, setReq(f.agentToken, "AGENT_TRY", "v")) if got := connect.CodeOf(err); got != connect.CodePermissionDenied { t.Fatalf("SetSecret as agent code = %v, want PermissionDenied", got) } - if len(f.resolver.setNames) != 0 { - t.Fatalf("resolver.Set called %v on a rejected agent SetSecret, want none", f.resolver.setNames) + if got := resolvedValues(t, ctx, f, f.agentID); len(got) != 0 { + t.Fatalf("a rejected agent SetSecret wrote rows: %v, want none", got) } - // User: succeeds and writes the value. The literal is hoisted because the - // value-absence assertion below asserts on it — inlining it twice lets the - // two drift, silently retiring that assertion. + // User: succeeds and the value is written — resolved back through the same + // production read the Runner uses, decrypted under the fixture key. const secretValue = "postgres://x" if _, err := f.client.SetSecret(ctx, setReq(f.userToken, "DB_URL", secretValue)); err != nil { t.Fatalf("SetSecret as user = %v, want success", err) } - if len(f.resolver.setNames) != 1 || f.resolver.setNames[0] != "DB_URL" { - t.Fatalf("resolver.Set names = %v, want [DB_URL]", f.resolver.setNames) - } - // The handler must hand the resolver a non-empty reason bound to the - // AUTHENTICATED caller: the provider's require_reason policy refuses a - // reasonless write outright, and the audit record is only useful if it names - // which operator wrote the secret. The reason must never carry the value. - if len(f.resolver.setReasons) != 1 || strings.TrimSpace(f.resolver.setReasons[0]) == "" { - t.Fatalf("resolver.Set reasons = %q, want one non-empty reason", f.resolver.setReasons) - } - if !strings.Contains(f.resolver.setReasons[0], string(f.userID)) { - t.Fatalf("resolver.Set reason = %q, want it to name the calling user %q", f.resolver.setReasons[0], f.userID) - } - if strings.Contains(f.resolver.setReasons[0], secretValue) { - t.Fatalf("resolver.Set reason = %q, must never carry the secret value", f.resolver.setReasons[0]) + got := resolvedValues(t, ctx, f, f.agentID) + if got["DB_URL"] != secretValue { + t.Fatalf("resolved DB_URL = %q, want the written value", got["DB_URL"]) } } @@ -246,9 +281,10 @@ func TestSetSecretBumpsSecretsVersion(t *testing.T) { } } -// TestSetSecretReSetRewrites: a re-Set of an already-declared name is a value -// rewrite (declare returns ErrConflict; the handler proceeds to resolver.Set), not -// a failure — the brief's conflict policy. +// TestSetSecretReSetRewrites: a re-Set of an existing coordinate is the upsert's +// UPDATE arm — the value is rewritten, not an ErrConflict. The old +// declare-then-set-then-ErrConflict branch is gone (declaration and value are one +// row now). func TestSetSecretReSetRewrites(t *testing.T) { f := newSecretsFixture(t) ctx := context.Background() @@ -258,57 +294,23 @@ func TestSetSecretReSetRewrites(t *testing.T) { if _, err := f.client.SetSecret(ctx, setReq(f.userToken, "DB_URL", "v2")); err != nil { t.Fatalf("re-SetSecret (value rewrite) = %v, want success", err) } - if len(f.resolver.setNames) != 2 { - t.Fatalf("resolver.Set called %d times across two Sets, want 2", len(f.resolver.setNames)) - } -} - -// TestSetSecretRollsBackDeclarationOnWriteFailure: a FRESH declaration whose -// resolver.Set fails (a provider/exec fault) is rolled back — the RPC returns -// CodeUnavailable and no orphaned declaration survives. An orphan would be -// required=true in the resolve manifest and poison EVERY live session's -// FetchSecrets, so the surface must be left clean. A follow-up re-Set (once the -// provider recovers) then succeeds and the name appears, proving the failure left -// nothing behind. -func TestSetSecretRollsBackDeclarationOnWriteFailure(t *testing.T) { - f := newSecretsFixture(t) - ctx := context.Background() - f.resolver.setErr = errors.New("provider down") - - _, err := f.client.SetSecret(ctx, setReq(f.userToken, "DB_URL", "v")) - if got := connect.CodeOf(err); got != connect.CodeUnavailable { - t.Fatalf("SetSecret with a failing provider write code = %v, want Unavailable", got) + got := resolvedValues(t, ctx, f, f.agentID) + if got["DB_URL"] != "v2" { + t.Fatalf("resolved DB_URL = %q after re-set, want the second value v2", got["DB_URL"]) } - - // The failed fresh write must leave no orphaned declaration behind. - resp, err := f.client.ListSecrets(ctx, listReq(f.userToken)) + // One row, not two: the re-set updated in place. + recs, err := f.st.SecretRecordsForAgent(ctx, f.agentID) if err != nil { - t.Fatalf("ListSecrets = %v, want success", err) + t.Fatalf("SecretRecordsForAgent: %v", err) } - for _, s := range resp.Msg.GetSecrets() { - if s.GetName() == "DB_URL" { - t.Fatal("declaration survived a failed fresh write — orphan left behind") + n := 0 + for _, r := range recs { + if r.Name == "DB_URL" { + n++ } } - - // Provider recovers: a re-Set of the same name now succeeds and appears, - // proving the earlier failure left the surface clean (a fresh declaration). - f.resolver.setErr = nil - if _, err := f.client.SetSecret(ctx, setReq(f.userToken, "DB_URL", "v")); err != nil { - t.Fatalf("re-SetSecret after provider recovery = %v, want success", err) - } - resp, err = f.client.ListSecrets(ctx, listReq(f.userToken)) - if err != nil { - t.Fatalf("ListSecrets after recovery = %v, want success", err) - } - var found bool - for _, s := range resp.Msg.GetSecrets() { - if s.GetName() == "DB_URL" { - found = true - } - } - if !found { - t.Fatal("DB_URL absent after a successful re-Set, want present") + if n != 1 { + t.Fatalf("DB_URL resolved to %d rows after a re-set, want 1 (an in-place rewrite)", n) } } @@ -323,22 +325,25 @@ func TestDeleteSecretUserOnly(t *testing.T) { } versionBefore := f.signaler.calls - // Agent: rejected, and the row survives (resolver.Delete never reached). + // Agent: rejected, and the row survives (still resolves for the owning agent). _, err := f.client.DeleteSecret(ctx, delReq(f.agentToken, "DB_URL")) if got := connect.CodeOf(err); got != connect.CodePermissionDenied { t.Fatalf("DeleteSecret as agent code = %v, want PermissionDenied", got) } - if len(f.resolver.deleteNames) != 0 { - t.Fatalf("resolver.Delete called %v on a rejected agent DeleteSecret, want none", f.resolver.deleteNames) + if got := resolvedValues(t, ctx, f, f.agentID); got["DB_URL"] != "v" { + t.Fatalf("row did not survive a rejected agent DeleteSecret: resolved %q, want v", got["DB_URL"]) } - // User: succeeds and bumps the version. + // User: succeeds, bumps the version, and the row is gone (no longer resolves). if _, err := f.client.DeleteSecret(ctx, delReq(f.userToken, "DB_URL")); err != nil { t.Fatalf("DeleteSecret as user = %v, want success", err) } if f.signaler.calls != versionBefore+1 { t.Fatalf("secrets version bumped to %d after Delete, want %d", f.signaler.calls, versionBefore+1) } + if got := resolvedValues(t, ctx, f, f.agentID); len(got) != 0 { + t.Fatalf("row survived a user DeleteSecret: %v, want none", got) + } } // TestDeleteSecretNotFound: deleting a name that was never declared is @@ -353,8 +358,10 @@ func TestDeleteSecretNotFound(t *testing.T) { } // TestListSecretsUserAndAgent: both a user and an agent token succeed and get -// value-free SecretStatus with is_set=true for a declared row — and the resolver's -// Resolve is NEVER called (is_set is computed without fetching values). +// value-free SecretStatus with is_set=true for a declared row. ListSecrets reads +// the declaration registry (DeclaredSecrets), never the resolver — so is_set is +// computed without decrypting any value; that the service's StoreResolver is never +// touched here is now structural, not something a fake can record. func TestListSecretsUserAndAgent(t *testing.T) { f := newSecretsFixture(t) ctx := context.Background() @@ -390,10 +397,6 @@ func TestListSecretsUserAndAgent(t *testing.T) { } }) } - // The is_set-without-resolve invariant: ListSecrets never resolved any value. - if f.resolver.resolveHit { - t.Fatal("ListSecrets resolved values to compute is_set — must not fetch secrets to list them") - } } func setServerReq(bearer, name, value string) *connect.Request[compassv1.SetServerSecretRequest] { @@ -446,9 +449,8 @@ func TestSetServerSecretWritesOnlyTheServerResolver(t *testing.T) { if len(f.serverResolver.setNames) != 1 || f.serverResolver.setNames[0] != "SERVER_LINEAR_FORGE_CLIENT_SECRET" { t.Fatalf("server resolver sets = %v, want the one server secret", f.serverResolver.setNames) } - if len(f.resolver.setNames) != 0 { - t.Fatalf("user resolver was written: %v — a server secret must never reach the container registry", f.resolver.setNames) - } + // The user registry (DeclaredSecrets, which the container-delivery path and + // the CLI read) must never see it — asserted directly through ListSecrets below. // And it must not appear in the user-facing list, which is what the // container-delivery path and the CLI both read. @@ -643,3 +645,201 @@ func TestListServerSecretsAdminOnly(t *testing.T) { t.Fatalf("admin token: %v", err) } } + +// D9 scope selector: an omitted scope must land at the CALLER's user coordinate, +// never the shared tenant one. This is the load-bearing default — a client built +// before the selector existed writes a private value, not a tenant-wide one. +func TestSetSecretOmittedScopeLandsAtCallerUserCoordinate(t *testing.T) { + f := newSecretsFixture(t) + ctx := context.Background() + + if _, err := f.client.SetSecret(ctx, setReq(f.userToken, "DB_URL", "v")); err != nil { + t.Fatalf("SetSecret(omitted scope): %v", err) + } + // An EXPLICIT user scope must reach the same coordinate as an omitted one; + // otherwise the default and the named tier could drift apart unnoticed. + if _, err := f.client.SetSecret(ctx, scopedSetReq(f.userToken, "API_KEY", "v", compassv1.SecretScope_SECRET_SCOPE_USER)); err != nil { + t.Fatalf("SetSecret(explicit user scope): %v", err) + } + + recs, err := f.st.SecretRecordsForAgent(ctx, f.agentID) + if err != nil { + t.Fatalf("SecretRecordsForAgent: %v", err) + } + found := 0 + for _, r := range recs { + if r.Name != "DB_URL" && r.Name != "API_KEY" { + continue + } + found++ + if r.ScopeKind != store.SecretScopeUser { + t.Errorf("scope_kind = %d, want %d (user)", r.ScopeKind, store.SecretScopeUser) + } + if r.ScopeID != string(f.userID) { + t.Errorf("scope_id = %q, want the caller %q", r.ScopeID, f.userID) + } + } + if found != 2 { + t.Fatalf("resolved %d of the 2 written names for the owning agent; got %d record(s)", found, len(recs)) + } +} + +// A plain user may not write the shared tenant coordinate (D8's matrix), on +// either verb. Without this the selector would be a request field any caller +// could use to overwrite every other user's value. +func TestTenantScopeWriteRequiresAdmin(t *testing.T) { + f := newSecretsFixture(t) + ctx := context.Background() + + _, err := f.client.SetSecret(ctx, scopedSetReq(f.userToken, "DB_URL", "v", compassv1.SecretScope_SECRET_SCOPE_TENANT)) + if got := connect.CodeOf(err); got != connect.CodePermissionDenied { + t.Fatalf("SetSecret tenant scope as member: code = %v, want PermissionDenied (err %v)", got, err) + } + _, err = f.client.DeleteSecret(ctx, scopedDelReq(f.userToken, "DB_URL", compassv1.SecretScope_SECRET_SCOPE_TENANT)) + if got := connect.CodeOf(err); got != connect.CodePermissionDenied { + t.Fatalf("DeleteSecret tenant scope as member: code = %v, want PermissionDenied (err %v)", got, err) + } + + if _, err := f.client.SetSecret(ctx, scopedSetReq(f.adminToken, "DB_URL", "v", compassv1.SecretScope_SECRET_SCOPE_TENANT)); err != nil { + t.Fatalf("SetSecret tenant scope as admin: %v", err) + } +} + +// The isolation property that motivated D9: a user-scoped value is private to its +// owner's agents, while a tenant row stays shared. Asserted through the real +// resolution query, not by reading the row back by primary key. +func TestUserScopedSecretIsNotVisibleToAnotherUsersAgent(t *testing.T) { + f := newSecretsFixture(t) + ctx := context.Background() + + other, err := f.st.CreateUser(ctx, store.NewUser{Handle: "other", DisplayName: "other"}) + if err != nil { + t.Fatalf("CreateUser(other): %v", err) + } + otherAgent, err := f.st.CreateAgent(ctx, other.ID, store.NewAgent{Handle: "otheragent", DisplayName: "otheragent"}) + if err != nil { + t.Fatalf("CreateAgent(other): %v", err) + } + + if _, err := f.client.SetSecret(ctx, setReq(f.userToken, "PRIVATE_ONE", "v")); err != nil { + t.Fatalf("SetSecret(user scope): %v", err) + } + if _, err := f.client.SetSecret(ctx, scopedSetReq(f.adminToken, "SHARED_ONE", "v", compassv1.SecretScope_SECRET_SCOPE_TENANT)); err != nil { + t.Fatalf("SetSecret(tenant scope): %v", err) + } + + names := resolvedNames(t, ctx, f, otherAgent.ID) + if names["PRIVATE_ONE"] { + t.Errorf("another user's agent resolved PRIVATE_ONE; user-scoped rows must not leak across users") + } + if !names["SHARED_ONE"] { + t.Errorf("another user's agent did NOT resolve the tenant-scoped SHARED_ONE; tenant rows stay shared") + } +} + +// Re-setting a name that already has a tenant row writes a NEW user row and does +// NOT retire the shared one — it keeps resolving for every other user until an +// admin deletes it at tenant scope. Pins the real behavior against the intuition +// that a re-set privatizes a secret (record D9). +func TestUserScopeResetDoesNotRetireTheTenantRow(t *testing.T) { + f := newSecretsFixture(t) + ctx := context.Background() + + other, err := f.st.CreateUser(ctx, store.NewUser{Handle: "other", DisplayName: "other"}) + if err != nil { + t.Fatalf("CreateUser(other): %v", err) + } + otherAgent, err := f.st.CreateAgent(ctx, other.ID, store.NewAgent{Handle: "otheragent", DisplayName: "otheragent"}) + if err != nil { + t.Fatalf("CreateAgent(other): %v", err) + } + + if _, err := f.client.SetSecret(ctx, scopedSetReq(f.adminToken, "DB_URL", "shared", compassv1.SecretScope_SECRET_SCOPE_TENANT)); err != nil { + t.Fatalf("SetSecret(tenant): %v", err) + } + if _, err := f.client.SetSecret(ctx, setReq(f.userToken, "DB_URL", "private")); err != nil { + t.Fatalf("SetSecret(user re-set): %v", err) + } + + if !resolvedNames(t, ctx, f, otherAgent.ID)["DB_URL"] { + t.Errorf("the tenant row stopped resolving for another user after a user-scope re-set; a re-set must not retire the shared value") + } + recs, err := f.st.SecretRecordsForAgent(ctx, f.agentID) + if err != nil { + t.Fatalf("SecretRecordsForAgent: %v", err) + } + for _, r := range recs { + if r.Name == "DB_URL" && r.ScopeKind != store.SecretScopeUser { + t.Errorf("the setter's own agent resolved scope_kind %d, want %d (its private row shadows the tenant one)", r.ScopeKind, store.SecretScopeUser) + } + } +} + +// TestTwoUsersSameNameDoNotClobber is the T5 cutover's headline regression: the +// value path is now scope-keyed, so two different users setting the SAME name at +// (default) user scope each keep their OWN value. Under the pre-cutover +// name-keyed write, user B's Set overwrote user A's value — this asserts A's +// value survives B's write, resolved back through each owner's agent. +func TestTwoUsersSameNameDoNotClobber(t *testing.T) { + f := newSecretsFixture(t) + ctx := context.Background() + + userB, err := f.st.CreateUser(ctx, store.NewUser{Handle: "userb", DisplayName: "userb"}) + if err != nil { + t.Fatalf("CreateUser(b): %v", err) + } + agentB, err := f.st.CreateAgent(ctx, userB.ID, store.NewAgent{Handle: "agentb", DisplayName: "agentb"}) + if err != nil { + t.Fatalf("CreateAgent(b): %v", err) + } + tokB, err := auth.IssueAccountToken(ctx, f.st, userB.ID) + if err != nil { + t.Fatalf("IssueAccountToken(b): %v", err) + } + + // A writes DB_URL, then B writes DB_URL — same name, different callers. + if _, err := f.client.SetSecret(ctx, setReq(f.userToken, "DB_URL", "a-value")); err != nil { + t.Fatalf("SetSecret(a): %v", err) + } + if _, err := f.client.SetSecret(ctx, setReq(tokB, "DB_URL", "b-value")); err != nil { + t.Fatalf("SetSecret(b): %v", err) + } + + // Each owner's agent resolves ITS user's value — B's write did not clobber A. + if got := resolvedValues(t, ctx, f, f.agentID)["DB_URL"]; got != "a-value" { + t.Errorf("user A's agent resolved DB_URL = %q, want a-value (B's write clobbered A)", got) + } + if got := resolvedValues(t, ctx, f, agentB.ID)["DB_URL"]; got != "b-value" { + t.Errorf("user B's agent resolved DB_URL = %q, want b-value", got) + } +} + +func resolvedNames(t *testing.T, ctx context.Context, f secretsFixture, agent store.AccountID) map[string]bool { + t.Helper() + recs, err := f.st.SecretRecordsForAgent(ctx, agent) + if err != nil { + t.Fatalf("SecretRecordsForAgent: %v", err) + } + out := make(map[string]bool, len(recs)) + for _, r := range recs { + out[r.Name] = true + } + return out +} + +// resolvedValues resolves an agent's visible secrets through the production +// StoreResolver.ResolveFor (the same read the Runner's FetchSecrets drives) and +// returns name→DECRYPTED value. It proves a write both landed AND is decryptable +// under the fixture key — a stronger check than reading the row by primary key. +func resolvedValues(t *testing.T, ctx context.Context, f secretsFixture, agent store.AccountID) map[string]string { + t.Helper() + resolved, err := f.resolver.ResolveFor(ctx, agent, "test resolve") + if err != nil { + t.Fatalf("ResolveFor: %v", err) + } + out := make(map[string]string, len(resolved)) + for _, r := range resolved { + out[r.Name] = r.Value + } + return out +} diff --git a/go/server/serve.go b/go/server/serve.go index 0516abef1..0fc731051 100644 --- a/go/server/serve.go +++ b/go/server/serve.go @@ -122,6 +122,14 @@ type ServeConfig struct { // the bootstrap installs no provider and the RPC interceptors are inert // no-ops (no active span, so no traceresponse header). OtelEndpoint string + // SecretProvider is the secretspec provider URI both secret resolvers resolve + // through (e.g. "keyring://", "onepassword://Production", "dotenv://path"). + // Empty uses secretspec's default provider chain — today's behavior, so an + // existing deployment is unchanged. The CLI supplies it (flag + // --secret-provider / $COMPASS_SECRET_PROVIDER). It is the ONE knob that + // points the whole deployment's custody — including the master key — at a + // managed store, per the record's A2 KMS-by-provider-URI custody note. + SecretProvider string } // ForgeConfig configures the board webhook-ingestion lane (RIG-2883) and the @@ -353,7 +361,7 @@ const initialKeyVersion int16 = 1 // It NEVER generates a key (operator-seeded custody, DL-355) and NEVER echoes // the value or any part of it in an error — a wrong length or non-hex value is // reported by what was expected, not by what was found. -func resolveMasterKey(ctx context.Context, st *store.Store, server secrets.Resolver) (envelope.Key, int16, error) { //nolint:unparam // st is nil only in the DB-free decode/fail-closed unit tests; the pgtest lane and the boot caller pass a real store. +func resolveMasterKey(ctx context.Context, st *store.Store, server secrets.Resolver) (envelope.Key, int16, error) { resolved, err := server.Resolve(ctx, "master key resolve") if err != nil { return envelope.Key{}, 0, fmt.Errorf("resolve master key: %w", err) @@ -435,27 +443,50 @@ func reconcileKeyState(ctx context.Context, st *store.Store, key envelope.Key) ( return key, state.KeyVersion, nil } -// buildSecretResolvers constructs the TWO SpecResolver instances the Server -// runs, returning (container, server) in that order. +// buildServerSecretResolver constructs the read-only SERVER-secret resolver: a +// SpecResolver whose manifest is built from the SEPARATE server_secrets registry +// (the ServerDeclaredSecrets view), resolving through cfg.SecretProvider. It is +// the surface resolveMasterKey and the forge-secret consumers read, and the +// value-free Statuses probe ListServerSecrets uses. Its own state dir keeps its +// manifest off the (now DB-backed) user path's disk. An empty provider uses +// secretspec's default chain — today's behavior. // -// The first reads the store's user names registry and is the single place -// SecretSpec runs for container delivery — the RunnerService FetchSecrets -// handler and the user SecretsService write path both delegate to it. Its state -// dir is a "secrets" subdirectory of the state dir the bootstrap-admin token is -// written under; NewSpecResolver creates it 0700 if absent. -// -// The second has the same project and profile but builds its manifest from the -// SEPARATE server_secrets registry, via the ServerDeclaredSecrets view. Two -// instances rather than one filtered instance: the container path keeps reading -// the user registry, so a server secret cannot be delivered into an agent -// container even if a future caller forgets a filter. Its own state dir keeps -// the two manifests from overwriting each other on disk. -func buildSecretResolvers(st *store.Store, cfg ServeConfig) (container, server secrets.Resolver) { - return secrets.NewSpecResolver(st, secretsStateDir(cfg)), - secrets.NewSpecResolver( - store.ServerDeclaredSecrets{Store: st}, - filepath.Join(secretsStateDir(cfg), "server"), - ) +// The user/container side is no longer a SpecResolver: it is the DB-backed +// StoreResolver (record A4), built in Serve from the master key this resolver +// yields, so a server secret cannot be delivered into an agent container by +// construction — the two paths read different tables. +func buildServerSecretResolver(st *store.Store, cfg ServeConfig) secrets.Resolver { + opts := []secrets.SpecOption{} + if cfg.SecretProvider != "" { + opts = append(opts, secrets.WithProvider(cfg.SecretProvider)) + } + return secrets.NewSpecResolver( + store.ServerDeclaredSecrets{Store: st}, + filepath.Join(secretsStateDir(cfg), "server"), + opts..., + ) +} + +// buildUserSecretResolver declares the server-secret names (including the +// reserved master-key name, without which resolveMasterKey reads a provisioned +// key as absent), resolves the master key, and returns the DB-backed user-secret +// resolver built from it. Fail-closed per DL-355: an absent, empty, wrong-length +// or non-hex key aborts startup with the provisioning runbook, and boot NEVER +// generates one — nothing decrypts a stored value until this succeeds. +func buildUserSecretResolver( + ctx context.Context, st *store.Store, cfg ServeConfig, serverResolver secrets.Resolver, +) (*secrets.StoreResolver, error) { + if err := declareServerSecretNames(ctx, st, cfg); err != nil { + return nil, err + } + if err := st.DeclareServerSecret(ctx, "", store.MasterKeyName); err != nil && !errors.Is(err, store.ErrConflict) { + return nil, fmt.Errorf("declaring master key name: %w", err) + } + masterKey, keyVersion, err := resolveMasterKey(ctx, st, serverResolver) + if err != nil { + return nil, err + } + return secrets.NewStoreResolver(st, masterKey, keyVersion), nil } // declareServerSecretNames declares the six forge secret NAMEs into @@ -757,12 +788,9 @@ func Serve(ctx context.Context, cfg ServeConfig) error { // before serving; the store invokes it on its own tx. commsSvc.RegisterCoordinationHook(st) - resolver, serverResolver := buildSecretResolvers(st, cfg) - - // Declare the six forge secret NAMEs into server_secrets before any - // consumer resolves them: the re-pointed consumers read the SERVER - // registry, and an empty registry short-circuits Resolve to (nil, nil). - if err := declareServerSecretNames(ctx, st, cfg); err != nil { + serverResolver := buildServerSecretResolver(st, cfg) + resolver, err := buildUserSecretResolver(ctx, st, cfg, serverResolver) + if err != nil { return failStartup(udsListener, listeners, err) } @@ -895,13 +923,14 @@ type serveDoors struct { uds *http.Server dev *http.Server net *http.Server - // netResolver is the resolver INSTANCE threaded to the net door, i.e. the - // one runnerhub's FetchSecrets delivers from. It must always be the - // CONTAINER instance; recorded because buildNetworkServer resolves nothing - // at build time, so the wiring is otherwise unobservable and a swap to the - // server instance would silently deliver every deployment secret into every - // agent container. Asserted by the buildDoors routing test. - netResolver secrets.Resolver + // netResolver is the user-secret resolver INSTANCE threaded to the net door, + // i.e. the one runnerhub's FetchSecrets delivers from. It must always be the + // CONTAINER instance (the DB-backed StoreResolver reading `secrets`); recorded + // because buildNetworkServer resolves nothing at build time, so the wiring is + // otherwise unobservable and a swap to the server instance would silently + // deliver every deployment secret into every agent container. Asserted by the + // buildDoors routing test. + netResolver *secrets.StoreResolver // linearNotify is the Linear agent-notification lane (RIG-2732 T7), built // beside the webhook handler it feeds; nil when Linear is not configured (its // client-credentials pair undeclared). Serve starts its arm + reconciler on @@ -924,12 +953,12 @@ func buildDoors( hub *runnerhub.Hub, st *store.Store, adminID store.AccountID, - // resolver is the CONTAINER instance (reads `secrets`), threaded to - // buildNetworkServer -> the FetchSecrets delivery path. serverResolver reads - // `server_secrets` and is threaded ONLY to the Linear webhook wiring. Do not - // collapse these into one parameter: that is how every server secret ends up - // delivered into every agent container. - resolver secrets.Resolver, + // resolver is the CONTAINER user-secret resolver (the DB-backed StoreResolver + // reading `secrets`), threaded to buildNetworkServer -> the FetchSecrets + // delivery path. serverResolver reads `server_secrets` and is threaded ONLY to + // the Linear webhook wiring. Do not collapse these: that is how every server + // secret ends up delivered into every agent container. + resolver *secrets.StoreResolver, serverResolver secrets.Resolver, devListener net.Listener, netListener net.Listener, diff --git a/go/server/serve_forge_pgtest_test.go b/go/server/serve_forge_pgtest_test.go index 9913bba8b..4737089af 100644 --- a/go/server/serve_forge_pgtest_test.go +++ b/go/server/serve_forge_pgtest_test.go @@ -499,10 +499,12 @@ func TestBuildDoorsRoutesTheResolverInstancesOverTheRealCallGraph(t *testing.T) ctx := context.Background() st := forgeTestStore(t) - // Distinct, non-overlapping sets: only the server fake carries the - // SERVER_-prefixed webhook secret the Linear wiring needs, so a handler - // exists if and only if that instance was the one threaded there. - container := &fakeResolver{resolved: []secrets.ResolvedSecret{{Name: "USER_ONLY", Value: "u"}}} + // The container instance is the real DB-backed StoreResolver (T5); the server + // instance is a fake carrying ONLY the SERVER_-prefixed webhook secret the + // Linear wiring needs, so a handler exists if and only if that instance was + // threaded there. Pointer identity of the container instance is what the D6 + // assertion below turns on. + container := secrets.NewStoreResolver(st, secretsFixtureKey(t), 1) server := &fakeResolver{resolved: []secrets.ResolvedSecret{ {Name: serverSecretName("LWH"), Value: "shh"}, }} @@ -565,9 +567,7 @@ func TestBuildDoorsRoutesTheResolverInstancesOverTheRealCallGraph(t *testing.T) // D6: the net door — runnerhub's FetchSecrets delivery path — must have // received the CONTAINER instance. Pointer identity is the assertion: this // is the swap that leaks every deployment secret into every agent container, - // and buildNetworkServer resolves nothing at build time, so nothing else - // about the built door reveals which instance it holds. - if doors.netResolver != secrets.Resolver(container) { + if doors.netResolver != container { t.Fatal("net door did not receive the CONTAINER resolver: runnerhub FetchSecrets would serve server_secrets, delivering every deployment secret (App PEMs, webhook secrets, Linear credentials) into every agent container") } diff --git a/go/server/serve_pgtest_test.go b/go/server/serve_pgtest_test.go index 02d7b306c..c0d9fa876 100644 --- a/go/server/serve_pgtest_test.go +++ b/go/server/serve_pgtest_test.go @@ -32,12 +32,14 @@ func TestServeBindsSocketServesClientAndCleansUpOnCancel(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) errCh := make(chan error, 1) + cfg := ServeConfig{ + SocketPath: socketPath, + Version: "serve-test", + DatabaseDSN: pgtest.RequireDSN(t), //nolint:contextcheck // RequireDSN is a shared test helper; ctx-threading is tracked separately + } + provisionMasterKeyProvider(t, &cfg) go func() { - errCh <- Serve(ctx, ServeConfig{ - SocketPath: socketPath, - Version: "serve-test", - DatabaseDSN: pgtest.RequireDSN(t), //nolint:contextcheck // RequireDSN is a shared test helper; ctx-threading is tracked separately - }) + errCh <- Serve(ctx, cfg) }() // Event-gate on the socket being bound, then assert its mode and that a real @@ -97,12 +99,14 @@ func TestServeShutdownIsClean(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) errCh := make(chan error, 1) + cfg := ServeConfig{ + SocketPath: socketPath, + Version: "serve-test", + DatabaseDSN: pgtest.RequireDSN(t), //nolint:contextcheck // RequireDSN is a shared test helper; ctx-threading is tracked separately + } + provisionMasterKeyProvider(t, &cfg) go func() { - errCh <- Serve(ctx, ServeConfig{ - SocketPath: socketPath, - Version: "serve-test", - DatabaseDSN: pgtest.RequireDSN(t), //nolint:contextcheck // RequireDSN is a shared test helper; ctx-threading is tracked separately - }) + errCh <- Serve(ctx, cfg) }() // Gate on a SERVED RPC, not just the socket being connectable: Serve binds @@ -156,12 +160,14 @@ func TestServeShutdownWithLiveCommsSubscriberReturnsClean(t *testing.T) { // pool for the remainder of the binary. defer cancel() errCh := make(chan error, 1) + cfg := ServeConfig{ + SocketPath: socketPath, + Version: "serve-test", + DatabaseDSN: pgtest.RequireDSN(t), //nolint:contextcheck // RequireDSN is a shared test helper; ctx-threading is tracked separately + } + provisionMasterKeyProvider(t, &cfg) go func() { - errCh <- Serve(serveCtx, ServeConfig{ - SocketPath: socketPath, - Version: "serve-test", - DatabaseDSN: pgtest.RequireDSN(t), //nolint:contextcheck // RequireDSN is a shared test helper; ctx-threading is tracked separately - }) + errCh <- Serve(serveCtx, cfg) }() waitListening(t, socketPath) diff --git a/packages/compass-agent/src/gen/compass/v1/compass_pb.ts b/packages/compass-agent/src/gen/compass/v1/compass_pb.ts index cc0b54ac6..b0c7bf4d6 100644 --- a/packages/compass-agent/src/gen/compass/v1/compass_pb.ts +++ b/packages/compass-agent/src/gen/compass/v1/compass_pb.ts @@ -20,7 +20,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file compass/v1/compass.proto. */ export const file_compass_v1_compass: GenFile = /*@__PURE__*/ - fileDesc("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEiqAEKEFNldFNlY3JldFJlcXVlc3QSDAoEbmFtZRgBIAEoCRISCgV2YWx1ZRgCIAEoCUIDgAEBEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkiEwoRU2V0U2VjcmV0UmVzcG9uc2UiFAoSTGlzdFNlY3JldHNSZXF1ZXN0IkAKE0xpc3RTZWNyZXRzUmVzcG9uc2USKQoHc2VjcmV0cxgBIAMoCzIYLmNvbXBhc3MudjEuU2VjcmV0U3RhdHVzIqABCgxTZWNyZXRTdGF0dXMSDAoEbmFtZRgBIAEoCRIOCgZpc19zZXQYAiABKAgSLAoIZGVsaXZlcnkYAyABKA4yGi5jb21wYXNzLnYxLlNlY3JldERlbGl2ZXJ5EiQKBGtpbmQYBCABKA4yFi5jb21wYXNzLnYxLlNlY3JldEtpbmQSEAoIcHJvdmlkZXIYBSABKAkSDAoEaG9zdBgGIAEoCSIjChNEZWxldGVTZWNyZXRSZXF1ZXN0EgwKBG5hbWUYASABKAkiFgoURGVsZXRlU2VjcmV0UmVzcG9uc2UiOgoWU2V0U2VydmVyU2VjcmV0UmVxdWVzdBIMCgRuYW1lGAEgASgJEhIKBXZhbHVlGAIgASgJQgOAAQEiGQoXU2V0U2VydmVyU2VjcmV0UmVzcG9uc2UiKQoZRGVsZXRlU2VydmVyU2VjcmV0UmVxdWVzdBIMCgRuYW1lGAEgASgJIhwKGkRlbGV0ZVNlcnZlclNlY3JldFJlc3BvbnNlIhoKGExpc3RTZXJ2ZXJTZWNyZXRzUmVxdWVzdCJTChlMaXN0U2VydmVyU2VjcmV0c1Jlc3BvbnNlEjYKDnNlcnZlcl9zZWNyZXRzGAEgAygLMh4uY29tcGFzcy52MS5TZXJ2ZXJTZWNyZXRTdGF0dXMiMgoSU2VydmVyU2VjcmV0U3RhdHVzEgwKBG5hbWUYASABKAkSDgoGaXNfc2V0GAIgASgIIhYKFEdldFNlcnZlckluZm9SZXF1ZXN0Ij0KFUdldFNlcnZlckluZm9SZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJEhMKC2FwaV92ZXJzaW9uGAIgASgJIg8KDVdob0FtSVJlcXVlc3QiJAoOV2hvQW1JUmVzcG9uc2USEgoKYWNjb3VudF9pZBgBIAEoCSJDChZTdWJzY3JpYmVFdmVudHNSZXF1ZXN0EhEKCXNpbmNlX3NlcRgBIAEoBBIWCg5pbnN0YW5jZV9lcG9jaBgCIAEoBCLiAwoXU3Vic2NyaWJlRXZlbnRzUmVzcG9uc2USCwoDc2VxGAEgASgEEhIKCmF0X3VuaXhfbXMYAiABKAMSFgoOaW5zdGFuY2VfZXBvY2gYAyABKAQSFAoMc25hcHNob3Rfc2VxGAQgASgEEjEKDXNlcnZlcl9zdGF0dXMYCiABKAsyGC5jb21wYXNzLnYxLlNlcnZlclN0YXR1c0gAEjUKD3Jlc3luY19yZXF1aXJlZBgLIAEoCzIaLmNvbXBhc3MudjEuUmVzeW5jUmVxdWlyZWRIABI+ChRhZ2VudF9zZXNzaW9uX3N0YXR1cxgMIAEoCzIeLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdHVzSAASPAoTYWdlbnRfbWVzc2FnZV9jaHVuaxgNIAEoCzIdLmNvbXBhc3MudjEuQWdlbnRNZXNzYWdlQ2h1bmtIABI0Cg9hZ2VudF90b29sX2NhbGwYDiABKAsyGS5jb21wYXNzLnYxLkFnZW50VG9vbENhbGxIABIrCgphZ2VudF9wbGFuGA8gASgLMhUuY29tcGFzcy52MS5BZ2VudFBsYW5IABIiCgVpc3N1ZRgQIAEoCzIRLmNvbXBhc3MudjEuSXNzdWVIAEIJCgdwYXlsb2FkIi4KFkxpc3RCb2FyZElzc3Vlc1JlcXVlc3QSFAoMc25hcHNob3Rfc2VxGAEgASgEIjwKF0xpc3RCb2FyZElzc3Vlc1Jlc3BvbnNlEiEKBmlzc3VlcxgBIAMoCzIRLmNvbXBhc3MudjEuSXNzdWUiNgoMU2VydmVyU3RhdHVzEiYKBXN0YXRlGAEgASgOMhcuY29tcGFzcy52MS5TZXJ2ZXJTdGF0ZSIQCg5SZXN5bmNSZXF1aXJlZCLSAQoSQWdlbnRTZXNzaW9uU3RhdHVzEhIKCnNlc3Npb25faWQYASABKAkSLAoFc3RhdGUYAiABKA4yHS5jb21wYXNzLnYxLkFnZW50U2Vzc2lvblN0YXRlEhgKEGFnZW50X2FjY291bnRfaWQYAyABKAkSLQoMcnVudGltZV90aWVyGAQgASgOMhcuY29tcGFzcy52MS5SdW50aW1lVGllchIxCg5lZ3Jlc3NfcG9zdHVyZRgFIAEoDjIZLmNvbXBhc3MudjEuRWdyZXNzUG9zdHVyZSJJChFBZ2VudE1lc3NhZ2VDaHVuaxISCgpzZXNzaW9uX2lkGAEgASgJEgwKBHRleHQYAiABKAkSEgoKaXNfdGhvdWdodBgDIAEoCCJ5Cg1BZ2VudFRvb2xDYWxsEhIKCnNlc3Npb25faWQYASABKAkSFAoMdG9vbF9jYWxsX2lkGAIgASgJEg0KBXRpdGxlGAMgASgJEi8KBnN0YXR1cxgEIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cyJMCglBZ2VudFBsYW4SEgoKc2Vzc2lvbl9pZBgBIAEoCRIrCgdlbnRyaWVzGAIgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSJTCg5BZ2VudFBsYW5FbnRyeRIPCgdjb250ZW50GAEgASgJEjAKBnN0YXR1cxgCIAEoDjIgLmNvbXBhc3MudjEuQWdlbnRQbGFuRW50cnlTdGF0dXMi3wMKDFNlc3Npb25FdmVudBIQCghldmVudF9pZBgBIAEoCRISCgphdF91bml4X21zGAIgASgDEjoKDmFzc2lzdGFudF90ZXh0GAMgASgLMiAuY29tcGFzcy52MS5TZXNzaW9uQXNzaXN0YW50VGV4dEgAEi8KCHRoaW5raW5nGAQgASgLMhsuY29tcGFzcy52MS5TZXNzaW9uVGhpbmtpbmdIABIwCgl0b29sX2NhbGwYBSABKAsyGy5jb21wYXNzLnYxLlNlc3Npb25Ub29sQ2FsbEgAEj0KEHRvb2xfY2FsbF91cGRhdGUYBiABKAsyIS5jb21wYXNzLnYxLlNlc3Npb25Ub29sQ2FsbFVwZGF0ZUgAEicKBHBsYW4YByABKAsyFy5jb21wYXNzLnYxLlNlc3Npb25QbGFuSAASKwoGbm90aWNlGAggASgLMhkuY29tcGFzcy52MS5TZXNzaW9uTm90aWNlSAASOQoRc2Vzc2lvbl9pbmplY3Rpb24YCSABKAsyHC5jb21wYXNzLnYxLlNlc3Npb25JbmplY3Rpb25IABIxCg1zZXNzaW9uX2Vycm9yGAogASgLMhguY29tcGFzcy52MS5TZXNzaW9uRXJyb3JIAEIHCgVldmVudCI4ChRTZXNzaW9uQXNzaXN0YW50VGV4dBIMCgR0ZXh0GAEgASgJEhIKCm1lc3NhZ2VfaWQYAiABKAkiMwoPU2Vzc2lvblRoaW5raW5nEgwKBHRleHQYASABKAkSEgoKbWVzc2FnZV9pZBgCIAEoCSJnCg9TZXNzaW9uVG9vbENhbGwSFAoMdG9vbF9jYWxsX2lkGAEgASgJEg0KBXRpdGxlGAIgASgJEi8KBnN0YXR1cxgDIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cyKaAQoVU2Vzc2lvblRvb2xDYWxsVXBkYXRlEhQKDHRvb2xfY2FsbF9pZBgBIAEoCRIvCgZzdGF0dXMYAiABKA4yHy5jb21wYXNzLnYxLkFnZW50VG9vbENhbGxTdGF0dXMSDgoGb3V0cHV0GAMgASgJEioKBWRpZmZzGAQgAygLMhsuY29tcGFzcy52MS5TZXNzaW9uRmlsZURpZmYiVQoPU2Vzc2lvbkZpbGVEaWZmEgwKBHBhdGgYASABKAkSFQoIb2xkX3RleHQYAiABKAlIAIgBARIQCghuZXdfdGV4dBgDIAEoCUILCglfb2xkX3RleHQiOgoLU2Vzc2lvblBsYW4SKwoHZW50cmllcxgBIAMoCzIaLmNvbXBhc3MudjEuQWdlbnRQbGFuRW50cnkiOQoNU2Vzc2lvbk5vdGljZRIMCgR0ZXh0GAEgASgJEhEKBGxpbmsYAiABKAlIAIgBAUIHCgVfbGluayKDAQoQU2Vzc2lvbkluamVjdGlvbhIxCgdvcF9raW5kGAEgASgOMiAuY29tcGFzcy52MS5TZXNzaW9uSW5qZWN0aW9uS2luZBISCgptZXNzYWdlX2lkGAIgASgJEhMKC2Zyb21faGFuZGxlGAMgASgJEhMKC3RyYWNlcGFyZW50GAQgASgJImsKDFNlc3Npb25FcnJvchIqCgRraW5kGAEgASgOMhwuY29tcGFzcy52MS5TZXNzaW9uRXJyb3JLaW5kEg8KB21lc3NhZ2UYAiABKAkSEwoGc3RhdHVzGAMgASgFSACIAQFCCQoHX3N0YXR1cyIyChxTdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkifgoRQWdlbnRTZXNzaW9uRnJhbWUSEgoKc2Vzc2lvbl9pZBgBIAEoCRInCgVldmVudBgCIAEoCzIYLmNvbXBhc3MudjEuU2Vzc2lvbkV2ZW50EiwKBXN0YXRlGAMgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZSJwCh5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJEg8KB3BlcnNvbmEYAyABKAkSDAoEcm9sZRgEIAEoCSI5Ch9Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEhYKDmNvbnRhaW5lcl9uYW1lGAEgASgJIlAKG1JlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgCIAEoCSIeChxSZW1vdmVBZ2VudFdvcmtzcGFjZVJlc3BvbnNlImMKGFN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFyZXN1bWVfc2Vzc2lvbl9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiLwoZU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIloKEVNwYXduQWdlbnRSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiQAoSU3Bhd25BZ2VudFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSFgoOY29udGFpbmVyX25hbWUYAiABKAkiLQoXU3RvcEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIaChhTdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2UiLwoZUmVsb2FkQWdlbnRTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIjAKGlJlbG9hZEFnZW50U2Vzc2lvblJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkiKwoVR2V0QWdlbnRTdGF0dXNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiSgoWR2V0QWdlbnRTdGF0dXNSZXNwb25zZRIwCghzdGF0dXNlcxgBIAMoCzIeLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdHVzIisKEUlzc3VlVG9rZW5SZXF1ZXN0EhYKDmFjY291bnRfaGFuZGxlGAEgASgJIiMKEklzc3VlVG9rZW5SZXNwb25zZRINCgV0b2tlbhgBIAEoCSIoChJSZXZva2VUb2tlblJlcXVlc3QSEgoFdG9rZW4YASABKAlCA4ABASIVChNSZXZva2VUb2tlblJlc3BvbnNlIicKFVB1dEFnZW50Q29uZmlnUmVxdWVzdBIOCgZidW5kbGUYASABKAwiKQoWUHV0QWdlbnRDb25maWdSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJIhsKGUdldEFnZW50Q29uZmlnSW5mb1JlcXVlc3Qi2gEKGkdldEFnZW50Q29uZmlnSW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSDgoGc2tpbGxzGAIgAygJEhIKCmV4dGVuc2lvbnMYAyADKAkSEwoLbWNwX3NlcnZlcnMYBCADKAkSFAoMaGFzX3NldHRpbmdzGAUgASgIEhUKDWhhc19hZ2VudHNfbWQYBiABKAgSDQoFcnVsZXMYByADKAkSEQoJc3ViYWdlbnRzGAggAygJEhIKCmhhc19tb2RlbHMYCSABKAgSDwoHcHJvbXB0cxgKIAMoCSIaChhEZWxldGVBZ2VudENvbmZpZ1JlcXVlc3QiGwoZRGVsZXRlQWdlbnRDb25maWdSZXNwb25zZSI0Cg5Nb2RlbENhbmRpZGF0ZRIQCghwcm92aWRlchgBIAEoCRIQCghtb2RlbF9pZBgCIAEoCSJxCg1Nb2RlbE1ldGFkYXRhEhYKDmNvbnRleHRfd2luZG93GAEgASgDEhwKFGlucHV0X2Nvc3RfbWljcm9fdXNkGAIgASgDEh0KFW91dHB1dF9jb3N0X21pY3JvX3VzZBgDIAEoAxILCgNhcGkYBCABKAkihwEKEk1vZGVsUmVnaXN0cnlFbnRyeRIUCgxkaXNwbGF5X25hbWUYASABKAkSLgoKY2FuZGlkYXRlcxgCIAMoCzIaLmNvbXBhc3MudjEuTW9kZWxDYW5kaWRhdGUSKwoIbWV0YWRhdGEYAyABKAsyGS5jb21wYXNzLnYxLk1vZGVsTWV0YWRhdGEimAEKDU1vZGVsUmVnaXN0cnkSNwoHZW50cmllcxgBIAMoCzImLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeS5FbnRyaWVzRW50cnkaTgoMRW50cmllc0VudHJ5EgsKA2tleRgBIAEoCRItCgV2YWx1ZRgCIAEoCzIeLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeUVudHJ5OgI4ASJgChdQdXRNb2RlbFJlZ2lzdHJ5UmVxdWVzdBIrCghyZWdpc3RyeRgBIAEoCzIZLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeRIYChBleHBlY3RlZF92ZXJzaW9uGAIgASgDIisKGFB1dE1vZGVsUmVnaXN0cnlSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgDIhkKF0dldE1vZGVsUmVnaXN0cnlSZXF1ZXN0IlgKGEdldE1vZGVsUmVnaXN0cnlSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgDEisKCHJlZ2lzdHJ5GAIgASgLMhkuY29tcGFzcy52MS5Nb2RlbFJlZ2lzdHJ5IhwKGkRlbGV0ZU1vZGVsUmVnaXN0cnlSZXF1ZXN0Ih0KG0RlbGV0ZU1vZGVsUmVnaXN0cnlSZXNwb25zZSI+ChBBZ2VudEF0dHJpYnV0aW9uEhQKDGFnZW50X2hhbmRsZRgBIAEoCRIUCgxvd25lcl9oYW5kbGUYAiABKAkiRQoIRm9yZ2VSZWYSKwoIcHJvdmlkZXIYASABKA4yGS5jb21wYXNzLnYxLkZvcmdlUHJvdmlkZXISDAoEaG9zdBgCIAEoCSLUAwoFSXNzdWUSCgoCaWQYASABKAkSIwoFZm9yZ2UYAiABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAyABKAkSDgoGbnVtYmVyGAQgASgNEg0KBXRpdGxlGAUgASgJEgwKBGJvZHkYBiABKAkSEwoLZm9yZ2Vfc3RhdGUYByABKAkSCwoDdXJsGAggASgJEisKBWFnZW50GAkgASgLMhwuY29tcGFzcy52MS5BZ2VudEF0dHJpYnV0aW9uEhUKDWZvcmdlX2FjY291bnQYCiABKAkSDgoGbGFiZWxzGAsgAygJEi4KCnVwZGF0ZWRfYXQYEyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEiUKBXN0YXRlGAwgASgOMhYuY29tcGFzcy52MS5Jc3N1ZVN0YXRlEhAKCHByaW9yaXR5GA0gASgJEhAKCGFzc2lnbmVlGA4gASgJEg8KB3N1bW1hcnkYDyABKAkSDgoGYnJhbmNoGBAgASgJEiQKA3BycxgRIAMoCzIXLmNvbXBhc3MudjEuUHVsbFJlcXVlc3QSJwoHdHJhY2tlchgSIAEoCzIWLmNvbXBhc3MudjEuVHJhY2tlclJlZiKeAwoLUHVsbFJlcXVlc3QSIwoFZm9yZ2UYASABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAiABKAkSDgoGbnVtYmVyGAMgASgNEg0KBXRpdGxlGAQgASgJEhMKC2ZvcmdlX3N0YXRlGAUgASgJEgsKA3VybBgGIAEoCRIQCghoZWFkX3JlZhgHIAEoCRIQCghiYXNlX3JlZhgIIAEoCRIrCgVhZ2VudBgJIAEoCzIcLmNvbXBhc3MudjEuQWdlbnRBdHRyaWJ1dGlvbhIVCg1mb3JnZV9hY2NvdW50GAogASgJEg0KBWRyYWZ0GAsgASgIEikKB2NoYW5nZWQYDCABKAsyGC5jb21wYXNzLnYxLkNoYW5nZWRTdGF0cxIpCgZjaGVja3MYDSABKAsyGS5jb21wYXNzLnYxLkNoZWNrc1N1bW1hcnkSIwoHcmV2aWV3cxgOIAMoCzISLmNvbXBhc3MudjEuUmV2aWV3EikKB3RocmVhZHMYDyADKAsyGC5jb21wYXNzLnYxLlJldmlld1RocmVhZCJTCg1DaGVja3NTdW1tYXJ5EhAKCGhlYWRfc2hhGAEgASgJEg0KBXN0YXRlGAIgASgJEiEKBmNoZWNrcxgDIAMoCzIRLmNvbXBhc3MudjEuQ2hlY2siQwoFQ2hlY2sSDAoEbmFtZRgBIAEoCRINCgVzdGF0ZRgCIAEoCRILCgN1cmwYAyABKAkSEAoIcmVxdWlyZWQYBCABKAgiQwoMQ2hhbmdlZFN0YXRzEg0KBWZpbGVzGAEgASgNEhEKCWFkZGl0aW9ucxgCIAEoDRIRCglkZWxldGlvbnMYAyABKA0iQwoKVHJhY2tlclJlZhIMCgRraW5kGAEgASgJEgoKAmlkGAIgASgJEg4KBnN0YXR1cxgDIAEoCRILCgN1cmwYBCABKAkiRwoGUmV2aWV3Eg4KBmF1dGhvchgBIAEoCRIOCgZpc19ib3QYAiABKAgSDwoHdmVyZGljdBgDIAEoCRIMCgRib2R5GAQgASgJIlUKDFJldmlld1RocmVhZBIMCgRwYXRoGAEgASgJEhAKCHJlc29sdmVkGAIgASgIEiUKCGNvbW1lbnRzGAMgAygLMhMuY29tcGFzcy52MS5Db21tZW50IjcKB0NvbW1lbnQSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIMCgRib2R5GAMgASgJKmQKDlNlY3JldERlbGl2ZXJ5Eh8KG1NFQ1JFVF9ERUxJVkVSWV9VTlNQRUNJRklFRBAAEhgKFFNFQ1JFVF9ERUxJVkVSWV9GSUxFEAESFwoTU0VDUkVUX0RFTElWRVJZX0VOVhACKnAKClNlY3JldEtpbmQSGwoXU0VDUkVUX0tJTkRfVU5TUEVDSUZJRUQQABIXChNTRUNSRVRfS0lORF9HRU5FUklDEAESGAoUU0VDUkVUX0tJTkRfUFJPVklERVIQAhISCg5TRUNSRVRfS0lORF9HSBADKkMKC1NlcnZlclN0YXRlEhwKGFNFUlZFUl9TVEFURV9VTlNQRUNJRklFRBAAEhYKElNFUlZFUl9TVEFURV9SRUFEWRABKpcBCgtSdW50aW1lVGllchIcChhSVU5USU1FX1RJRVJfVU5TUEVDSUZJRUQQABIXChNSVU5USU1FX1RJRVJfUE9ETUFOEAESGAoUUlVOVElNRV9USUVSX01JQ1JPVk0QAhIgChxSVU5USU1FX1RJRVJfQVBQTEVfQ09OVEFJTkVSEAMSFQoRUlVOVElNRV9USUVSX0hPU1QQBCpoCg1FZ3Jlc3NQb3N0dXJlEh4KGkVHUkVTU19QT1NUVVJFX1VOU1BFQ0lGSUVEEAASGAoURUdSRVNTX1BPU1RVUkVfQVJNRUQQARIdChlFR1JFU1NfUE9TVFVSRV9VTkVORk9SQ0VEEAIqggIKEUFnZW50U2Vzc2lvblN0YXRlEiMKH0FHRU5UX1NFU1NJT05fU1RBVEVfVU5TUEVDSUZJRUQQABIgChxBR0VOVF9TRVNTSU9OX1NUQVRFX1NUQVJUSU5HEAESHQoZQUdFTlRfU0VTU0lPTl9TVEFURV9SRUFEWRACEh8KG0FHRU5UX1NFU1NJT05fU1RBVEVfV09SS0lORxADEh8KG0FHRU5UX1NFU1NJT05fU1RBVEVfU1RPUFBFRBAEEh8KG0FHRU5UX1NFU1NJT05fU1RBVEVfRVJST1JFRBAFEiQKIEFHRU5UX1NFU1NJT05fU1RBVEVfRElTQ09OTkVDVEVEEAYq0gEKE0FnZW50VG9vbENhbGxTdGF0dXMSJgoiQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19VTlNQRUNJRklFRBAAEiIKHkFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfUEVORElORxABEiYKIkFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfSU5fUFJPR1JFU1MQAhIkCiBBR0VOVF9UT09MX0NBTExfU1RBVFVTX0NPTVBMRVRFRBADEiEKHUFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfRkFJTEVEEAQqtAEKFEFnZW50UGxhbkVudHJ5U3RhdHVzEicKI0FHRU5UX1BMQU5fRU5UUllfU1RBVFVTX1VOU1BFQ0lGSUVEEAASIwofQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfUEVORElORxABEicKI0FHRU5UX1BMQU5fRU5UUllfU1RBVFVTX0lOX1BST0dSRVNTEAISJQohQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfQ09NUExFVEVEEAMqhAEKFFNlc3Npb25JbmplY3Rpb25LaW5kEiYKIlNFU1NJT05fSU5KRUNUSU9OX0tJTkRfVU5TUEVDSUZJRUQQABIgChxTRVNTSU9OX0lOSkVDVElPTl9LSU5EX1NURUVSEAESIgoeU0VTU0lPTl9JTkpFQ1RJT05fS0lORF9ERUxJVkVSEAIqdAoQU2Vzc2lvbkVycm9yS2luZBIiCh5TRVNTSU9OX0VSUk9SX0tJTkRfVU5TUEVDSUZJRUQQABIcChhTRVNTSU9OX0VSUk9SX0tJTkRfRVJST1IQARIeChpTRVNTSU9OX0VSUk9SX0tJTkRfQUJPUlRFRBACKvEBCgpJc3N1ZVN0YXRlEhsKF0lTU1VFX1NUQVRFX1VOU1BFQ0lGSUVEEAASFwoTSVNTVUVfU1RBVEVfQkFDS0xPRxABEhQKEElTU1VFX1NUQVRFX1RPRE8QAhIWChJJU1NVRV9TVEFURV9RVUVVRUQQAxIXChNJU1NVRV9TVEFURV9CTE9DS0VEEAQSGwoXSVNTVUVfU1RBVEVfSU5fUFJPR1JFU1MQBRIZChVJU1NVRV9TVEFURV9JTl9SRVZJRVcQBhIUChBJU1NVRV9TVEFURV9ET05FEAcSGAoUSVNTVUVfU1RBVEVfQVJDSElWRUQQCCqcAQoNRm9yZ2VQcm92aWRlchIeChpGT1JHRV9QUk9WSURFUl9VTlNQRUNJRklFRBAAEhkKFUZPUkdFX1BST1ZJREVSX0dJVEhVQhABEhkKFUZPUkdFX1BST1ZJREVSX0dJVExBQhACEhoKFkZPUkdFX1BST1ZJREVSX0ZPUkdFSk8QAxIZChVGT1JHRV9QUk9WSURFUl9MSU5FQVIQBDLTDgoOQ29tcGFzc1NlcnZpY2USVAoNR2V0U2VydmVySW5mbxIgLmNvbXBhc3MudjEuR2V0U2VydmVySW5mb1JlcXVlc3QaIS5jb21wYXNzLnYxLkdldFNlcnZlckluZm9SZXNwb25zZRI/CgZXaG9BbUkSGS5jb21wYXNzLnYxLldob0FtSVJlcXVlc3QaGi5jb21wYXNzLnYxLldob0FtSVJlc3BvbnNlElwKD1N1YnNjcmliZUV2ZW50cxIiLmNvbXBhc3MudjEuU3Vic2NyaWJlRXZlbnRzUmVxdWVzdBojLmNvbXBhc3MudjEuU3Vic2NyaWJlRXZlbnRzUmVzcG9uc2UwARJaCg9MaXN0Qm9hcmRJc3N1ZXMSIi5jb21wYXNzLnYxLkxpc3RCb2FyZElzc3Vlc1JlcXVlc3QaIy5jb21wYXNzLnYxLkxpc3RCb2FyZElzc3Vlc1Jlc3BvbnNlEnIKF1Byb3Zpc2lvbkFnZW50V29ya3NwYWNlEiouY29tcGFzcy52MS5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QaKy5jb21wYXNzLnYxLlByb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVzcG9uc2USYAoRU3RhcnRBZ2VudFNlc3Npb24SJC5jb21wYXNzLnYxLlN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBolLmNvbXBhc3MudjEuU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRJLCgpTcGF3bkFnZW50Eh0uY29tcGFzcy52MS5TcGF3bkFnZW50UmVxdWVzdBoeLmNvbXBhc3MudjEuU3Bhd25BZ2VudFJlc3BvbnNlEl0KEFN0b3BBZ2VudFNlc3Npb24SIy5jb21wYXNzLnYxLlN0b3BBZ2VudFNlc3Npb25SZXF1ZXN0GiQuY29tcGFzcy52MS5TdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2USaQoUUmVtb3ZlQWdlbnRXb3Jrc3BhY2USJy5jb21wYXNzLnYxLlJlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBooLmNvbXBhc3MudjEuUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJjChJSZWxvYWRBZ2VudFNlc3Npb24SJS5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QaJi5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlc3BvbnNlElcKDkdldEFnZW50U3RhdHVzEiEuY29tcGFzcy52MS5HZXRBZ2VudFN0YXR1c1JlcXVlc3QaIi5jb21wYXNzLnYxLkdldEFnZW50U3RhdHVzUmVzcG9uc2USYgoVU3Vic2NyaWJlQWdlbnRTZXNzaW9uEiguY29tcGFzcy52MS5TdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0Gh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25GcmFtZTABEksKCklzc3VlVG9rZW4SHS5jb21wYXNzLnYxLklzc3VlVG9rZW5SZXF1ZXN0Gh4uY29tcGFzcy52MS5Jc3N1ZVRva2VuUmVzcG9uc2USTgoLUmV2b2tlVG9rZW4SHi5jb21wYXNzLnYxLlJldm9rZVRva2VuUmVxdWVzdBofLmNvbXBhc3MudjEuUmV2b2tlVG9rZW5SZXNwb25zZRJXCg5QdXRBZ2VudENvbmZpZxIhLmNvbXBhc3MudjEuUHV0QWdlbnRDb25maWdSZXF1ZXN0GiIuY29tcGFzcy52MS5QdXRBZ2VudENvbmZpZ1Jlc3BvbnNlEmMKEkdldEFnZW50Q29uZmlnSW5mbxIlLmNvbXBhc3MudjEuR2V0QWdlbnRDb25maWdJbmZvUmVxdWVzdBomLmNvbXBhc3MudjEuR2V0QWdlbnRDb25maWdJbmZvUmVzcG9uc2USYAoRRGVsZXRlQWdlbnRDb25maWcSJC5jb21wYXNzLnYxLkRlbGV0ZUFnZW50Q29uZmlnUmVxdWVzdBolLmNvbXBhc3MudjEuRGVsZXRlQWdlbnRDb25maWdSZXNwb25zZRJdChBQdXRNb2RlbFJlZ2lzdHJ5EiMuY29tcGFzcy52MS5QdXRNb2RlbFJlZ2lzdHJ5UmVxdWVzdBokLmNvbXBhc3MudjEuUHV0TW9kZWxSZWdpc3RyeVJlc3BvbnNlEl0KEEdldE1vZGVsUmVnaXN0cnkSIy5jb21wYXNzLnYxLkdldE1vZGVsUmVnaXN0cnlSZXF1ZXN0GiQuY29tcGFzcy52MS5HZXRNb2RlbFJlZ2lzdHJ5UmVzcG9uc2USZgoTRGVsZXRlTW9kZWxSZWdpc3RyeRImLmNvbXBhc3MudjEuRGVsZXRlTW9kZWxSZWdpc3RyeVJlcXVlc3QaJy5jb21wYXNzLnYxLkRlbGV0ZU1vZGVsUmVnaXN0cnlSZXNwb25zZTKgBAoOU2VjcmV0c1NlcnZpY2USSAoJU2V0U2VjcmV0EhwuY29tcGFzcy52MS5TZXRTZWNyZXRSZXF1ZXN0Gh0uY29tcGFzcy52MS5TZXRTZWNyZXRSZXNwb25zZRJOCgtMaXN0U2VjcmV0cxIeLmNvbXBhc3MudjEuTGlzdFNlY3JldHNSZXF1ZXN0Gh8uY29tcGFzcy52MS5MaXN0U2VjcmV0c1Jlc3BvbnNlElEKDERlbGV0ZVNlY3JldBIfLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVxdWVzdBogLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVzcG9uc2USWgoPU2V0U2VydmVyU2VjcmV0EiIuY29tcGFzcy52MS5TZXRTZXJ2ZXJTZWNyZXRSZXF1ZXN0GiMuY29tcGFzcy52MS5TZXRTZXJ2ZXJTZWNyZXRSZXNwb25zZRJjChJEZWxldGVTZXJ2ZXJTZWNyZXQSJS5jb21wYXNzLnYxLkRlbGV0ZVNlcnZlclNlY3JldFJlcXVlc3QaJi5jb21wYXNzLnYxLkRlbGV0ZVNlcnZlclNlY3JldFJlc3BvbnNlEmAKEUxpc3RTZXJ2ZXJTZWNyZXRzEiQuY29tcGFzcy52MS5MaXN0U2VydmVyU2VjcmV0c1JlcXVlc3QaJS5jb21wYXNzLnYxLkxpc3RTZXJ2ZXJTZWNyZXRzUmVzcG9uc2ViBnByb3RvMw", [file_google_protobuf_timestamp]); + fileDesc("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEi0AEKEFNldFNlY3JldFJlcXVlc3QSDAoEbmFtZRgBIAEoCRISCgV2YWx1ZRgCIAEoCUIDgAEBEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkSJgoFc2NvcGUYByABKA4yFy5jb21wYXNzLnYxLlNlY3JldFNjb3BlIhMKEVNldFNlY3JldFJlc3BvbnNlIhQKEkxpc3RTZWNyZXRzUmVxdWVzdCJAChNMaXN0U2VjcmV0c1Jlc3BvbnNlEikKB3NlY3JldHMYASADKAsyGC5jb21wYXNzLnYxLlNlY3JldFN0YXR1cyKgAQoMU2VjcmV0U3RhdHVzEgwKBG5hbWUYASABKAkSDgoGaXNfc2V0GAIgASgIEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkiSwoTRGVsZXRlU2VjcmV0UmVxdWVzdBIMCgRuYW1lGAEgASgJEiYKBXNjb3BlGAIgASgOMhcuY29tcGFzcy52MS5TZWNyZXRTY29wZSIWChREZWxldGVTZWNyZXRSZXNwb25zZSI6ChZTZXRTZXJ2ZXJTZWNyZXRSZXF1ZXN0EgwKBG5hbWUYASABKAkSEgoFdmFsdWUYAiABKAlCA4ABASIZChdTZXRTZXJ2ZXJTZWNyZXRSZXNwb25zZSIpChlEZWxldGVTZXJ2ZXJTZWNyZXRSZXF1ZXN0EgwKBG5hbWUYASABKAkiHAoaRGVsZXRlU2VydmVyU2VjcmV0UmVzcG9uc2UiGgoYTGlzdFNlcnZlclNlY3JldHNSZXF1ZXN0IlMKGUxpc3RTZXJ2ZXJTZWNyZXRzUmVzcG9uc2USNgoOc2VydmVyX3NlY3JldHMYASADKAsyHi5jb21wYXNzLnYxLlNlcnZlclNlY3JldFN0YXR1cyIyChJTZXJ2ZXJTZWNyZXRTdGF0dXMSDAoEbmFtZRgBIAEoCRIOCgZpc19zZXQYAiABKAgiFgoUR2V0U2VydmVySW5mb1JlcXVlc3QiPQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSEwoLYXBpX3ZlcnNpb24YAiABKAkiDwoNV2hvQW1JUmVxdWVzdCIkCg5XaG9BbUlSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJIkMKFlN1YnNjcmliZUV2ZW50c1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEIuIDChdTdWJzY3JpYmVFdmVudHNSZXNwb25zZRILCgNzZXEYASABKAQSEgoKYXRfdW5peF9tcxgCIAEoAxIWCg5pbnN0YW5jZV9lcG9jaBgDIAEoBBIUCgxzbmFwc2hvdF9zZXEYBCABKAQSMQoNc2VydmVyX3N0YXR1cxgKIAEoCzIYLmNvbXBhc3MudjEuU2VydmVyU3RhdHVzSAASNQoPcmVzeW5jX3JlcXVpcmVkGAsgASgLMhouY29tcGFzcy52MS5SZXN5bmNSZXF1aXJlZEgAEj4KFGFnZW50X3Nlc3Npb25fc3RhdHVzGAwgASgLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXNIABI8ChNhZ2VudF9tZXNzYWdlX2NodW5rGA0gASgLMh0uY29tcGFzcy52MS5BZ2VudE1lc3NhZ2VDaHVua0gAEjQKD2FnZW50X3Rvb2xfY2FsbBgOIAEoCzIZLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbEgAEisKCmFnZW50X3BsYW4YDyABKAsyFS5jb21wYXNzLnYxLkFnZW50UGxhbkgAEiIKBWlzc3VlGBAgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAQgkKB3BheWxvYWQiLgoWTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPAoXTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSI2CgxTZXJ2ZXJTdGF0dXMSJgoFc3RhdGUYASABKA4yFy5jb21wYXNzLnYxLlNlcnZlclN0YXRlIhAKDlJlc3luY1JlcXVpcmVkItIBChJBZ2VudFNlc3Npb25TdGF0dXMSEgoKc2Vzc2lvbl9pZBgBIAEoCRIsCgVzdGF0ZRgCIAEoDjIdLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdGUSGAoQYWdlbnRfYWNjb3VudF9pZBgDIAEoCRItCgxydW50aW1lX3RpZXIYBCABKA4yFy5jb21wYXNzLnYxLlJ1bnRpbWVUaWVyEjEKDmVncmVzc19wb3N0dXJlGAUgASgOMhkuY29tcGFzcy52MS5FZ3Jlc3NQb3N0dXJlIkkKEUFnZW50TWVzc2FnZUNodW5rEhIKCnNlc3Npb25faWQYASABKAkSDAoEdGV4dBgCIAEoCRISCgppc190aG91Z2h0GAMgASgIInkKDUFnZW50VG9vbENhbGwSEgoKc2Vzc2lvbl9pZBgBIAEoCRIUCgx0b29sX2NhbGxfaWQYAiABKAkSDQoFdGl0bGUYAyABKAkSLwoGc3RhdHVzGAQgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIkwKCUFnZW50UGxhbhISCgpzZXNzaW9uX2lkGAEgASgJEisKB2VudHJpZXMYAiADKAsyGi5jb21wYXNzLnYxLkFnZW50UGxhbkVudHJ5IlMKDkFnZW50UGxhbkVudHJ5Eg8KB2NvbnRlbnQYASABKAkSMAoGc3RhdHVzGAIgASgOMiAuY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeVN0YXR1cyLfAwoMU2Vzc2lvbkV2ZW50EhAKCGV2ZW50X2lkGAEgASgJEhIKCmF0X3VuaXhfbXMYAiABKAMSOgoOYXNzaXN0YW50X3RleHQYAyABKAsyIC5jb21wYXNzLnYxLlNlc3Npb25Bc3Npc3RhbnRUZXh0SAASLwoIdGhpbmtpbmcYBCABKAsyGy5jb21wYXNzLnYxLlNlc3Npb25UaGlua2luZ0gAEjAKCXRvb2xfY2FsbBgFIAEoCzIbLmNvbXBhc3MudjEuU2Vzc2lvblRvb2xDYWxsSAASPQoQdG9vbF9jYWxsX3VwZGF0ZRgGIAEoCzIhLmNvbXBhc3MudjEuU2Vzc2lvblRvb2xDYWxsVXBkYXRlSAASJwoEcGxhbhgHIAEoCzIXLmNvbXBhc3MudjEuU2Vzc2lvblBsYW5IABIrCgZub3RpY2UYCCABKAsyGS5jb21wYXNzLnYxLlNlc3Npb25Ob3RpY2VIABI5ChFzZXNzaW9uX2luamVjdGlvbhgJIAEoCzIcLmNvbXBhc3MudjEuU2Vzc2lvbkluamVjdGlvbkgAEjEKDXNlc3Npb25fZXJyb3IYCiABKAsyGC5jb21wYXNzLnYxLlNlc3Npb25FcnJvckgAQgcKBWV2ZW50IjgKFFNlc3Npb25Bc3Npc3RhbnRUZXh0EgwKBHRleHQYASABKAkSEgoKbWVzc2FnZV9pZBgCIAEoCSIzCg9TZXNzaW9uVGhpbmtpbmcSDAoEdGV4dBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJImcKD1Nlc3Npb25Ub29sQ2FsbBIUCgx0b29sX2NhbGxfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSLwoGc3RhdHVzGAMgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIpoBChVTZXNzaW9uVG9vbENhbGxVcGRhdGUSFAoMdG9vbF9jYWxsX2lkGAEgASgJEi8KBnN0YXR1cxgCIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cxIOCgZvdXRwdXQYAyABKAkSKgoFZGlmZnMYBCADKAsyGy5jb21wYXNzLnYxLlNlc3Npb25GaWxlRGlmZiJVCg9TZXNzaW9uRmlsZURpZmYSDAoEcGF0aBgBIAEoCRIVCghvbGRfdGV4dBgCIAEoCUgAiAEBEhAKCG5ld190ZXh0GAMgASgJQgsKCV9vbGRfdGV4dCI6CgtTZXNzaW9uUGxhbhIrCgdlbnRyaWVzGAEgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSI5Cg1TZXNzaW9uTm90aWNlEgwKBHRleHQYASABKAkSEQoEbGluaxgCIAEoCUgAiAEBQgcKBV9saW5rIoMBChBTZXNzaW9uSW5qZWN0aW9uEjEKB29wX2tpbmQYASABKA4yIC5jb21wYXNzLnYxLlNlc3Npb25JbmplY3Rpb25LaW5kEhIKCm1lc3NhZ2VfaWQYAiABKAkSEwoLZnJvbV9oYW5kbGUYAyABKAkSEwoLdHJhY2VwYXJlbnQYBCABKAkiawoMU2Vzc2lvbkVycm9yEioKBGtpbmQYASABKA4yHC5jb21wYXNzLnYxLlNlc3Npb25FcnJvcktpbmQSDwoHbWVzc2FnZRgCIAEoCRITCgZzdGF0dXMYAyABKAVIAIgBAUIJCgdfc3RhdHVzIjIKHFN1YnNjcmliZUFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSJ+ChFBZ2VudFNlc3Npb25GcmFtZRISCgpzZXNzaW9uX2lkGAEgASgJEicKBWV2ZW50GAIgASgLMhguY29tcGFzcy52MS5TZXNzaW9uRXZlbnQSLAoFc3RhdGUYAyABKA4yHS5jb21wYXNzLnYxLkFnZW50U2Vzc2lvblN0YXRlInAKHlByb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVxdWVzdBIUCgxhZ2VudF9oYW5kbGUYASABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAiABKAkSDwoHcGVyc29uYRgDIAEoCRIMCgRyb2xlGAQgASgJIjkKH1Byb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVzcG9uc2USFgoOY29udGFpbmVyX25hbWUYASABKAkiUAobUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0EhYKDmNvbnRhaW5lcl9uYW1lGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJIh4KHFJlbW92ZUFnZW50V29ya3NwYWNlUmVzcG9uc2UiYwoYU3RhcnRBZ2VudFNlc3Npb25SZXF1ZXN0EhYKDmNvbnRhaW5lcl9uYW1lGAEgASgJEhkKEXJlc3VtZV9zZXNzaW9uX2lkGAMgASgJSgQIAhADUg5pbml0aWFsX3Byb21wdCIvChlTdGFydEFnZW50U2Vzc2lvblJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkiWgoRU3Bhd25BZ2VudFJlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAMgASgJSgQIAhADUg5pbml0aWFsX3Byb21wdCJAChJTcGF3bkFnZW50UmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRIWCg5jb250YWluZXJfbmFtZRgCIAEoCSItChdTdG9wQWdlbnRTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIhoKGFN0b3BBZ2VudFNlc3Npb25SZXNwb25zZSIvChlSZWxvYWRBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiMAoaUmVsb2FkQWdlbnRTZXNzaW9uUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCSIrChVHZXRBZ2VudFN0YXR1c1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSJKChZHZXRBZ2VudFN0YXR1c1Jlc3BvbnNlEjAKCHN0YXR1c2VzGAEgAygLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXMiKwoRSXNzdWVUb2tlblJlcXVlc3QSFgoOYWNjb3VudF9oYW5kbGUYASABKAkiIwoSSXNzdWVUb2tlblJlc3BvbnNlEg0KBXRva2VuGAEgASgJIigKElJldm9rZVRva2VuUmVxdWVzdBISCgV0b2tlbhgBIAEoCUIDgAEBIhUKE1Jldm9rZVRva2VuUmVzcG9uc2UiJwoVUHV0QWdlbnRDb25maWdSZXF1ZXN0Eg4KBmJ1bmRsZRgBIAEoDCIpChZQdXRBZ2VudENvbmZpZ1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkiGwoZR2V0QWdlbnRDb25maWdJbmZvUmVxdWVzdCLaAQoaR2V0QWdlbnRDb25maWdJbmZvUmVzcG9uc2USDwoHdmVyc2lvbhgBIAEoCRIOCgZza2lsbHMYAiADKAkSEgoKZXh0ZW5zaW9ucxgDIAMoCRITCgttY3Bfc2VydmVycxgEIAMoCRIUCgxoYXNfc2V0dGluZ3MYBSABKAgSFQoNaGFzX2FnZW50c19tZBgGIAEoCBINCgVydWxlcxgHIAMoCRIRCglzdWJhZ2VudHMYCCADKAkSEgoKaGFzX21vZGVscxgJIAEoCBIPCgdwcm9tcHRzGAogAygJIhoKGERlbGV0ZUFnZW50Q29uZmlnUmVxdWVzdCIbChlEZWxldGVBZ2VudENvbmZpZ1Jlc3BvbnNlIjQKDk1vZGVsQ2FuZGlkYXRlEhAKCHByb3ZpZGVyGAEgASgJEhAKCG1vZGVsX2lkGAIgASgJInEKDU1vZGVsTWV0YWRhdGESFgoOY29udGV4dF93aW5kb3cYASABKAMSHAoUaW5wdXRfY29zdF9taWNyb191c2QYAiABKAMSHQoVb3V0cHV0X2Nvc3RfbWljcm9fdXNkGAMgASgDEgsKA2FwaRgEIAEoCSKHAQoSTW9kZWxSZWdpc3RyeUVudHJ5EhQKDGRpc3BsYXlfbmFtZRgBIAEoCRIuCgpjYW5kaWRhdGVzGAIgAygLMhouY29tcGFzcy52MS5Nb2RlbENhbmRpZGF0ZRIrCghtZXRhZGF0YRgDIAEoCzIZLmNvbXBhc3MudjEuTW9kZWxNZXRhZGF0YSKYAQoNTW9kZWxSZWdpc3RyeRI3CgdlbnRyaWVzGAEgAygLMiYuY29tcGFzcy52MS5Nb2RlbFJlZ2lzdHJ5LkVudHJpZXNFbnRyeRpOCgxFbnRyaWVzRW50cnkSCwoDa2V5GAEgASgJEi0KBXZhbHVlGAIgASgLMh4uY29tcGFzcy52MS5Nb2RlbFJlZ2lzdHJ5RW50cnk6AjgBImAKF1B1dE1vZGVsUmVnaXN0cnlSZXF1ZXN0EisKCHJlZ2lzdHJ5GAEgASgLMhkuY29tcGFzcy52MS5Nb2RlbFJlZ2lzdHJ5EhgKEGV4cGVjdGVkX3ZlcnNpb24YAiABKAMiKwoYUHV0TW9kZWxSZWdpc3RyeVJlc3BvbnNlEg8KB3ZlcnNpb24YASABKAMiGQoXR2V0TW9kZWxSZWdpc3RyeVJlcXVlc3QiWAoYR2V0TW9kZWxSZWdpc3RyeVJlc3BvbnNlEg8KB3ZlcnNpb24YASABKAMSKwoIcmVnaXN0cnkYAiABKAsyGS5jb21wYXNzLnYxLk1vZGVsUmVnaXN0cnkiHAoaRGVsZXRlTW9kZWxSZWdpc3RyeVJlcXVlc3QiHQobRGVsZXRlTW9kZWxSZWdpc3RyeVJlc3BvbnNlIj4KEEFnZW50QXR0cmlidXRpb24SFAoMYWdlbnRfaGFuZGxlGAEgASgJEhQKDG93bmVyX2hhbmRsZRgCIAEoCSJFCghGb3JnZVJlZhIrCghwcm92aWRlchgBIAEoDjIZLmNvbXBhc3MudjEuRm9yZ2VQcm92aWRlchIMCgRob3N0GAIgASgJItQDCgVJc3N1ZRIKCgJpZBgBIAEoCRIjCgVmb3JnZRgCIAEoCzIULmNvbXBhc3MudjEuRm9yZ2VSZWYSDAoEcmVwbxgDIAEoCRIOCgZudW1iZXIYBCABKA0SDQoFdGl0bGUYBSABKAkSDAoEYm9keRgGIAEoCRITCgtmb3JnZV9zdGF0ZRgHIAEoCRILCgN1cmwYCCABKAkSKwoFYWdlbnQYCSABKAsyHC5jb21wYXNzLnYxLkFnZW50QXR0cmlidXRpb24SFQoNZm9yZ2VfYWNjb3VudBgKIAEoCRIOCgZsYWJlbHMYCyADKAkSLgoKdXBkYXRlZF9hdBgTIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASJQoFc3RhdGUYDCABKA4yFi5jb21wYXNzLnYxLklzc3VlU3RhdGUSEAoIcHJpb3JpdHkYDSABKAkSEAoIYXNzaWduZWUYDiABKAkSDwoHc3VtbWFyeRgPIAEoCRIOCgZicmFuY2gYECABKAkSJAoDcHJzGBEgAygLMhcuY29tcGFzcy52MS5QdWxsUmVxdWVzdBInCgd0cmFja2VyGBIgASgLMhYuY29tcGFzcy52MS5UcmFja2VyUmVmIp4DCgtQdWxsUmVxdWVzdBIjCgVmb3JnZRgBIAEoCzIULmNvbXBhc3MudjEuRm9yZ2VSZWYSDAoEcmVwbxgCIAEoCRIOCgZudW1iZXIYAyABKA0SDQoFdGl0bGUYBCABKAkSEwoLZm9yZ2Vfc3RhdGUYBSABKAkSCwoDdXJsGAYgASgJEhAKCGhlYWRfcmVmGAcgASgJEhAKCGJhc2VfcmVmGAggASgJEisKBWFnZW50GAkgASgLMhwuY29tcGFzcy52MS5BZ2VudEF0dHJpYnV0aW9uEhUKDWZvcmdlX2FjY291bnQYCiABKAkSDQoFZHJhZnQYCyABKAgSKQoHY2hhbmdlZBgMIAEoCzIYLmNvbXBhc3MudjEuQ2hhbmdlZFN0YXRzEikKBmNoZWNrcxgNIAEoCzIZLmNvbXBhc3MudjEuQ2hlY2tzU3VtbWFyeRIjCgdyZXZpZXdzGA4gAygLMhIuY29tcGFzcy52MS5SZXZpZXcSKQoHdGhyZWFkcxgPIAMoCzIYLmNvbXBhc3MudjEuUmV2aWV3VGhyZWFkIlMKDUNoZWNrc1N1bW1hcnkSEAoIaGVhZF9zaGEYASABKAkSDQoFc3RhdGUYAiABKAkSIQoGY2hlY2tzGAMgAygLMhEuY29tcGFzcy52MS5DaGVjayJDCgVDaGVjaxIMCgRuYW1lGAEgASgJEg0KBXN0YXRlGAIgASgJEgsKA3VybBgDIAEoCRIQCghyZXF1aXJlZBgEIAEoCCJDCgxDaGFuZ2VkU3RhdHMSDQoFZmlsZXMYASABKA0SEQoJYWRkaXRpb25zGAIgASgNEhEKCWRlbGV0aW9ucxgDIAEoDSJDCgpUcmFja2VyUmVmEgwKBGtpbmQYASABKAkSCgoCaWQYAiABKAkSDgoGc3RhdHVzGAMgASgJEgsKA3VybBgEIAEoCSJHCgZSZXZpZXcSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIPCgd2ZXJkaWN0GAMgASgJEgwKBGJvZHkYBCABKAkiVQoMUmV2aWV3VGhyZWFkEgwKBHBhdGgYASABKAkSEAoIcmVzb2x2ZWQYAiABKAgSJQoIY29tbWVudHMYAyADKAsyEy5jb21wYXNzLnYxLkNvbW1lbnQiNwoHQ29tbWVudBIOCgZhdXRob3IYASABKAkSDgoGaXNfYm90GAIgASgIEgwKBGJvZHkYAyABKAkqZAoOU2VjcmV0RGVsaXZlcnkSHwobU0VDUkVUX0RFTElWRVJZX1VOU1BFQ0lGSUVEEAASGAoUU0VDUkVUX0RFTElWRVJZX0ZJTEUQARIXChNTRUNSRVRfREVMSVZFUllfRU5WEAIqcAoKU2VjcmV0S2luZBIbChdTRUNSRVRfS0lORF9VTlNQRUNJRklFRBAAEhcKE1NFQ1JFVF9LSU5EX0dFTkVSSUMQARIYChRTRUNSRVRfS0lORF9QUk9WSURFUhACEhIKDlNFQ1JFVF9LSU5EX0dIEAMqWwoLU2VjcmV0U2NvcGUSHAoYU0VDUkVUX1NDT1BFX1VOU1BFQ0lGSUVEEAASFQoRU0VDUkVUX1NDT1BFX1VTRVIQARIXChNTRUNSRVRfU0NPUEVfVEVOQU5UEAIqQwoLU2VydmVyU3RhdGUSHAoYU0VSVkVSX1NUQVRFX1VOU1BFQ0lGSUVEEAASFgoSU0VSVkVSX1NUQVRFX1JFQURZEAEqlwEKC1J1bnRpbWVUaWVyEhwKGFJVTlRJTUVfVElFUl9VTlNQRUNJRklFRBAAEhcKE1JVTlRJTUVfVElFUl9QT0RNQU4QARIYChRSVU5USU1FX1RJRVJfTUlDUk9WTRACEiAKHFJVTlRJTUVfVElFUl9BUFBMRV9DT05UQUlORVIQAxIVChFSVU5USU1FX1RJRVJfSE9TVBAEKmgKDUVncmVzc1Bvc3R1cmUSHgoaRUdSRVNTX1BPU1RVUkVfVU5TUEVDSUZJRUQQABIYChRFR1JFU1NfUE9TVFVSRV9BUk1FRBABEh0KGUVHUkVTU19QT1NUVVJFX1VORU5GT1JDRUQQAiqCAgoRQWdlbnRTZXNzaW9uU3RhdGUSIwofQUdFTlRfU0VTU0lPTl9TVEFURV9VTlNQRUNJRklFRBAAEiAKHEFHRU5UX1NFU1NJT05fU1RBVEVfU1RBUlRJTkcQARIdChlBR0VOVF9TRVNTSU9OX1NUQVRFX1JFQURZEAISHwobQUdFTlRfU0VTU0lPTl9TVEFURV9XT1JLSU5HEAMSHwobQUdFTlRfU0VTU0lPTl9TVEFURV9TVE9QUEVEEAQSHwobQUdFTlRfU0VTU0lPTl9TVEFURV9FUlJPUkVEEAUSJAogQUdFTlRfU0VTU0lPTl9TVEFURV9ESVNDT05ORUNURUQQBirSAQoTQWdlbnRUb29sQ2FsbFN0YXR1cxImCiJBR0VOVF9UT09MX0NBTExfU1RBVFVTX1VOU1BFQ0lGSUVEEAASIgoeQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19QRU5ESU5HEAESJgoiQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19JTl9QUk9HUkVTUxACEiQKIEFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfQ09NUExFVEVEEAMSIQodQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19GQUlMRUQQBCq0AQoUQWdlbnRQbGFuRW50cnlTdGF0dXMSJwojQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfVU5TUEVDSUZJRUQQABIjCh9BR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19QRU5ESU5HEAESJwojQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfSU5fUFJPR1JFU1MQAhIlCiFBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19DT01QTEVURUQQAyqEAQoUU2Vzc2lvbkluamVjdGlvbktpbmQSJgoiU0VTU0lPTl9JTkpFQ1RJT05fS0lORF9VTlNQRUNJRklFRBAAEiAKHFNFU1NJT05fSU5KRUNUSU9OX0tJTkRfU1RFRVIQARIiCh5TRVNTSU9OX0lOSkVDVElPTl9LSU5EX0RFTElWRVIQAip0ChBTZXNzaW9uRXJyb3JLaW5kEiIKHlNFU1NJT05fRVJST1JfS0lORF9VTlNQRUNJRklFRBAAEhwKGFNFU1NJT05fRVJST1JfS0lORF9FUlJPUhABEh4KGlNFU1NJT05fRVJST1JfS0lORF9BQk9SVEVEEAIq8QEKCklzc3VlU3RhdGUSGwoXSVNTVUVfU1RBVEVfVU5TUEVDSUZJRUQQABIXChNJU1NVRV9TVEFURV9CQUNLTE9HEAESFAoQSVNTVUVfU1RBVEVfVE9ETxACEhYKEklTU1VFX1NUQVRFX1FVRVVFRBADEhcKE0lTU1VFX1NUQVRFX0JMT0NLRUQQBBIbChdJU1NVRV9TVEFURV9JTl9QUk9HUkVTUxAFEhkKFUlTU1VFX1NUQVRFX0lOX1JFVklFVxAGEhQKEElTU1VFX1NUQVRFX0RPTkUQBxIYChRJU1NVRV9TVEFURV9BUkNISVZFRBAIKpwBCg1Gb3JnZVByb3ZpZGVyEh4KGkZPUkdFX1BST1ZJREVSX1VOU1BFQ0lGSUVEEAASGQoVRk9SR0VfUFJPVklERVJfR0lUSFVCEAESGQoVRk9SR0VfUFJPVklERVJfR0lUTEFCEAISGgoWRk9SR0VfUFJPVklERVJfRk9SR0VKTxADEhkKFUZPUkdFX1BST1ZJREVSX0xJTkVBUhAEMtMOCg5Db21wYXNzU2VydmljZRJUCg1HZXRTZXJ2ZXJJbmZvEiAuY29tcGFzcy52MS5HZXRTZXJ2ZXJJbmZvUmVxdWVzdBohLmNvbXBhc3MudjEuR2V0U2VydmVySW5mb1Jlc3BvbnNlEj8KBldob0FtSRIZLmNvbXBhc3MudjEuV2hvQW1JUmVxdWVzdBoaLmNvbXBhc3MudjEuV2hvQW1JUmVzcG9uc2USXAoPU3Vic2NyaWJlRXZlbnRzEiIuY29tcGFzcy52MS5TdWJzY3JpYmVFdmVudHNSZXF1ZXN0GiMuY29tcGFzcy52MS5TdWJzY3JpYmVFdmVudHNSZXNwb25zZTABEloKD0xpc3RCb2FyZElzc3VlcxIiLmNvbXBhc3MudjEuTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBojLmNvbXBhc3MudjEuTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2UScgoXUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2USKi5jb21wYXNzLnYxLlByb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVxdWVzdBorLmNvbXBhc3MudjEuUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJgChFTdGFydEFnZW50U2Vzc2lvbhIkLmNvbXBhc3MudjEuU3RhcnRBZ2VudFNlc3Npb25SZXF1ZXN0GiUuY29tcGFzcy52MS5TdGFydEFnZW50U2Vzc2lvblJlc3BvbnNlEksKClNwYXduQWdlbnQSHS5jb21wYXNzLnYxLlNwYXduQWdlbnRSZXF1ZXN0Gh4uY29tcGFzcy52MS5TcGF3bkFnZW50UmVzcG9uc2USXQoQU3RvcEFnZW50U2Vzc2lvbhIjLmNvbXBhc3MudjEuU3RvcEFnZW50U2Vzc2lvblJlcXVlc3QaJC5jb21wYXNzLnYxLlN0b3BBZ2VudFNlc3Npb25SZXNwb25zZRJpChRSZW1vdmVBZ2VudFdvcmtzcGFjZRInLmNvbXBhc3MudjEuUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0GiguY29tcGFzcy52MS5SZW1vdmVBZ2VudFdvcmtzcGFjZVJlc3BvbnNlEmMKElJlbG9hZEFnZW50U2Vzc2lvbhIlLmNvbXBhc3MudjEuUmVsb2FkQWdlbnRTZXNzaW9uUmVxdWVzdBomLmNvbXBhc3MudjEuUmVsb2FkQWdlbnRTZXNzaW9uUmVzcG9uc2USVwoOR2V0QWdlbnRTdGF0dXMSIS5jb21wYXNzLnYxLkdldEFnZW50U3RhdHVzUmVxdWVzdBoiLmNvbXBhc3MudjEuR2V0QWdlbnRTdGF0dXNSZXNwb25zZRJiChVTdWJzY3JpYmVBZ2VudFNlc3Npb24SKC5jb21wYXNzLnYxLlN1YnNjcmliZUFnZW50U2Vzc2lvblJlcXVlc3QaHS5jb21wYXNzLnYxLkFnZW50U2Vzc2lvbkZyYW1lMAESSwoKSXNzdWVUb2tlbhIdLmNvbXBhc3MudjEuSXNzdWVUb2tlblJlcXVlc3QaHi5jb21wYXNzLnYxLklzc3VlVG9rZW5SZXNwb25zZRJOCgtSZXZva2VUb2tlbhIeLmNvbXBhc3MudjEuUmV2b2tlVG9rZW5SZXF1ZXN0Gh8uY29tcGFzcy52MS5SZXZva2VUb2tlblJlc3BvbnNlElcKDlB1dEFnZW50Q29uZmlnEiEuY29tcGFzcy52MS5QdXRBZ2VudENvbmZpZ1JlcXVlc3QaIi5jb21wYXNzLnYxLlB1dEFnZW50Q29uZmlnUmVzcG9uc2USYwoSR2V0QWdlbnRDb25maWdJbmZvEiUuY29tcGFzcy52MS5HZXRBZ2VudENvbmZpZ0luZm9SZXF1ZXN0GiYuY29tcGFzcy52MS5HZXRBZ2VudENvbmZpZ0luZm9SZXNwb25zZRJgChFEZWxldGVBZ2VudENvbmZpZxIkLmNvbXBhc3MudjEuRGVsZXRlQWdlbnRDb25maWdSZXF1ZXN0GiUuY29tcGFzcy52MS5EZWxldGVBZ2VudENvbmZpZ1Jlc3BvbnNlEl0KEFB1dE1vZGVsUmVnaXN0cnkSIy5jb21wYXNzLnYxLlB1dE1vZGVsUmVnaXN0cnlSZXF1ZXN0GiQuY29tcGFzcy52MS5QdXRNb2RlbFJlZ2lzdHJ5UmVzcG9uc2USXQoQR2V0TW9kZWxSZWdpc3RyeRIjLmNvbXBhc3MudjEuR2V0TW9kZWxSZWdpc3RyeVJlcXVlc3QaJC5jb21wYXNzLnYxLkdldE1vZGVsUmVnaXN0cnlSZXNwb25zZRJmChNEZWxldGVNb2RlbFJlZ2lzdHJ5EiYuY29tcGFzcy52MS5EZWxldGVNb2RlbFJlZ2lzdHJ5UmVxdWVzdBonLmNvbXBhc3MudjEuRGVsZXRlTW9kZWxSZWdpc3RyeVJlc3BvbnNlMqAECg5TZWNyZXRzU2VydmljZRJICglTZXRTZWNyZXQSHC5jb21wYXNzLnYxLlNldFNlY3JldFJlcXVlc3QaHS5jb21wYXNzLnYxLlNldFNlY3JldFJlc3BvbnNlEk4KC0xpc3RTZWNyZXRzEh4uY29tcGFzcy52MS5MaXN0U2VjcmV0c1JlcXVlc3QaHy5jb21wYXNzLnYxLkxpc3RTZWNyZXRzUmVzcG9uc2USUQoMRGVsZXRlU2VjcmV0Eh8uY29tcGFzcy52MS5EZWxldGVTZWNyZXRSZXF1ZXN0GiAuY29tcGFzcy52MS5EZWxldGVTZWNyZXRSZXNwb25zZRJaCg9TZXRTZXJ2ZXJTZWNyZXQSIi5jb21wYXNzLnYxLlNldFNlcnZlclNlY3JldFJlcXVlc3QaIy5jb21wYXNzLnYxLlNldFNlcnZlclNlY3JldFJlc3BvbnNlEmMKEkRlbGV0ZVNlcnZlclNlY3JldBIlLmNvbXBhc3MudjEuRGVsZXRlU2VydmVyU2VjcmV0UmVxdWVzdBomLmNvbXBhc3MudjEuRGVsZXRlU2VydmVyU2VjcmV0UmVzcG9uc2USYAoRTGlzdFNlcnZlclNlY3JldHMSJC5jb21wYXNzLnYxLkxpc3RTZXJ2ZXJTZWNyZXRzUmVxdWVzdBolLmNvbXBhc3MudjEuTGlzdFNlcnZlclNlY3JldHNSZXNwb25zZWIGcHJvdG8z", [file_google_protobuf_timestamp]); /** * @generated from message compass.v1.SetSecretRequest @@ -59,6 +59,13 @@ export type SetSecretRequest = Message<"compass.v1.SetSecretRequest"> & { * @generated from field: string host = 6; */ host: string; + + /** + * tier the write targets; unspecified == user + * + * @generated from field: compass.v1.SecretScope scope = 7; + */ + scope: SecretScope; }; /** @@ -163,6 +170,13 @@ export type DeleteSecretRequest = Message<"compass.v1.DeleteSecretRequest"> & { * @generated from field: string name = 1; */ name: string; + + /** + * tier the delete targets; unspecified == user + * + * @generated from field: compass.v1.SecretScope scope = 2; + */ + scope: SecretScope; }; /** @@ -2720,6 +2734,43 @@ export enum SecretKind { export const SecretKindSchema: GenEnum = /*@__PURE__*/ enumDesc(file_compass_v1_compass, 1); +/** + * The tier a user-secret write targets. Names the same tiers as the store's + * SecretScope* constants but NOT the same numbers: proto reserves 0 for + * unspecified, so tenant is 2 here but 0 in the store — the handler maps + * explicitly, never casts. The unspecified default is USER: a client that omits + * the field writes its own private coordinate, never a tenant-wide value every + * other user's agents resolve. + * + * @generated from enum compass.v1.SecretScope + */ +export enum SecretScope { + /** + * treated as USER — private by default + * + * @generated from enum value: SECRET_SCOPE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: SECRET_SCOPE_USER = 1; + */ + USER = 1, + + /** + * admin-only + * + * @generated from enum value: SECRET_SCOPE_TENANT = 2; + */ + TENANT = 2, +} + +/** + * Describes the enum compass.v1.SecretScope. + */ +export const SecretScopeSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_compass_v1_compass, 2); + /** * @generated from enum compass.v1.ServerState */ @@ -2739,7 +2790,7 @@ export enum ServerState { * Describes the enum compass.v1.ServerState. */ export const ServerStateSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_compass, 2); + enumDesc(file_compass_v1_compass, 3); /** * The runtime tier an agent workload runs on. `HOST` runs agents as direct @@ -2778,7 +2829,7 @@ export enum RuntimeTier { * Describes the enum compass.v1.RuntimeTier. */ export const RuntimeTierSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_compass, 3); + enumDesc(file_compass_v1_compass, 4); /** * How an agent's egress is constrained. `UNENFORCED` means the tier cannot @@ -2809,7 +2860,7 @@ export enum EgressPosture { * Describes the enum compass.v1.EgressPosture. */ export const EgressPostureSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_compass, 4); + enumDesc(file_compass_v1_compass, 5); /** * Agent-session lifecycle states. `ERRORED` is an unexpected agent exit (OOM, @@ -2874,7 +2925,7 @@ export enum AgentSessionState { * Describes the enum compass.v1.AgentSessionState. */ export const AgentSessionStateSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_compass, 5); + enumDesc(file_compass_v1_compass, 6); /** * @generated from enum compass.v1.AgentToolCallStatus @@ -2910,7 +2961,7 @@ export enum AgentToolCallStatus { * Describes the enum compass.v1.AgentToolCallStatus. */ export const AgentToolCallStatusSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_compass, 6); + enumDesc(file_compass_v1_compass, 7); /** * @generated from enum compass.v1.AgentPlanEntryStatus @@ -2941,7 +2992,7 @@ export enum AgentPlanEntryStatus { * Describes the enum compass.v1.AgentPlanEntryStatus. */ export const AgentPlanEntryStatusSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_compass, 7); + enumDesc(file_compass_v1_compass, 8); /** * The control op-kind a SessionInjection records. Mirrors the internal @@ -2973,7 +3024,7 @@ export enum SessionInjectionKind { * Describes the enum compass.v1.SessionInjectionKind. */ export const SessionInjectionKindSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_compass, 8); + enumDesc(file_compass_v1_compass, 9); /** * The class of a SessionError. ERROR pairs with the ERRORED lifecycle @@ -3003,7 +3054,7 @@ export enum SessionErrorKind { * Describes the enum compass.v1.SessionErrorKind. */ export const SessionErrorKindSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_compass, 9); + enumDesc(file_compass_v1_compass, 10); /** * The Compass issue lifecycle, server-owned (DL-032/DL-033 + terminal ARCHIVED, @@ -3067,7 +3118,7 @@ export enum IssueState { * Describes the enum compass.v1.IssueState. */ export const IssueStateSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_compass, 10); + enumDesc(file_compass_v1_compass, 11); /** * Which forge (and which host, for self-hosted instances) an artifact lives on. @@ -3109,7 +3160,7 @@ export enum ForgeProvider { * Describes the enum compass.v1.ForgeProvider. */ export const ForgeProviderSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_compass, 11); + enumDesc(file_compass_v1_compass, 12); /** * The Compass server service. diff --git a/packages/compass-client/src/gen/compass/v1/compass_pb.ts b/packages/compass-client/src/gen/compass/v1/compass_pb.ts index cc0b54ac6..b0c7bf4d6 100644 --- a/packages/compass-client/src/gen/compass/v1/compass_pb.ts +++ b/packages/compass-client/src/gen/compass/v1/compass_pb.ts @@ -20,7 +20,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file compass/v1/compass.proto. */ export const file_compass_v1_compass: GenFile = /*@__PURE__*/ - fileDesc("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEiqAEKEFNldFNlY3JldFJlcXVlc3QSDAoEbmFtZRgBIAEoCRISCgV2YWx1ZRgCIAEoCUIDgAEBEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkiEwoRU2V0U2VjcmV0UmVzcG9uc2UiFAoSTGlzdFNlY3JldHNSZXF1ZXN0IkAKE0xpc3RTZWNyZXRzUmVzcG9uc2USKQoHc2VjcmV0cxgBIAMoCzIYLmNvbXBhc3MudjEuU2VjcmV0U3RhdHVzIqABCgxTZWNyZXRTdGF0dXMSDAoEbmFtZRgBIAEoCRIOCgZpc19zZXQYAiABKAgSLAoIZGVsaXZlcnkYAyABKA4yGi5jb21wYXNzLnYxLlNlY3JldERlbGl2ZXJ5EiQKBGtpbmQYBCABKA4yFi5jb21wYXNzLnYxLlNlY3JldEtpbmQSEAoIcHJvdmlkZXIYBSABKAkSDAoEaG9zdBgGIAEoCSIjChNEZWxldGVTZWNyZXRSZXF1ZXN0EgwKBG5hbWUYASABKAkiFgoURGVsZXRlU2VjcmV0UmVzcG9uc2UiOgoWU2V0U2VydmVyU2VjcmV0UmVxdWVzdBIMCgRuYW1lGAEgASgJEhIKBXZhbHVlGAIgASgJQgOAAQEiGQoXU2V0U2VydmVyU2VjcmV0UmVzcG9uc2UiKQoZRGVsZXRlU2VydmVyU2VjcmV0UmVxdWVzdBIMCgRuYW1lGAEgASgJIhwKGkRlbGV0ZVNlcnZlclNlY3JldFJlc3BvbnNlIhoKGExpc3RTZXJ2ZXJTZWNyZXRzUmVxdWVzdCJTChlMaXN0U2VydmVyU2VjcmV0c1Jlc3BvbnNlEjYKDnNlcnZlcl9zZWNyZXRzGAEgAygLMh4uY29tcGFzcy52MS5TZXJ2ZXJTZWNyZXRTdGF0dXMiMgoSU2VydmVyU2VjcmV0U3RhdHVzEgwKBG5hbWUYASABKAkSDgoGaXNfc2V0GAIgASgIIhYKFEdldFNlcnZlckluZm9SZXF1ZXN0Ij0KFUdldFNlcnZlckluZm9SZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJEhMKC2FwaV92ZXJzaW9uGAIgASgJIg8KDVdob0FtSVJlcXVlc3QiJAoOV2hvQW1JUmVzcG9uc2USEgoKYWNjb3VudF9pZBgBIAEoCSJDChZTdWJzY3JpYmVFdmVudHNSZXF1ZXN0EhEKCXNpbmNlX3NlcRgBIAEoBBIWCg5pbnN0YW5jZV9lcG9jaBgCIAEoBCLiAwoXU3Vic2NyaWJlRXZlbnRzUmVzcG9uc2USCwoDc2VxGAEgASgEEhIKCmF0X3VuaXhfbXMYAiABKAMSFgoOaW5zdGFuY2VfZXBvY2gYAyABKAQSFAoMc25hcHNob3Rfc2VxGAQgASgEEjEKDXNlcnZlcl9zdGF0dXMYCiABKAsyGC5jb21wYXNzLnYxLlNlcnZlclN0YXR1c0gAEjUKD3Jlc3luY19yZXF1aXJlZBgLIAEoCzIaLmNvbXBhc3MudjEuUmVzeW5jUmVxdWlyZWRIABI+ChRhZ2VudF9zZXNzaW9uX3N0YXR1cxgMIAEoCzIeLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdHVzSAASPAoTYWdlbnRfbWVzc2FnZV9jaHVuaxgNIAEoCzIdLmNvbXBhc3MudjEuQWdlbnRNZXNzYWdlQ2h1bmtIABI0Cg9hZ2VudF90b29sX2NhbGwYDiABKAsyGS5jb21wYXNzLnYxLkFnZW50VG9vbENhbGxIABIrCgphZ2VudF9wbGFuGA8gASgLMhUuY29tcGFzcy52MS5BZ2VudFBsYW5IABIiCgVpc3N1ZRgQIAEoCzIRLmNvbXBhc3MudjEuSXNzdWVIAEIJCgdwYXlsb2FkIi4KFkxpc3RCb2FyZElzc3Vlc1JlcXVlc3QSFAoMc25hcHNob3Rfc2VxGAEgASgEIjwKF0xpc3RCb2FyZElzc3Vlc1Jlc3BvbnNlEiEKBmlzc3VlcxgBIAMoCzIRLmNvbXBhc3MudjEuSXNzdWUiNgoMU2VydmVyU3RhdHVzEiYKBXN0YXRlGAEgASgOMhcuY29tcGFzcy52MS5TZXJ2ZXJTdGF0ZSIQCg5SZXN5bmNSZXF1aXJlZCLSAQoSQWdlbnRTZXNzaW9uU3RhdHVzEhIKCnNlc3Npb25faWQYASABKAkSLAoFc3RhdGUYAiABKA4yHS5jb21wYXNzLnYxLkFnZW50U2Vzc2lvblN0YXRlEhgKEGFnZW50X2FjY291bnRfaWQYAyABKAkSLQoMcnVudGltZV90aWVyGAQgASgOMhcuY29tcGFzcy52MS5SdW50aW1lVGllchIxCg5lZ3Jlc3NfcG9zdHVyZRgFIAEoDjIZLmNvbXBhc3MudjEuRWdyZXNzUG9zdHVyZSJJChFBZ2VudE1lc3NhZ2VDaHVuaxISCgpzZXNzaW9uX2lkGAEgASgJEgwKBHRleHQYAiABKAkSEgoKaXNfdGhvdWdodBgDIAEoCCJ5Cg1BZ2VudFRvb2xDYWxsEhIKCnNlc3Npb25faWQYASABKAkSFAoMdG9vbF9jYWxsX2lkGAIgASgJEg0KBXRpdGxlGAMgASgJEi8KBnN0YXR1cxgEIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cyJMCglBZ2VudFBsYW4SEgoKc2Vzc2lvbl9pZBgBIAEoCRIrCgdlbnRyaWVzGAIgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSJTCg5BZ2VudFBsYW5FbnRyeRIPCgdjb250ZW50GAEgASgJEjAKBnN0YXR1cxgCIAEoDjIgLmNvbXBhc3MudjEuQWdlbnRQbGFuRW50cnlTdGF0dXMi3wMKDFNlc3Npb25FdmVudBIQCghldmVudF9pZBgBIAEoCRISCgphdF91bml4X21zGAIgASgDEjoKDmFzc2lzdGFudF90ZXh0GAMgASgLMiAuY29tcGFzcy52MS5TZXNzaW9uQXNzaXN0YW50VGV4dEgAEi8KCHRoaW5raW5nGAQgASgLMhsuY29tcGFzcy52MS5TZXNzaW9uVGhpbmtpbmdIABIwCgl0b29sX2NhbGwYBSABKAsyGy5jb21wYXNzLnYxLlNlc3Npb25Ub29sQ2FsbEgAEj0KEHRvb2xfY2FsbF91cGRhdGUYBiABKAsyIS5jb21wYXNzLnYxLlNlc3Npb25Ub29sQ2FsbFVwZGF0ZUgAEicKBHBsYW4YByABKAsyFy5jb21wYXNzLnYxLlNlc3Npb25QbGFuSAASKwoGbm90aWNlGAggASgLMhkuY29tcGFzcy52MS5TZXNzaW9uTm90aWNlSAASOQoRc2Vzc2lvbl9pbmplY3Rpb24YCSABKAsyHC5jb21wYXNzLnYxLlNlc3Npb25JbmplY3Rpb25IABIxCg1zZXNzaW9uX2Vycm9yGAogASgLMhguY29tcGFzcy52MS5TZXNzaW9uRXJyb3JIAEIHCgVldmVudCI4ChRTZXNzaW9uQXNzaXN0YW50VGV4dBIMCgR0ZXh0GAEgASgJEhIKCm1lc3NhZ2VfaWQYAiABKAkiMwoPU2Vzc2lvblRoaW5raW5nEgwKBHRleHQYASABKAkSEgoKbWVzc2FnZV9pZBgCIAEoCSJnCg9TZXNzaW9uVG9vbENhbGwSFAoMdG9vbF9jYWxsX2lkGAEgASgJEg0KBXRpdGxlGAIgASgJEi8KBnN0YXR1cxgDIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cyKaAQoVU2Vzc2lvblRvb2xDYWxsVXBkYXRlEhQKDHRvb2xfY2FsbF9pZBgBIAEoCRIvCgZzdGF0dXMYAiABKA4yHy5jb21wYXNzLnYxLkFnZW50VG9vbENhbGxTdGF0dXMSDgoGb3V0cHV0GAMgASgJEioKBWRpZmZzGAQgAygLMhsuY29tcGFzcy52MS5TZXNzaW9uRmlsZURpZmYiVQoPU2Vzc2lvbkZpbGVEaWZmEgwKBHBhdGgYASABKAkSFQoIb2xkX3RleHQYAiABKAlIAIgBARIQCghuZXdfdGV4dBgDIAEoCUILCglfb2xkX3RleHQiOgoLU2Vzc2lvblBsYW4SKwoHZW50cmllcxgBIAMoCzIaLmNvbXBhc3MudjEuQWdlbnRQbGFuRW50cnkiOQoNU2Vzc2lvbk5vdGljZRIMCgR0ZXh0GAEgASgJEhEKBGxpbmsYAiABKAlIAIgBAUIHCgVfbGluayKDAQoQU2Vzc2lvbkluamVjdGlvbhIxCgdvcF9raW5kGAEgASgOMiAuY29tcGFzcy52MS5TZXNzaW9uSW5qZWN0aW9uS2luZBISCgptZXNzYWdlX2lkGAIgASgJEhMKC2Zyb21faGFuZGxlGAMgASgJEhMKC3RyYWNlcGFyZW50GAQgASgJImsKDFNlc3Npb25FcnJvchIqCgRraW5kGAEgASgOMhwuY29tcGFzcy52MS5TZXNzaW9uRXJyb3JLaW5kEg8KB21lc3NhZ2UYAiABKAkSEwoGc3RhdHVzGAMgASgFSACIAQFCCQoHX3N0YXR1cyIyChxTdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkifgoRQWdlbnRTZXNzaW9uRnJhbWUSEgoKc2Vzc2lvbl9pZBgBIAEoCRInCgVldmVudBgCIAEoCzIYLmNvbXBhc3MudjEuU2Vzc2lvbkV2ZW50EiwKBXN0YXRlGAMgASgOMh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0ZSJwCh5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJEg8KB3BlcnNvbmEYAyABKAkSDAoEcm9sZRgEIAEoCSI5Ch9Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlc3BvbnNlEhYKDmNvbnRhaW5lcl9uYW1lGAEgASgJIlAKG1JlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgCIAEoCSIeChxSZW1vdmVBZ2VudFdvcmtzcGFjZVJlc3BvbnNlImMKGFN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBIWCg5jb250YWluZXJfbmFtZRgBIAEoCRIZChFyZXN1bWVfc2Vzc2lvbl9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiLwoZU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJIloKEVNwYXduQWdlbnRSZXF1ZXN0EhQKDGFnZW50X2hhbmRsZRgBIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgDIAEoCUoECAIQA1IOaW5pdGlhbF9wcm9tcHQiQAoSU3Bhd25BZ2VudFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSFgoOY29udGFpbmVyX25hbWUYAiABKAkiLQoXU3RvcEFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIaChhTdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2UiLwoZUmVsb2FkQWdlbnRTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIjAKGlJlbG9hZEFnZW50U2Vzc2lvblJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkiKwoVR2V0QWdlbnRTdGF0dXNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiSgoWR2V0QWdlbnRTdGF0dXNSZXNwb25zZRIwCghzdGF0dXNlcxgBIAMoCzIeLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdHVzIisKEUlzc3VlVG9rZW5SZXF1ZXN0EhYKDmFjY291bnRfaGFuZGxlGAEgASgJIiMKEklzc3VlVG9rZW5SZXNwb25zZRINCgV0b2tlbhgBIAEoCSIoChJSZXZva2VUb2tlblJlcXVlc3QSEgoFdG9rZW4YASABKAlCA4ABASIVChNSZXZva2VUb2tlblJlc3BvbnNlIicKFVB1dEFnZW50Q29uZmlnUmVxdWVzdBIOCgZidW5kbGUYASABKAwiKQoWUHV0QWdlbnRDb25maWdSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJIhsKGUdldEFnZW50Q29uZmlnSW5mb1JlcXVlc3Qi2gEKGkdldEFnZW50Q29uZmlnSW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSDgoGc2tpbGxzGAIgAygJEhIKCmV4dGVuc2lvbnMYAyADKAkSEwoLbWNwX3NlcnZlcnMYBCADKAkSFAoMaGFzX3NldHRpbmdzGAUgASgIEhUKDWhhc19hZ2VudHNfbWQYBiABKAgSDQoFcnVsZXMYByADKAkSEQoJc3ViYWdlbnRzGAggAygJEhIKCmhhc19tb2RlbHMYCSABKAgSDwoHcHJvbXB0cxgKIAMoCSIaChhEZWxldGVBZ2VudENvbmZpZ1JlcXVlc3QiGwoZRGVsZXRlQWdlbnRDb25maWdSZXNwb25zZSI0Cg5Nb2RlbENhbmRpZGF0ZRIQCghwcm92aWRlchgBIAEoCRIQCghtb2RlbF9pZBgCIAEoCSJxCg1Nb2RlbE1ldGFkYXRhEhYKDmNvbnRleHRfd2luZG93GAEgASgDEhwKFGlucHV0X2Nvc3RfbWljcm9fdXNkGAIgASgDEh0KFW91dHB1dF9jb3N0X21pY3JvX3VzZBgDIAEoAxILCgNhcGkYBCABKAkihwEKEk1vZGVsUmVnaXN0cnlFbnRyeRIUCgxkaXNwbGF5X25hbWUYASABKAkSLgoKY2FuZGlkYXRlcxgCIAMoCzIaLmNvbXBhc3MudjEuTW9kZWxDYW5kaWRhdGUSKwoIbWV0YWRhdGEYAyABKAsyGS5jb21wYXNzLnYxLk1vZGVsTWV0YWRhdGEimAEKDU1vZGVsUmVnaXN0cnkSNwoHZW50cmllcxgBIAMoCzImLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeS5FbnRyaWVzRW50cnkaTgoMRW50cmllc0VudHJ5EgsKA2tleRgBIAEoCRItCgV2YWx1ZRgCIAEoCzIeLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeUVudHJ5OgI4ASJgChdQdXRNb2RlbFJlZ2lzdHJ5UmVxdWVzdBIrCghyZWdpc3RyeRgBIAEoCzIZLmNvbXBhc3MudjEuTW9kZWxSZWdpc3RyeRIYChBleHBlY3RlZF92ZXJzaW9uGAIgASgDIisKGFB1dE1vZGVsUmVnaXN0cnlSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgDIhkKF0dldE1vZGVsUmVnaXN0cnlSZXF1ZXN0IlgKGEdldE1vZGVsUmVnaXN0cnlSZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgDEisKCHJlZ2lzdHJ5GAIgASgLMhkuY29tcGFzcy52MS5Nb2RlbFJlZ2lzdHJ5IhwKGkRlbGV0ZU1vZGVsUmVnaXN0cnlSZXF1ZXN0Ih0KG0RlbGV0ZU1vZGVsUmVnaXN0cnlSZXNwb25zZSI+ChBBZ2VudEF0dHJpYnV0aW9uEhQKDGFnZW50X2hhbmRsZRgBIAEoCRIUCgxvd25lcl9oYW5kbGUYAiABKAkiRQoIRm9yZ2VSZWYSKwoIcHJvdmlkZXIYASABKA4yGS5jb21wYXNzLnYxLkZvcmdlUHJvdmlkZXISDAoEaG9zdBgCIAEoCSLUAwoFSXNzdWUSCgoCaWQYASABKAkSIwoFZm9yZ2UYAiABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAyABKAkSDgoGbnVtYmVyGAQgASgNEg0KBXRpdGxlGAUgASgJEgwKBGJvZHkYBiABKAkSEwoLZm9yZ2Vfc3RhdGUYByABKAkSCwoDdXJsGAggASgJEisKBWFnZW50GAkgASgLMhwuY29tcGFzcy52MS5BZ2VudEF0dHJpYnV0aW9uEhUKDWZvcmdlX2FjY291bnQYCiABKAkSDgoGbGFiZWxzGAsgAygJEi4KCnVwZGF0ZWRfYXQYEyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEiUKBXN0YXRlGAwgASgOMhYuY29tcGFzcy52MS5Jc3N1ZVN0YXRlEhAKCHByaW9yaXR5GA0gASgJEhAKCGFzc2lnbmVlGA4gASgJEg8KB3N1bW1hcnkYDyABKAkSDgoGYnJhbmNoGBAgASgJEiQKA3BycxgRIAMoCzIXLmNvbXBhc3MudjEuUHVsbFJlcXVlc3QSJwoHdHJhY2tlchgSIAEoCzIWLmNvbXBhc3MudjEuVHJhY2tlclJlZiKeAwoLUHVsbFJlcXVlc3QSIwoFZm9yZ2UYASABKAsyFC5jb21wYXNzLnYxLkZvcmdlUmVmEgwKBHJlcG8YAiABKAkSDgoGbnVtYmVyGAMgASgNEg0KBXRpdGxlGAQgASgJEhMKC2ZvcmdlX3N0YXRlGAUgASgJEgsKA3VybBgGIAEoCRIQCghoZWFkX3JlZhgHIAEoCRIQCghiYXNlX3JlZhgIIAEoCRIrCgVhZ2VudBgJIAEoCzIcLmNvbXBhc3MudjEuQWdlbnRBdHRyaWJ1dGlvbhIVCg1mb3JnZV9hY2NvdW50GAogASgJEg0KBWRyYWZ0GAsgASgIEikKB2NoYW5nZWQYDCABKAsyGC5jb21wYXNzLnYxLkNoYW5nZWRTdGF0cxIpCgZjaGVja3MYDSABKAsyGS5jb21wYXNzLnYxLkNoZWNrc1N1bW1hcnkSIwoHcmV2aWV3cxgOIAMoCzISLmNvbXBhc3MudjEuUmV2aWV3EikKB3RocmVhZHMYDyADKAsyGC5jb21wYXNzLnYxLlJldmlld1RocmVhZCJTCg1DaGVja3NTdW1tYXJ5EhAKCGhlYWRfc2hhGAEgASgJEg0KBXN0YXRlGAIgASgJEiEKBmNoZWNrcxgDIAMoCzIRLmNvbXBhc3MudjEuQ2hlY2siQwoFQ2hlY2sSDAoEbmFtZRgBIAEoCRINCgVzdGF0ZRgCIAEoCRILCgN1cmwYAyABKAkSEAoIcmVxdWlyZWQYBCABKAgiQwoMQ2hhbmdlZFN0YXRzEg0KBWZpbGVzGAEgASgNEhEKCWFkZGl0aW9ucxgCIAEoDRIRCglkZWxldGlvbnMYAyABKA0iQwoKVHJhY2tlclJlZhIMCgRraW5kGAEgASgJEgoKAmlkGAIgASgJEg4KBnN0YXR1cxgDIAEoCRILCgN1cmwYBCABKAkiRwoGUmV2aWV3Eg4KBmF1dGhvchgBIAEoCRIOCgZpc19ib3QYAiABKAgSDwoHdmVyZGljdBgDIAEoCRIMCgRib2R5GAQgASgJIlUKDFJldmlld1RocmVhZBIMCgRwYXRoGAEgASgJEhAKCHJlc29sdmVkGAIgASgIEiUKCGNvbW1lbnRzGAMgAygLMhMuY29tcGFzcy52MS5Db21tZW50IjcKB0NvbW1lbnQSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIMCgRib2R5GAMgASgJKmQKDlNlY3JldERlbGl2ZXJ5Eh8KG1NFQ1JFVF9ERUxJVkVSWV9VTlNQRUNJRklFRBAAEhgKFFNFQ1JFVF9ERUxJVkVSWV9GSUxFEAESFwoTU0VDUkVUX0RFTElWRVJZX0VOVhACKnAKClNlY3JldEtpbmQSGwoXU0VDUkVUX0tJTkRfVU5TUEVDSUZJRUQQABIXChNTRUNSRVRfS0lORF9HRU5FUklDEAESGAoUU0VDUkVUX0tJTkRfUFJPVklERVIQAhISCg5TRUNSRVRfS0lORF9HSBADKkMKC1NlcnZlclN0YXRlEhwKGFNFUlZFUl9TVEFURV9VTlNQRUNJRklFRBAAEhYKElNFUlZFUl9TVEFURV9SRUFEWRABKpcBCgtSdW50aW1lVGllchIcChhSVU5USU1FX1RJRVJfVU5TUEVDSUZJRUQQABIXChNSVU5USU1FX1RJRVJfUE9ETUFOEAESGAoUUlVOVElNRV9USUVSX01JQ1JPVk0QAhIgChxSVU5USU1FX1RJRVJfQVBQTEVfQ09OVEFJTkVSEAMSFQoRUlVOVElNRV9USUVSX0hPU1QQBCpoCg1FZ3Jlc3NQb3N0dXJlEh4KGkVHUkVTU19QT1NUVVJFX1VOU1BFQ0lGSUVEEAASGAoURUdSRVNTX1BPU1RVUkVfQVJNRUQQARIdChlFR1JFU1NfUE9TVFVSRV9VTkVORk9SQ0VEEAIqggIKEUFnZW50U2Vzc2lvblN0YXRlEiMKH0FHRU5UX1NFU1NJT05fU1RBVEVfVU5TUEVDSUZJRUQQABIgChxBR0VOVF9TRVNTSU9OX1NUQVRFX1NUQVJUSU5HEAESHQoZQUdFTlRfU0VTU0lPTl9TVEFURV9SRUFEWRACEh8KG0FHRU5UX1NFU1NJT05fU1RBVEVfV09SS0lORxADEh8KG0FHRU5UX1NFU1NJT05fU1RBVEVfU1RPUFBFRBAEEh8KG0FHRU5UX1NFU1NJT05fU1RBVEVfRVJST1JFRBAFEiQKIEFHRU5UX1NFU1NJT05fU1RBVEVfRElTQ09OTkVDVEVEEAYq0gEKE0FnZW50VG9vbENhbGxTdGF0dXMSJgoiQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19VTlNQRUNJRklFRBAAEiIKHkFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfUEVORElORxABEiYKIkFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfSU5fUFJPR1JFU1MQAhIkCiBBR0VOVF9UT09MX0NBTExfU1RBVFVTX0NPTVBMRVRFRBADEiEKHUFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfRkFJTEVEEAQqtAEKFEFnZW50UGxhbkVudHJ5U3RhdHVzEicKI0FHRU5UX1BMQU5fRU5UUllfU1RBVFVTX1VOU1BFQ0lGSUVEEAASIwofQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfUEVORElORxABEicKI0FHRU5UX1BMQU5fRU5UUllfU1RBVFVTX0lOX1BST0dSRVNTEAISJQohQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfQ09NUExFVEVEEAMqhAEKFFNlc3Npb25JbmplY3Rpb25LaW5kEiYKIlNFU1NJT05fSU5KRUNUSU9OX0tJTkRfVU5TUEVDSUZJRUQQABIgChxTRVNTSU9OX0lOSkVDVElPTl9LSU5EX1NURUVSEAESIgoeU0VTU0lPTl9JTkpFQ1RJT05fS0lORF9ERUxJVkVSEAIqdAoQU2Vzc2lvbkVycm9yS2luZBIiCh5TRVNTSU9OX0VSUk9SX0tJTkRfVU5TUEVDSUZJRUQQABIcChhTRVNTSU9OX0VSUk9SX0tJTkRfRVJST1IQARIeChpTRVNTSU9OX0VSUk9SX0tJTkRfQUJPUlRFRBACKvEBCgpJc3N1ZVN0YXRlEhsKF0lTU1VFX1NUQVRFX1VOU1BFQ0lGSUVEEAASFwoTSVNTVUVfU1RBVEVfQkFDS0xPRxABEhQKEElTU1VFX1NUQVRFX1RPRE8QAhIWChJJU1NVRV9TVEFURV9RVUVVRUQQAxIXChNJU1NVRV9TVEFURV9CTE9DS0VEEAQSGwoXSVNTVUVfU1RBVEVfSU5fUFJPR1JFU1MQBRIZChVJU1NVRV9TVEFURV9JTl9SRVZJRVcQBhIUChBJU1NVRV9TVEFURV9ET05FEAcSGAoUSVNTVUVfU1RBVEVfQVJDSElWRUQQCCqcAQoNRm9yZ2VQcm92aWRlchIeChpGT1JHRV9QUk9WSURFUl9VTlNQRUNJRklFRBAAEhkKFUZPUkdFX1BST1ZJREVSX0dJVEhVQhABEhkKFUZPUkdFX1BST1ZJREVSX0dJVExBQhACEhoKFkZPUkdFX1BST1ZJREVSX0ZPUkdFSk8QAxIZChVGT1JHRV9QUk9WSURFUl9MSU5FQVIQBDLTDgoOQ29tcGFzc1NlcnZpY2USVAoNR2V0U2VydmVySW5mbxIgLmNvbXBhc3MudjEuR2V0U2VydmVySW5mb1JlcXVlc3QaIS5jb21wYXNzLnYxLkdldFNlcnZlckluZm9SZXNwb25zZRI/CgZXaG9BbUkSGS5jb21wYXNzLnYxLldob0FtSVJlcXVlc3QaGi5jb21wYXNzLnYxLldob0FtSVJlc3BvbnNlElwKD1N1YnNjcmliZUV2ZW50cxIiLmNvbXBhc3MudjEuU3Vic2NyaWJlRXZlbnRzUmVxdWVzdBojLmNvbXBhc3MudjEuU3Vic2NyaWJlRXZlbnRzUmVzcG9uc2UwARJaCg9MaXN0Qm9hcmRJc3N1ZXMSIi5jb21wYXNzLnYxLkxpc3RCb2FyZElzc3Vlc1JlcXVlc3QaIy5jb21wYXNzLnYxLkxpc3RCb2FyZElzc3Vlc1Jlc3BvbnNlEnIKF1Byb3Zpc2lvbkFnZW50V29ya3NwYWNlEiouY29tcGFzcy52MS5Qcm92aXNpb25BZ2VudFdvcmtzcGFjZVJlcXVlc3QaKy5jb21wYXNzLnYxLlByb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVzcG9uc2USYAoRU3RhcnRBZ2VudFNlc3Npb24SJC5jb21wYXNzLnYxLlN0YXJ0QWdlbnRTZXNzaW9uUmVxdWVzdBolLmNvbXBhc3MudjEuU3RhcnRBZ2VudFNlc3Npb25SZXNwb25zZRJLCgpTcGF3bkFnZW50Eh0uY29tcGFzcy52MS5TcGF3bkFnZW50UmVxdWVzdBoeLmNvbXBhc3MudjEuU3Bhd25BZ2VudFJlc3BvbnNlEl0KEFN0b3BBZ2VudFNlc3Npb24SIy5jb21wYXNzLnYxLlN0b3BBZ2VudFNlc3Npb25SZXF1ZXN0GiQuY29tcGFzcy52MS5TdG9wQWdlbnRTZXNzaW9uUmVzcG9uc2USaQoUUmVtb3ZlQWdlbnRXb3Jrc3BhY2USJy5jb21wYXNzLnYxLlJlbW92ZUFnZW50V29ya3NwYWNlUmVxdWVzdBooLmNvbXBhc3MudjEuUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJjChJSZWxvYWRBZ2VudFNlc3Npb24SJS5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlcXVlc3QaJi5jb21wYXNzLnYxLlJlbG9hZEFnZW50U2Vzc2lvblJlc3BvbnNlElcKDkdldEFnZW50U3RhdHVzEiEuY29tcGFzcy52MS5HZXRBZ2VudFN0YXR1c1JlcXVlc3QaIi5jb21wYXNzLnYxLkdldEFnZW50U3RhdHVzUmVzcG9uc2USYgoVU3Vic2NyaWJlQWdlbnRTZXNzaW9uEiguY29tcGFzcy52MS5TdWJzY3JpYmVBZ2VudFNlc3Npb25SZXF1ZXN0Gh0uY29tcGFzcy52MS5BZ2VudFNlc3Npb25GcmFtZTABEksKCklzc3VlVG9rZW4SHS5jb21wYXNzLnYxLklzc3VlVG9rZW5SZXF1ZXN0Gh4uY29tcGFzcy52MS5Jc3N1ZVRva2VuUmVzcG9uc2USTgoLUmV2b2tlVG9rZW4SHi5jb21wYXNzLnYxLlJldm9rZVRva2VuUmVxdWVzdBofLmNvbXBhc3MudjEuUmV2b2tlVG9rZW5SZXNwb25zZRJXCg5QdXRBZ2VudENvbmZpZxIhLmNvbXBhc3MudjEuUHV0QWdlbnRDb25maWdSZXF1ZXN0GiIuY29tcGFzcy52MS5QdXRBZ2VudENvbmZpZ1Jlc3BvbnNlEmMKEkdldEFnZW50Q29uZmlnSW5mbxIlLmNvbXBhc3MudjEuR2V0QWdlbnRDb25maWdJbmZvUmVxdWVzdBomLmNvbXBhc3MudjEuR2V0QWdlbnRDb25maWdJbmZvUmVzcG9uc2USYAoRRGVsZXRlQWdlbnRDb25maWcSJC5jb21wYXNzLnYxLkRlbGV0ZUFnZW50Q29uZmlnUmVxdWVzdBolLmNvbXBhc3MudjEuRGVsZXRlQWdlbnRDb25maWdSZXNwb25zZRJdChBQdXRNb2RlbFJlZ2lzdHJ5EiMuY29tcGFzcy52MS5QdXRNb2RlbFJlZ2lzdHJ5UmVxdWVzdBokLmNvbXBhc3MudjEuUHV0TW9kZWxSZWdpc3RyeVJlc3BvbnNlEl0KEEdldE1vZGVsUmVnaXN0cnkSIy5jb21wYXNzLnYxLkdldE1vZGVsUmVnaXN0cnlSZXF1ZXN0GiQuY29tcGFzcy52MS5HZXRNb2RlbFJlZ2lzdHJ5UmVzcG9uc2USZgoTRGVsZXRlTW9kZWxSZWdpc3RyeRImLmNvbXBhc3MudjEuRGVsZXRlTW9kZWxSZWdpc3RyeVJlcXVlc3QaJy5jb21wYXNzLnYxLkRlbGV0ZU1vZGVsUmVnaXN0cnlSZXNwb25zZTKgBAoOU2VjcmV0c1NlcnZpY2USSAoJU2V0U2VjcmV0EhwuY29tcGFzcy52MS5TZXRTZWNyZXRSZXF1ZXN0Gh0uY29tcGFzcy52MS5TZXRTZWNyZXRSZXNwb25zZRJOCgtMaXN0U2VjcmV0cxIeLmNvbXBhc3MudjEuTGlzdFNlY3JldHNSZXF1ZXN0Gh8uY29tcGFzcy52MS5MaXN0U2VjcmV0c1Jlc3BvbnNlElEKDERlbGV0ZVNlY3JldBIfLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVxdWVzdBogLmNvbXBhc3MudjEuRGVsZXRlU2VjcmV0UmVzcG9uc2USWgoPU2V0U2VydmVyU2VjcmV0EiIuY29tcGFzcy52MS5TZXRTZXJ2ZXJTZWNyZXRSZXF1ZXN0GiMuY29tcGFzcy52MS5TZXRTZXJ2ZXJTZWNyZXRSZXNwb25zZRJjChJEZWxldGVTZXJ2ZXJTZWNyZXQSJS5jb21wYXNzLnYxLkRlbGV0ZVNlcnZlclNlY3JldFJlcXVlc3QaJi5jb21wYXNzLnYxLkRlbGV0ZVNlcnZlclNlY3JldFJlc3BvbnNlEmAKEUxpc3RTZXJ2ZXJTZWNyZXRzEiQuY29tcGFzcy52MS5MaXN0U2VydmVyU2VjcmV0c1JlcXVlc3QaJS5jb21wYXNzLnYxLkxpc3RTZXJ2ZXJTZWNyZXRzUmVzcG9uc2ViBnByb3RvMw", [file_google_protobuf_timestamp]); + fileDesc("Chhjb21wYXNzL3YxL2NvbXBhc3MucHJvdG8SCmNvbXBhc3MudjEi0AEKEFNldFNlY3JldFJlcXVlc3QSDAoEbmFtZRgBIAEoCRISCgV2YWx1ZRgCIAEoCUIDgAEBEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkSJgoFc2NvcGUYByABKA4yFy5jb21wYXNzLnYxLlNlY3JldFNjb3BlIhMKEVNldFNlY3JldFJlc3BvbnNlIhQKEkxpc3RTZWNyZXRzUmVxdWVzdCJAChNMaXN0U2VjcmV0c1Jlc3BvbnNlEikKB3NlY3JldHMYASADKAsyGC5jb21wYXNzLnYxLlNlY3JldFN0YXR1cyKgAQoMU2VjcmV0U3RhdHVzEgwKBG5hbWUYASABKAkSDgoGaXNfc2V0GAIgASgIEiwKCGRlbGl2ZXJ5GAMgASgOMhouY29tcGFzcy52MS5TZWNyZXREZWxpdmVyeRIkCgRraW5kGAQgASgOMhYuY29tcGFzcy52MS5TZWNyZXRLaW5kEhAKCHByb3ZpZGVyGAUgASgJEgwKBGhvc3QYBiABKAkiSwoTRGVsZXRlU2VjcmV0UmVxdWVzdBIMCgRuYW1lGAEgASgJEiYKBXNjb3BlGAIgASgOMhcuY29tcGFzcy52MS5TZWNyZXRTY29wZSIWChREZWxldGVTZWNyZXRSZXNwb25zZSI6ChZTZXRTZXJ2ZXJTZWNyZXRSZXF1ZXN0EgwKBG5hbWUYASABKAkSEgoFdmFsdWUYAiABKAlCA4ABASIZChdTZXRTZXJ2ZXJTZWNyZXRSZXNwb25zZSIpChlEZWxldGVTZXJ2ZXJTZWNyZXRSZXF1ZXN0EgwKBG5hbWUYASABKAkiHAoaRGVsZXRlU2VydmVyU2VjcmV0UmVzcG9uc2UiGgoYTGlzdFNlcnZlclNlY3JldHNSZXF1ZXN0IlMKGUxpc3RTZXJ2ZXJTZWNyZXRzUmVzcG9uc2USNgoOc2VydmVyX3NlY3JldHMYASADKAsyHi5jb21wYXNzLnYxLlNlcnZlclNlY3JldFN0YXR1cyIyChJTZXJ2ZXJTZWNyZXRTdGF0dXMSDAoEbmFtZRgBIAEoCRIOCgZpc19zZXQYAiABKAgiFgoUR2V0U2VydmVySW5mb1JlcXVlc3QiPQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSEwoLYXBpX3ZlcnNpb24YAiABKAkiDwoNV2hvQW1JUmVxdWVzdCIkCg5XaG9BbUlSZXNwb25zZRISCgphY2NvdW50X2lkGAEgASgJIkMKFlN1YnNjcmliZUV2ZW50c1JlcXVlc3QSEQoJc2luY2Vfc2VxGAEgASgEEhYKDmluc3RhbmNlX2Vwb2NoGAIgASgEIuIDChdTdWJzY3JpYmVFdmVudHNSZXNwb25zZRILCgNzZXEYASABKAQSEgoKYXRfdW5peF9tcxgCIAEoAxIWCg5pbnN0YW5jZV9lcG9jaBgDIAEoBBIUCgxzbmFwc2hvdF9zZXEYBCABKAQSMQoNc2VydmVyX3N0YXR1cxgKIAEoCzIYLmNvbXBhc3MudjEuU2VydmVyU3RhdHVzSAASNQoPcmVzeW5jX3JlcXVpcmVkGAsgASgLMhouY29tcGFzcy52MS5SZXN5bmNSZXF1aXJlZEgAEj4KFGFnZW50X3Nlc3Npb25fc3RhdHVzGAwgASgLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXNIABI8ChNhZ2VudF9tZXNzYWdlX2NodW5rGA0gASgLMh0uY29tcGFzcy52MS5BZ2VudE1lc3NhZ2VDaHVua0gAEjQKD2FnZW50X3Rvb2xfY2FsbBgOIAEoCzIZLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbEgAEisKCmFnZW50X3BsYW4YDyABKAsyFS5jb21wYXNzLnYxLkFnZW50UGxhbkgAEiIKBWlzc3VlGBAgASgLMhEuY29tcGFzcy52MS5Jc3N1ZUgAQgkKB3BheWxvYWQiLgoWTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBIUCgxzbmFwc2hvdF9zZXEYASABKAQiPAoXTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2USIQoGaXNzdWVzGAEgAygLMhEuY29tcGFzcy52MS5Jc3N1ZSI2CgxTZXJ2ZXJTdGF0dXMSJgoFc3RhdGUYASABKA4yFy5jb21wYXNzLnYxLlNlcnZlclN0YXRlIhAKDlJlc3luY1JlcXVpcmVkItIBChJBZ2VudFNlc3Npb25TdGF0dXMSEgoKc2Vzc2lvbl9pZBgBIAEoCRIsCgVzdGF0ZRgCIAEoDjIdLmNvbXBhc3MudjEuQWdlbnRTZXNzaW9uU3RhdGUSGAoQYWdlbnRfYWNjb3VudF9pZBgDIAEoCRItCgxydW50aW1lX3RpZXIYBCABKA4yFy5jb21wYXNzLnYxLlJ1bnRpbWVUaWVyEjEKDmVncmVzc19wb3N0dXJlGAUgASgOMhkuY29tcGFzcy52MS5FZ3Jlc3NQb3N0dXJlIkkKEUFnZW50TWVzc2FnZUNodW5rEhIKCnNlc3Npb25faWQYASABKAkSDAoEdGV4dBgCIAEoCRISCgppc190aG91Z2h0GAMgASgIInkKDUFnZW50VG9vbENhbGwSEgoKc2Vzc2lvbl9pZBgBIAEoCRIUCgx0b29sX2NhbGxfaWQYAiABKAkSDQoFdGl0bGUYAyABKAkSLwoGc3RhdHVzGAQgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIkwKCUFnZW50UGxhbhISCgpzZXNzaW9uX2lkGAEgASgJEisKB2VudHJpZXMYAiADKAsyGi5jb21wYXNzLnYxLkFnZW50UGxhbkVudHJ5IlMKDkFnZW50UGxhbkVudHJ5Eg8KB2NvbnRlbnQYASABKAkSMAoGc3RhdHVzGAIgASgOMiAuY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeVN0YXR1cyLfAwoMU2Vzc2lvbkV2ZW50EhAKCGV2ZW50X2lkGAEgASgJEhIKCmF0X3VuaXhfbXMYAiABKAMSOgoOYXNzaXN0YW50X3RleHQYAyABKAsyIC5jb21wYXNzLnYxLlNlc3Npb25Bc3Npc3RhbnRUZXh0SAASLwoIdGhpbmtpbmcYBCABKAsyGy5jb21wYXNzLnYxLlNlc3Npb25UaGlua2luZ0gAEjAKCXRvb2xfY2FsbBgFIAEoCzIbLmNvbXBhc3MudjEuU2Vzc2lvblRvb2xDYWxsSAASPQoQdG9vbF9jYWxsX3VwZGF0ZRgGIAEoCzIhLmNvbXBhc3MudjEuU2Vzc2lvblRvb2xDYWxsVXBkYXRlSAASJwoEcGxhbhgHIAEoCzIXLmNvbXBhc3MudjEuU2Vzc2lvblBsYW5IABIrCgZub3RpY2UYCCABKAsyGS5jb21wYXNzLnYxLlNlc3Npb25Ob3RpY2VIABI5ChFzZXNzaW9uX2luamVjdGlvbhgJIAEoCzIcLmNvbXBhc3MudjEuU2Vzc2lvbkluamVjdGlvbkgAEjEKDXNlc3Npb25fZXJyb3IYCiABKAsyGC5jb21wYXNzLnYxLlNlc3Npb25FcnJvckgAQgcKBWV2ZW50IjgKFFNlc3Npb25Bc3Npc3RhbnRUZXh0EgwKBHRleHQYASABKAkSEgoKbWVzc2FnZV9pZBgCIAEoCSIzCg9TZXNzaW9uVGhpbmtpbmcSDAoEdGV4dBgBIAEoCRISCgptZXNzYWdlX2lkGAIgASgJImcKD1Nlc3Npb25Ub29sQ2FsbBIUCgx0b29sX2NhbGxfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSLwoGc3RhdHVzGAMgASgOMh8uY29tcGFzcy52MS5BZ2VudFRvb2xDYWxsU3RhdHVzIpoBChVTZXNzaW9uVG9vbENhbGxVcGRhdGUSFAoMdG9vbF9jYWxsX2lkGAEgASgJEi8KBnN0YXR1cxgCIAEoDjIfLmNvbXBhc3MudjEuQWdlbnRUb29sQ2FsbFN0YXR1cxIOCgZvdXRwdXQYAyABKAkSKgoFZGlmZnMYBCADKAsyGy5jb21wYXNzLnYxLlNlc3Npb25GaWxlRGlmZiJVCg9TZXNzaW9uRmlsZURpZmYSDAoEcGF0aBgBIAEoCRIVCghvbGRfdGV4dBgCIAEoCUgAiAEBEhAKCG5ld190ZXh0GAMgASgJQgsKCV9vbGRfdGV4dCI6CgtTZXNzaW9uUGxhbhIrCgdlbnRyaWVzGAEgAygLMhouY29tcGFzcy52MS5BZ2VudFBsYW5FbnRyeSI5Cg1TZXNzaW9uTm90aWNlEgwKBHRleHQYASABKAkSEQoEbGluaxgCIAEoCUgAiAEBQgcKBV9saW5rIoMBChBTZXNzaW9uSW5qZWN0aW9uEjEKB29wX2tpbmQYASABKA4yIC5jb21wYXNzLnYxLlNlc3Npb25JbmplY3Rpb25LaW5kEhIKCm1lc3NhZ2VfaWQYAiABKAkSEwoLZnJvbV9oYW5kbGUYAyABKAkSEwoLdHJhY2VwYXJlbnQYBCABKAkiawoMU2Vzc2lvbkVycm9yEioKBGtpbmQYASABKA4yHC5jb21wYXNzLnYxLlNlc3Npb25FcnJvcktpbmQSDwoHbWVzc2FnZRgCIAEoCRITCgZzdGF0dXMYAyABKAVIAIgBAUIJCgdfc3RhdHVzIjIKHFN1YnNjcmliZUFnZW50U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSJ+ChFBZ2VudFNlc3Npb25GcmFtZRISCgpzZXNzaW9uX2lkGAEgASgJEicKBWV2ZW50GAIgASgLMhguY29tcGFzcy52MS5TZXNzaW9uRXZlbnQSLAoFc3RhdGUYAyABKA4yHS5jb21wYXNzLnYxLkFnZW50U2Vzc2lvblN0YXRlInAKHlByb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVxdWVzdBIUCgxhZ2VudF9oYW5kbGUYASABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAiABKAkSDwoHcGVyc29uYRgDIAEoCRIMCgRyb2xlGAQgASgJIjkKH1Byb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVzcG9uc2USFgoOY29udGFpbmVyX25hbWUYASABKAkiUAobUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0EhYKDmNvbnRhaW5lcl9uYW1lGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJIh4KHFJlbW92ZUFnZW50V29ya3NwYWNlUmVzcG9uc2UiYwoYU3RhcnRBZ2VudFNlc3Npb25SZXF1ZXN0EhYKDmNvbnRhaW5lcl9uYW1lGAEgASgJEhkKEXJlc3VtZV9zZXNzaW9uX2lkGAMgASgJSgQIAhADUg5pbml0aWFsX3Byb21wdCIvChlTdGFydEFnZW50U2Vzc2lvblJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkiWgoRU3Bhd25BZ2VudFJlcXVlc3QSFAoMYWdlbnRfaGFuZGxlGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAMgASgJSgQIAhADUg5pbml0aWFsX3Byb21wdCJAChJTcGF3bkFnZW50UmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRIWCg5jb250YWluZXJfbmFtZRgCIAEoCSItChdTdG9wQWdlbnRTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIhoKGFN0b3BBZ2VudFNlc3Npb25SZXNwb25zZSIvChlSZWxvYWRBZ2VudFNlc3Npb25SZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiMAoaUmVsb2FkQWdlbnRTZXNzaW9uUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCSIrChVHZXRBZ2VudFN0YXR1c1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSJKChZHZXRBZ2VudFN0YXR1c1Jlc3BvbnNlEjAKCHN0YXR1c2VzGAEgAygLMh4uY29tcGFzcy52MS5BZ2VudFNlc3Npb25TdGF0dXMiKwoRSXNzdWVUb2tlblJlcXVlc3QSFgoOYWNjb3VudF9oYW5kbGUYASABKAkiIwoSSXNzdWVUb2tlblJlc3BvbnNlEg0KBXRva2VuGAEgASgJIigKElJldm9rZVRva2VuUmVxdWVzdBISCgV0b2tlbhgBIAEoCUIDgAEBIhUKE1Jldm9rZVRva2VuUmVzcG9uc2UiJwoVUHV0QWdlbnRDb25maWdSZXF1ZXN0Eg4KBmJ1bmRsZRgBIAEoDCIpChZQdXRBZ2VudENvbmZpZ1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkiGwoZR2V0QWdlbnRDb25maWdJbmZvUmVxdWVzdCLaAQoaR2V0QWdlbnRDb25maWdJbmZvUmVzcG9uc2USDwoHdmVyc2lvbhgBIAEoCRIOCgZza2lsbHMYAiADKAkSEgoKZXh0ZW5zaW9ucxgDIAMoCRITCgttY3Bfc2VydmVycxgEIAMoCRIUCgxoYXNfc2V0dGluZ3MYBSABKAgSFQoNaGFzX2FnZW50c19tZBgGIAEoCBINCgVydWxlcxgHIAMoCRIRCglzdWJhZ2VudHMYCCADKAkSEgoKaGFzX21vZGVscxgJIAEoCBIPCgdwcm9tcHRzGAogAygJIhoKGERlbGV0ZUFnZW50Q29uZmlnUmVxdWVzdCIbChlEZWxldGVBZ2VudENvbmZpZ1Jlc3BvbnNlIjQKDk1vZGVsQ2FuZGlkYXRlEhAKCHByb3ZpZGVyGAEgASgJEhAKCG1vZGVsX2lkGAIgASgJInEKDU1vZGVsTWV0YWRhdGESFgoOY29udGV4dF93aW5kb3cYASABKAMSHAoUaW5wdXRfY29zdF9taWNyb191c2QYAiABKAMSHQoVb3V0cHV0X2Nvc3RfbWljcm9fdXNkGAMgASgDEgsKA2FwaRgEIAEoCSKHAQoSTW9kZWxSZWdpc3RyeUVudHJ5EhQKDGRpc3BsYXlfbmFtZRgBIAEoCRIuCgpjYW5kaWRhdGVzGAIgAygLMhouY29tcGFzcy52MS5Nb2RlbENhbmRpZGF0ZRIrCghtZXRhZGF0YRgDIAEoCzIZLmNvbXBhc3MudjEuTW9kZWxNZXRhZGF0YSKYAQoNTW9kZWxSZWdpc3RyeRI3CgdlbnRyaWVzGAEgAygLMiYuY29tcGFzcy52MS5Nb2RlbFJlZ2lzdHJ5LkVudHJpZXNFbnRyeRpOCgxFbnRyaWVzRW50cnkSCwoDa2V5GAEgASgJEi0KBXZhbHVlGAIgASgLMh4uY29tcGFzcy52MS5Nb2RlbFJlZ2lzdHJ5RW50cnk6AjgBImAKF1B1dE1vZGVsUmVnaXN0cnlSZXF1ZXN0EisKCHJlZ2lzdHJ5GAEgASgLMhkuY29tcGFzcy52MS5Nb2RlbFJlZ2lzdHJ5EhgKEGV4cGVjdGVkX3ZlcnNpb24YAiABKAMiKwoYUHV0TW9kZWxSZWdpc3RyeVJlc3BvbnNlEg8KB3ZlcnNpb24YASABKAMiGQoXR2V0TW9kZWxSZWdpc3RyeVJlcXVlc3QiWAoYR2V0TW9kZWxSZWdpc3RyeVJlc3BvbnNlEg8KB3ZlcnNpb24YASABKAMSKwoIcmVnaXN0cnkYAiABKAsyGS5jb21wYXNzLnYxLk1vZGVsUmVnaXN0cnkiHAoaRGVsZXRlTW9kZWxSZWdpc3RyeVJlcXVlc3QiHQobRGVsZXRlTW9kZWxSZWdpc3RyeVJlc3BvbnNlIj4KEEFnZW50QXR0cmlidXRpb24SFAoMYWdlbnRfaGFuZGxlGAEgASgJEhQKDG93bmVyX2hhbmRsZRgCIAEoCSJFCghGb3JnZVJlZhIrCghwcm92aWRlchgBIAEoDjIZLmNvbXBhc3MudjEuRm9yZ2VQcm92aWRlchIMCgRob3N0GAIgASgJItQDCgVJc3N1ZRIKCgJpZBgBIAEoCRIjCgVmb3JnZRgCIAEoCzIULmNvbXBhc3MudjEuRm9yZ2VSZWYSDAoEcmVwbxgDIAEoCRIOCgZudW1iZXIYBCABKA0SDQoFdGl0bGUYBSABKAkSDAoEYm9keRgGIAEoCRITCgtmb3JnZV9zdGF0ZRgHIAEoCRILCgN1cmwYCCABKAkSKwoFYWdlbnQYCSABKAsyHC5jb21wYXNzLnYxLkFnZW50QXR0cmlidXRpb24SFQoNZm9yZ2VfYWNjb3VudBgKIAEoCRIOCgZsYWJlbHMYCyADKAkSLgoKdXBkYXRlZF9hdBgTIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASJQoFc3RhdGUYDCABKA4yFi5jb21wYXNzLnYxLklzc3VlU3RhdGUSEAoIcHJpb3JpdHkYDSABKAkSEAoIYXNzaWduZWUYDiABKAkSDwoHc3VtbWFyeRgPIAEoCRIOCgZicmFuY2gYECABKAkSJAoDcHJzGBEgAygLMhcuY29tcGFzcy52MS5QdWxsUmVxdWVzdBInCgd0cmFja2VyGBIgASgLMhYuY29tcGFzcy52MS5UcmFja2VyUmVmIp4DCgtQdWxsUmVxdWVzdBIjCgVmb3JnZRgBIAEoCzIULmNvbXBhc3MudjEuRm9yZ2VSZWYSDAoEcmVwbxgCIAEoCRIOCgZudW1iZXIYAyABKA0SDQoFdGl0bGUYBCABKAkSEwoLZm9yZ2Vfc3RhdGUYBSABKAkSCwoDdXJsGAYgASgJEhAKCGhlYWRfcmVmGAcgASgJEhAKCGJhc2VfcmVmGAggASgJEisKBWFnZW50GAkgASgLMhwuY29tcGFzcy52MS5BZ2VudEF0dHJpYnV0aW9uEhUKDWZvcmdlX2FjY291bnQYCiABKAkSDQoFZHJhZnQYCyABKAgSKQoHY2hhbmdlZBgMIAEoCzIYLmNvbXBhc3MudjEuQ2hhbmdlZFN0YXRzEikKBmNoZWNrcxgNIAEoCzIZLmNvbXBhc3MudjEuQ2hlY2tzU3VtbWFyeRIjCgdyZXZpZXdzGA4gAygLMhIuY29tcGFzcy52MS5SZXZpZXcSKQoHdGhyZWFkcxgPIAMoCzIYLmNvbXBhc3MudjEuUmV2aWV3VGhyZWFkIlMKDUNoZWNrc1N1bW1hcnkSEAoIaGVhZF9zaGEYASABKAkSDQoFc3RhdGUYAiABKAkSIQoGY2hlY2tzGAMgAygLMhEuY29tcGFzcy52MS5DaGVjayJDCgVDaGVjaxIMCgRuYW1lGAEgASgJEg0KBXN0YXRlGAIgASgJEgsKA3VybBgDIAEoCRIQCghyZXF1aXJlZBgEIAEoCCJDCgxDaGFuZ2VkU3RhdHMSDQoFZmlsZXMYASABKA0SEQoJYWRkaXRpb25zGAIgASgNEhEKCWRlbGV0aW9ucxgDIAEoDSJDCgpUcmFja2VyUmVmEgwKBGtpbmQYASABKAkSCgoCaWQYAiABKAkSDgoGc3RhdHVzGAMgASgJEgsKA3VybBgEIAEoCSJHCgZSZXZpZXcSDgoGYXV0aG9yGAEgASgJEg4KBmlzX2JvdBgCIAEoCBIPCgd2ZXJkaWN0GAMgASgJEgwKBGJvZHkYBCABKAkiVQoMUmV2aWV3VGhyZWFkEgwKBHBhdGgYASABKAkSEAoIcmVzb2x2ZWQYAiABKAgSJQoIY29tbWVudHMYAyADKAsyEy5jb21wYXNzLnYxLkNvbW1lbnQiNwoHQ29tbWVudBIOCgZhdXRob3IYASABKAkSDgoGaXNfYm90GAIgASgIEgwKBGJvZHkYAyABKAkqZAoOU2VjcmV0RGVsaXZlcnkSHwobU0VDUkVUX0RFTElWRVJZX1VOU1BFQ0lGSUVEEAASGAoUU0VDUkVUX0RFTElWRVJZX0ZJTEUQARIXChNTRUNSRVRfREVMSVZFUllfRU5WEAIqcAoKU2VjcmV0S2luZBIbChdTRUNSRVRfS0lORF9VTlNQRUNJRklFRBAAEhcKE1NFQ1JFVF9LSU5EX0dFTkVSSUMQARIYChRTRUNSRVRfS0lORF9QUk9WSURFUhACEhIKDlNFQ1JFVF9LSU5EX0dIEAMqWwoLU2VjcmV0U2NvcGUSHAoYU0VDUkVUX1NDT1BFX1VOU1BFQ0lGSUVEEAASFQoRU0VDUkVUX1NDT1BFX1VTRVIQARIXChNTRUNSRVRfU0NPUEVfVEVOQU5UEAIqQwoLU2VydmVyU3RhdGUSHAoYU0VSVkVSX1NUQVRFX1VOU1BFQ0lGSUVEEAASFgoSU0VSVkVSX1NUQVRFX1JFQURZEAEqlwEKC1J1bnRpbWVUaWVyEhwKGFJVTlRJTUVfVElFUl9VTlNQRUNJRklFRBAAEhcKE1JVTlRJTUVfVElFUl9QT0RNQU4QARIYChRSVU5USU1FX1RJRVJfTUlDUk9WTRACEiAKHFJVTlRJTUVfVElFUl9BUFBMRV9DT05UQUlORVIQAxIVChFSVU5USU1FX1RJRVJfSE9TVBAEKmgKDUVncmVzc1Bvc3R1cmUSHgoaRUdSRVNTX1BPU1RVUkVfVU5TUEVDSUZJRUQQABIYChRFR1JFU1NfUE9TVFVSRV9BUk1FRBABEh0KGUVHUkVTU19QT1NUVVJFX1VORU5GT1JDRUQQAiqCAgoRQWdlbnRTZXNzaW9uU3RhdGUSIwofQUdFTlRfU0VTU0lPTl9TVEFURV9VTlNQRUNJRklFRBAAEiAKHEFHRU5UX1NFU1NJT05fU1RBVEVfU1RBUlRJTkcQARIdChlBR0VOVF9TRVNTSU9OX1NUQVRFX1JFQURZEAISHwobQUdFTlRfU0VTU0lPTl9TVEFURV9XT1JLSU5HEAMSHwobQUdFTlRfU0VTU0lPTl9TVEFURV9TVE9QUEVEEAQSHwobQUdFTlRfU0VTU0lPTl9TVEFURV9FUlJPUkVEEAUSJAogQUdFTlRfU0VTU0lPTl9TVEFURV9ESVNDT05ORUNURUQQBirSAQoTQWdlbnRUb29sQ2FsbFN0YXR1cxImCiJBR0VOVF9UT09MX0NBTExfU1RBVFVTX1VOU1BFQ0lGSUVEEAASIgoeQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19QRU5ESU5HEAESJgoiQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19JTl9QUk9HUkVTUxACEiQKIEFHRU5UX1RPT0xfQ0FMTF9TVEFUVVNfQ09NUExFVEVEEAMSIQodQUdFTlRfVE9PTF9DQUxMX1NUQVRVU19GQUlMRUQQBCq0AQoUQWdlbnRQbGFuRW50cnlTdGF0dXMSJwojQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfVU5TUEVDSUZJRUQQABIjCh9BR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19QRU5ESU5HEAESJwojQUdFTlRfUExBTl9FTlRSWV9TVEFUVVNfSU5fUFJPR1JFU1MQAhIlCiFBR0VOVF9QTEFOX0VOVFJZX1NUQVRVU19DT01QTEVURUQQAyqEAQoUU2Vzc2lvbkluamVjdGlvbktpbmQSJgoiU0VTU0lPTl9JTkpFQ1RJT05fS0lORF9VTlNQRUNJRklFRBAAEiAKHFNFU1NJT05fSU5KRUNUSU9OX0tJTkRfU1RFRVIQARIiCh5TRVNTSU9OX0lOSkVDVElPTl9LSU5EX0RFTElWRVIQAip0ChBTZXNzaW9uRXJyb3JLaW5kEiIKHlNFU1NJT05fRVJST1JfS0lORF9VTlNQRUNJRklFRBAAEhwKGFNFU1NJT05fRVJST1JfS0lORF9FUlJPUhABEh4KGlNFU1NJT05fRVJST1JfS0lORF9BQk9SVEVEEAIq8QEKCklzc3VlU3RhdGUSGwoXSVNTVUVfU1RBVEVfVU5TUEVDSUZJRUQQABIXChNJU1NVRV9TVEFURV9CQUNLTE9HEAESFAoQSVNTVUVfU1RBVEVfVE9ETxACEhYKEklTU1VFX1NUQVRFX1FVRVVFRBADEhcKE0lTU1VFX1NUQVRFX0JMT0NLRUQQBBIbChdJU1NVRV9TVEFURV9JTl9QUk9HUkVTUxAFEhkKFUlTU1VFX1NUQVRFX0lOX1JFVklFVxAGEhQKEElTU1VFX1NUQVRFX0RPTkUQBxIYChRJU1NVRV9TVEFURV9BUkNISVZFRBAIKpwBCg1Gb3JnZVByb3ZpZGVyEh4KGkZPUkdFX1BST1ZJREVSX1VOU1BFQ0lGSUVEEAASGQoVRk9SR0VfUFJPVklERVJfR0lUSFVCEAESGQoVRk9SR0VfUFJPVklERVJfR0lUTEFCEAISGgoWRk9SR0VfUFJPVklERVJfRk9SR0VKTxADEhkKFUZPUkdFX1BST1ZJREVSX0xJTkVBUhAEMtMOCg5Db21wYXNzU2VydmljZRJUCg1HZXRTZXJ2ZXJJbmZvEiAuY29tcGFzcy52MS5HZXRTZXJ2ZXJJbmZvUmVxdWVzdBohLmNvbXBhc3MudjEuR2V0U2VydmVySW5mb1Jlc3BvbnNlEj8KBldob0FtSRIZLmNvbXBhc3MudjEuV2hvQW1JUmVxdWVzdBoaLmNvbXBhc3MudjEuV2hvQW1JUmVzcG9uc2USXAoPU3Vic2NyaWJlRXZlbnRzEiIuY29tcGFzcy52MS5TdWJzY3JpYmVFdmVudHNSZXF1ZXN0GiMuY29tcGFzcy52MS5TdWJzY3JpYmVFdmVudHNSZXNwb25zZTABEloKD0xpc3RCb2FyZElzc3VlcxIiLmNvbXBhc3MudjEuTGlzdEJvYXJkSXNzdWVzUmVxdWVzdBojLmNvbXBhc3MudjEuTGlzdEJvYXJkSXNzdWVzUmVzcG9uc2UScgoXUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2USKi5jb21wYXNzLnYxLlByb3Zpc2lvbkFnZW50V29ya3NwYWNlUmVxdWVzdBorLmNvbXBhc3MudjEuUHJvdmlzaW9uQWdlbnRXb3Jrc3BhY2VSZXNwb25zZRJgChFTdGFydEFnZW50U2Vzc2lvbhIkLmNvbXBhc3MudjEuU3RhcnRBZ2VudFNlc3Npb25SZXF1ZXN0GiUuY29tcGFzcy52MS5TdGFydEFnZW50U2Vzc2lvblJlc3BvbnNlEksKClNwYXduQWdlbnQSHS5jb21wYXNzLnYxLlNwYXduQWdlbnRSZXF1ZXN0Gh4uY29tcGFzcy52MS5TcGF3bkFnZW50UmVzcG9uc2USXQoQU3RvcEFnZW50U2Vzc2lvbhIjLmNvbXBhc3MudjEuU3RvcEFnZW50U2Vzc2lvblJlcXVlc3QaJC5jb21wYXNzLnYxLlN0b3BBZ2VudFNlc3Npb25SZXNwb25zZRJpChRSZW1vdmVBZ2VudFdvcmtzcGFjZRInLmNvbXBhc3MudjEuUmVtb3ZlQWdlbnRXb3Jrc3BhY2VSZXF1ZXN0GiguY29tcGFzcy52MS5SZW1vdmVBZ2VudFdvcmtzcGFjZVJlc3BvbnNlEmMKElJlbG9hZEFnZW50U2Vzc2lvbhIlLmNvbXBhc3MudjEuUmVsb2FkQWdlbnRTZXNzaW9uUmVxdWVzdBomLmNvbXBhc3MudjEuUmVsb2FkQWdlbnRTZXNzaW9uUmVzcG9uc2USVwoOR2V0QWdlbnRTdGF0dXMSIS5jb21wYXNzLnYxLkdldEFnZW50U3RhdHVzUmVxdWVzdBoiLmNvbXBhc3MudjEuR2V0QWdlbnRTdGF0dXNSZXNwb25zZRJiChVTdWJzY3JpYmVBZ2VudFNlc3Npb24SKC5jb21wYXNzLnYxLlN1YnNjcmliZUFnZW50U2Vzc2lvblJlcXVlc3QaHS5jb21wYXNzLnYxLkFnZW50U2Vzc2lvbkZyYW1lMAESSwoKSXNzdWVUb2tlbhIdLmNvbXBhc3MudjEuSXNzdWVUb2tlblJlcXVlc3QaHi5jb21wYXNzLnYxLklzc3VlVG9rZW5SZXNwb25zZRJOCgtSZXZva2VUb2tlbhIeLmNvbXBhc3MudjEuUmV2b2tlVG9rZW5SZXF1ZXN0Gh8uY29tcGFzcy52MS5SZXZva2VUb2tlblJlc3BvbnNlElcKDlB1dEFnZW50Q29uZmlnEiEuY29tcGFzcy52MS5QdXRBZ2VudENvbmZpZ1JlcXVlc3QaIi5jb21wYXNzLnYxLlB1dEFnZW50Q29uZmlnUmVzcG9uc2USYwoSR2V0QWdlbnRDb25maWdJbmZvEiUuY29tcGFzcy52MS5HZXRBZ2VudENvbmZpZ0luZm9SZXF1ZXN0GiYuY29tcGFzcy52MS5HZXRBZ2VudENvbmZpZ0luZm9SZXNwb25zZRJgChFEZWxldGVBZ2VudENvbmZpZxIkLmNvbXBhc3MudjEuRGVsZXRlQWdlbnRDb25maWdSZXF1ZXN0GiUuY29tcGFzcy52MS5EZWxldGVBZ2VudENvbmZpZ1Jlc3BvbnNlEl0KEFB1dE1vZGVsUmVnaXN0cnkSIy5jb21wYXNzLnYxLlB1dE1vZGVsUmVnaXN0cnlSZXF1ZXN0GiQuY29tcGFzcy52MS5QdXRNb2RlbFJlZ2lzdHJ5UmVzcG9uc2USXQoQR2V0TW9kZWxSZWdpc3RyeRIjLmNvbXBhc3MudjEuR2V0TW9kZWxSZWdpc3RyeVJlcXVlc3QaJC5jb21wYXNzLnYxLkdldE1vZGVsUmVnaXN0cnlSZXNwb25zZRJmChNEZWxldGVNb2RlbFJlZ2lzdHJ5EiYuY29tcGFzcy52MS5EZWxldGVNb2RlbFJlZ2lzdHJ5UmVxdWVzdBonLmNvbXBhc3MudjEuRGVsZXRlTW9kZWxSZWdpc3RyeVJlc3BvbnNlMqAECg5TZWNyZXRzU2VydmljZRJICglTZXRTZWNyZXQSHC5jb21wYXNzLnYxLlNldFNlY3JldFJlcXVlc3QaHS5jb21wYXNzLnYxLlNldFNlY3JldFJlc3BvbnNlEk4KC0xpc3RTZWNyZXRzEh4uY29tcGFzcy52MS5MaXN0U2VjcmV0c1JlcXVlc3QaHy5jb21wYXNzLnYxLkxpc3RTZWNyZXRzUmVzcG9uc2USUQoMRGVsZXRlU2VjcmV0Eh8uY29tcGFzcy52MS5EZWxldGVTZWNyZXRSZXF1ZXN0GiAuY29tcGFzcy52MS5EZWxldGVTZWNyZXRSZXNwb25zZRJaCg9TZXRTZXJ2ZXJTZWNyZXQSIi5jb21wYXNzLnYxLlNldFNlcnZlclNlY3JldFJlcXVlc3QaIy5jb21wYXNzLnYxLlNldFNlcnZlclNlY3JldFJlc3BvbnNlEmMKEkRlbGV0ZVNlcnZlclNlY3JldBIlLmNvbXBhc3MudjEuRGVsZXRlU2VydmVyU2VjcmV0UmVxdWVzdBomLmNvbXBhc3MudjEuRGVsZXRlU2VydmVyU2VjcmV0UmVzcG9uc2USYAoRTGlzdFNlcnZlclNlY3JldHMSJC5jb21wYXNzLnYxLkxpc3RTZXJ2ZXJTZWNyZXRzUmVxdWVzdBolLmNvbXBhc3MudjEuTGlzdFNlcnZlclNlY3JldHNSZXNwb25zZWIGcHJvdG8z", [file_google_protobuf_timestamp]); /** * @generated from message compass.v1.SetSecretRequest @@ -59,6 +59,13 @@ export type SetSecretRequest = Message<"compass.v1.SetSecretRequest"> & { * @generated from field: string host = 6; */ host: string; + + /** + * tier the write targets; unspecified == user + * + * @generated from field: compass.v1.SecretScope scope = 7; + */ + scope: SecretScope; }; /** @@ -163,6 +170,13 @@ export type DeleteSecretRequest = Message<"compass.v1.DeleteSecretRequest"> & { * @generated from field: string name = 1; */ name: string; + + /** + * tier the delete targets; unspecified == user + * + * @generated from field: compass.v1.SecretScope scope = 2; + */ + scope: SecretScope; }; /** @@ -2720,6 +2734,43 @@ export enum SecretKind { export const SecretKindSchema: GenEnum = /*@__PURE__*/ enumDesc(file_compass_v1_compass, 1); +/** + * The tier a user-secret write targets. Names the same tiers as the store's + * SecretScope* constants but NOT the same numbers: proto reserves 0 for + * unspecified, so tenant is 2 here but 0 in the store — the handler maps + * explicitly, never casts. The unspecified default is USER: a client that omits + * the field writes its own private coordinate, never a tenant-wide value every + * other user's agents resolve. + * + * @generated from enum compass.v1.SecretScope + */ +export enum SecretScope { + /** + * treated as USER — private by default + * + * @generated from enum value: SECRET_SCOPE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: SECRET_SCOPE_USER = 1; + */ + USER = 1, + + /** + * admin-only + * + * @generated from enum value: SECRET_SCOPE_TENANT = 2; + */ + TENANT = 2, +} + +/** + * Describes the enum compass.v1.SecretScope. + */ +export const SecretScopeSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_compass_v1_compass, 2); + /** * @generated from enum compass.v1.ServerState */ @@ -2739,7 +2790,7 @@ export enum ServerState { * Describes the enum compass.v1.ServerState. */ export const ServerStateSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_compass, 2); + enumDesc(file_compass_v1_compass, 3); /** * The runtime tier an agent workload runs on. `HOST` runs agents as direct @@ -2778,7 +2829,7 @@ export enum RuntimeTier { * Describes the enum compass.v1.RuntimeTier. */ export const RuntimeTierSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_compass, 3); + enumDesc(file_compass_v1_compass, 4); /** * How an agent's egress is constrained. `UNENFORCED` means the tier cannot @@ -2809,7 +2860,7 @@ export enum EgressPosture { * Describes the enum compass.v1.EgressPosture. */ export const EgressPostureSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_compass, 4); + enumDesc(file_compass_v1_compass, 5); /** * Agent-session lifecycle states. `ERRORED` is an unexpected agent exit (OOM, @@ -2874,7 +2925,7 @@ export enum AgentSessionState { * Describes the enum compass.v1.AgentSessionState. */ export const AgentSessionStateSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_compass, 5); + enumDesc(file_compass_v1_compass, 6); /** * @generated from enum compass.v1.AgentToolCallStatus @@ -2910,7 +2961,7 @@ export enum AgentToolCallStatus { * Describes the enum compass.v1.AgentToolCallStatus. */ export const AgentToolCallStatusSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_compass, 6); + enumDesc(file_compass_v1_compass, 7); /** * @generated from enum compass.v1.AgentPlanEntryStatus @@ -2941,7 +2992,7 @@ export enum AgentPlanEntryStatus { * Describes the enum compass.v1.AgentPlanEntryStatus. */ export const AgentPlanEntryStatusSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_compass, 7); + enumDesc(file_compass_v1_compass, 8); /** * The control op-kind a SessionInjection records. Mirrors the internal @@ -2973,7 +3024,7 @@ export enum SessionInjectionKind { * Describes the enum compass.v1.SessionInjectionKind. */ export const SessionInjectionKindSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_compass, 8); + enumDesc(file_compass_v1_compass, 9); /** * The class of a SessionError. ERROR pairs with the ERRORED lifecycle @@ -3003,7 +3054,7 @@ export enum SessionErrorKind { * Describes the enum compass.v1.SessionErrorKind. */ export const SessionErrorKindSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_compass, 9); + enumDesc(file_compass_v1_compass, 10); /** * The Compass issue lifecycle, server-owned (DL-032/DL-033 + terminal ARCHIVED, @@ -3067,7 +3118,7 @@ export enum IssueState { * Describes the enum compass.v1.IssueState. */ export const IssueStateSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_compass, 10); + enumDesc(file_compass_v1_compass, 11); /** * Which forge (and which host, for self-hosted instances) an artifact lives on. @@ -3109,7 +3160,7 @@ export enum ForgeProvider { * Describes the enum compass.v1.ForgeProvider. */ export const ForgeProviderSchema: GenEnum = /*@__PURE__*/ - enumDesc(file_compass_v1_compass, 11); + enumDesc(file_compass_v1_compass, 12); /** * The Compass server service. diff --git a/proto/compass/v1/compass.proto b/proto/compass/v1/compass.proto index d11a37fa9..58c6b9bdb 100644 --- a/proto/compass/v1/compass.proto +++ b/proto/compass/v1/compass.proto @@ -195,6 +195,18 @@ enum SecretKind { SECRET_KIND_GH = 3; // gh credential; carries host } +// The tier a user-secret write targets. Names the same tiers as the store's +// SecretScope* constants but NOT the same numbers: proto reserves 0 for +// unspecified, so tenant is 2 here but 0 in the store — the handler maps +// explicitly, never casts. The unspecified default is USER: a client that omits +// the field writes its own private coordinate, never a tenant-wide value every +// other user's agents resolve. +enum SecretScope { + SECRET_SCOPE_UNSPECIFIED = 0; // treated as USER — private by default + SECRET_SCOPE_USER = 1; + SECRET_SCOPE_TENANT = 2; // admin-only +} + // User-facing secret registry writes. SetSecret/DeleteSecret are user-only // (enforced in the handler: caller account kind == User, an agent account is // rejected PermissionDenied); ListSecrets is callable by user AND agent tokens @@ -236,6 +248,7 @@ message SetSecretRequest { SecretKind kind = 4; string provider = 5; // set for SECRET_KIND_PROVIDER string host = 6; // set for SECRET_KIND_GH + SecretScope scope = 7; // tier the write targets; unspecified == user } message SetSecretResponse {} @@ -255,6 +268,7 @@ message SecretStatus { message DeleteSecretRequest { string name = 1; + SecretScope scope = 2; // tier the delete targets; unspecified == user } message DeleteSecretResponse {}