From 8751631dee18c5169a5a44ed28ca647305b46e03 Mon Sep 17 00:00:00 2001 From: mintaka Date: Sat, 12 Sep 2026 16:25:28 -0400 Subject: [PATCH 1/3] feat(server): scope user-secret writes per caller (RIG-3655) SetSecret and DeleteSecret now carry a SecretScope selector. An unspecified scope resolves to the caller's own user coordinate, so a client built before the field existed writes a private value rather than a tenant-wide one. Tenant scope is explicit and requires an admin, checked at the RPC edge where requireUser already holds the caller's role. The wire numbers deliberately differ from the store's, so the handler maps between them; a cast would turn an omitted field into a tenant write, which is the case the default exists to prevent. DeclareSecret and its InsertSecret query were scope-blind and hardcoded the tenant coordinate, so every declare landed at (0, '') no matter what the handler resolved. Both now take the coordinate, validated by the same validateScopeShape the upsert door uses. A re-set at user scope does NOT retire an existing tenant row: it writes a new primary key, and the shared value keeps resolving for every other user until an admin deletes it at tenant scope. A regression test pins that, since the intuition runs the other way. Refs RIG-3655 Co-authored-by: Matt Wilkinson --- go/cmd/compass/secret.go | 47 +- go/cmd/compass/secret_test.go | 16 +- go/gen/compass/v1/compass.pb.go | 586 ++++++++++-------- go/internal/store/db/querier.go | 6 +- go/internal/store/db/secrets.sql.go | 14 +- go/internal/store/queries/secrets.sql | 10 +- go/internal/store/secrets.go | 23 +- go/internal/store/secrets_test.go | 20 +- .../store/server_secrets_pgtest_test.go | 4 +- go/internal/store/updated_at_pgtest_test.go | 2 +- go/server/secrets_service.go | 80 ++- go/server/secrets_service_pgtest_test.go | 164 ++++- .../src/gen/compass/v1/compass_pb.ts | 73 ++- .../src/gen/compass/v1/compass_pb.ts | 73 ++- proto/compass/v1/compass.proto | 14 + 15 files changed, 787 insertions(+), 345 deletions(-) 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/store/db/querier.go b/go/internal/store/db/querier.go index 3c80e184b..700e05b4b 100644 --- a/go/internal/store/db/querier.go +++ b/go/internal/store/db/querier.go @@ -274,9 +274,9 @@ type Querier interface { // // 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 writes the value-free declaration at the scope coordinate the + // caller resolved (D9); the value columns stay NULL. Retained for the SetSecret + // caller, removed with it when the upsert becomes the sole writer. 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 diff --git a/go/internal/store/db/secrets.sql.go b/go/internal/store/db/secrets.sql.go index fe12441bf..1e44a47e4 100644 --- a/go/internal/store/db/secrets.sql.go +++ b/go/internal/store/db/secrets.sql.go @@ -80,12 +80,14 @@ func (q *Queries) DeleteSecret(ctx context.Context, arg DeleteSecretParams) (int const insertSecret = `-- name: InsertSecret :exec -INSERT INTO secrets (name, scope_kind, delivery, kind, provider, host, declared_by) -VALUES ($1, 0, $2, $3, $4, $5, $6) +INSERT INTO secrets (name, scope_kind, scope_id, delivery, kind, provider, host, declared_by) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ` type InsertSecretParams struct { Name string + ScopeKind int16 + ScopeID string Delivery int16 Kind int16 Provider string @@ -101,12 +103,14 @@ type InsertSecretParams struct { // // 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 writes the value-free declaration at the scope coordinate the +// caller resolved (D9); the value columns stay NULL. Retained for the SetSecret +// caller, removed with it when the upsert becomes the sole writer. func (q *Queries) InsertSecret(ctx context.Context, arg InsertSecretParams) error { _, err := q.db.Exec(ctx, insertSecret, arg.Name, + arg.ScopeKind, + arg.ScopeID, arg.Delivery, arg.Kind, arg.Provider, diff --git a/go/internal/store/queries/secrets.sql b/go/internal/store/queries/secrets.sql index 81ff0a439..83558c865 100644 --- a/go/internal/store/queries/secrets.sql +++ b/go/internal/store/queries/secrets.sql @@ -7,12 +7,12 @@ -- 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 writes the value-free declaration at the scope coordinate the +-- caller resolved (D9); the value columns stay NULL. Retained for the SetSecret +-- caller, removed with it when the upsert becomes the sole writer. -- name: InsertSecret :exec -INSERT INTO secrets (name, scope_kind, delivery, kind, provider, host, declared_by) -VALUES ($1, 0, $2, $3, $4, $5, $6); +INSERT INTO secrets (name, scope_kind, scope_id, delivery, kind, provider, host, declared_by) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8); -- 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..846e4ca5e 100644 --- a/go/internal/store/secrets.go +++ b/go/internal/store/secrets.go @@ -71,15 +71,15 @@ 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 { +// DeclareSecret adds a names-only registry row. 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 is ErrConflict at that COORDINATE: the PK is +// (name, scope_kind, scope_id), so one name may be declared once per scope. 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. +func (s *Store) DeclareSecret(ctx context.Context, actor AccountID, name string, scopeKind int16, scopeID 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()) } @@ -98,8 +98,13 @@ func (s *Store) DeclareSecret(ctx context.Context, actor AccountID, name string, if err := validateKindRouting(kind, provider, host); err != nil { return err } + if err := validateScopeShape(scopeKind, scopeID); err != nil { + return err + } if err := s.q.InsertSecret(ctx, db.InsertSecretParams{ Name: name, + ScopeKind: scopeKind, + ScopeID: scopeID, 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, diff --git a/go/internal/store/secrets_test.go b/go/internal/store/secrets_test.go index 4098ceb62..62022dc25 100644 --- a/go/internal/store/secrets_test.go +++ b/go/internal/store/secrets_test.go @@ -22,13 +22,13 @@ func TestDeclareSecretRoundTrip(t *testing.T) { // 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 { + if err := s.DeclareSecret(ctx, actor.ID, "ZED_TOKEN", SecretScopeTenant, "", SecretDeliveryFile, SecretKindGeneric, "", ""); err != nil { t.Fatalf("declare generic: %v", err) } - if err := s.DeclareSecret(ctx, actor.ID, "ANTHROPIC_KEY", SecretDeliveryEnv, SecretKindProvider, "anthropic", ""); err != nil { + if err := s.DeclareSecret(ctx, actor.ID, "ANTHROPIC_KEY", SecretScopeTenant, "", SecretDeliveryEnv, SecretKindProvider, "anthropic", ""); err != nil { t.Fatalf("declare provider: %v", err) } - if err := s.DeclareSecret(ctx, actor.ID, "GH_TOKEN", SecretDeliveryFile, SecretKindGH, "", "github.com"); err != nil { + if err := s.DeclareSecret(ctx, actor.ID, "GH_TOKEN", SecretScopeTenant, "", SecretDeliveryFile, SecretKindGH, "", "github.com"); err != nil { t.Fatalf("declare gh: %v", err) } @@ -73,10 +73,10 @@ func TestDeclareSecretDuplicateConflict(t *testing.T) { s := newTestStore(t) actor := mustUser(t, s, "declarer") - if err := s.DeclareSecret(ctx, actor.ID, "API_KEY", SecretDeliveryEnv, SecretKindGeneric, "", ""); err != nil { + if err := s.DeclareSecret(ctx, actor.ID, "API_KEY", SecretScopeTenant, "", SecretDeliveryEnv, SecretKindGeneric, "", ""); err != nil { t.Fatalf("first declare: %v", err) } - err := s.DeclareSecret(ctx, actor.ID, "API_KEY", SecretDeliveryFile, SecretKindGeneric, "", "") + err := s.DeclareSecret(ctx, actor.ID, "API_KEY", SecretScopeTenant, "", SecretDeliveryFile, SecretKindGeneric, "", "") sentinelIs(t, err, ErrConflict, "duplicate secret name") } @@ -86,7 +86,7 @@ func TestDeclareSecretInvalidNameRejected(t *testing.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.DeclareSecret(ctx, actor.ID, bad, SecretScopeTenant, "", SecretDeliveryEnv, SecretKindGeneric, "", "") sentinelIs(t, err, ErrInvalidArgument, "invalid secret name "+bad) } @@ -106,7 +106,7 @@ func TestDeclareSecretUnknownActorInvalid(t *testing.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.DeclareSecret(ctx, AccountID("acct-never-created"), "API_KEY", SecretScopeTenant, "", SecretDeliveryEnv, SecretKindGeneric, "", "") sentinelIs(t, err, ErrInvalidArgument, "unknown declaring account") } @@ -115,7 +115,7 @@ 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 { + if err := s.DeclareSecret(ctx, actor.ID, "API_KEY", SecretScopeTenant, "", SecretDeliveryEnv, SecretKindGeneric, "", ""); err != nil { t.Fatalf("declare: %v", err) } if err := s.DeleteSecretDeclaration(ctx, actor.ID, "API_KEY", SecretScopeTenant, ""); err != nil { @@ -164,7 +164,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.DeclareSecret(ctx, actor.ID, "API_KEY", SecretScopeTenant, "", SecretDeliveryEnv, tc.kind, tc.provider, tc.host) sentinelIs(t, err, ErrInvalidArgument, tc.name) }) } @@ -197,7 +197,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.DeclareSecret(ctx, actor.ID, tc.name, SecretScopeTenant, "", SecretDeliveryEnv, tc.kind, tc.provider, tc.host); err != nil { t.Errorf("%s: valid combo rejected: %v", tc.name, err) } } diff --git a/go/internal/store/server_secrets_pgtest_test.go b/go/internal/store/server_secrets_pgtest_test.go index 63d38c74a..a3ff65aa1 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.DeclareSecret(ctx, actor, name, SecretScopeTenant, "", SecretDeliveryEnv, SecretKindGeneric, "", "") 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.DeclareSecret(ctx, actor, "PLAIN_USER_TOKEN", SecretScopeTenant, "", SecretDeliveryEnv, SecretKindGeneric, "", ""); 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..d97532c58 100644 --- a/go/internal/store/updated_at_pgtest_test.go +++ b/go/internal/store/updated_at_pgtest_test.go @@ -123,7 +123,7 @@ func TestSecretsUpdatedAtIsLive(t *testing.T) { s := newTestStore(t) actor := mustUser(t, s, "secrets-owner") - if err := s.DeclareSecret(ctx, actor.ID, "DATABASE_URL", SecretDeliveryEnv, SecretKindGeneric, "", ""); err != nil { + if err := s.DeclareSecret(ctx, actor.ID, "DATABASE_URL", SecretScopeTenant, "", SecretDeliveryEnv, SecretKindGeneric, "", ""); err != nil { t.Fatalf("DeclareSecret: %v", err) } createdBefore, updatedBefore := secretStamps(t, s, "DATABASE_URL") diff --git a/go/server/secrets_service.go b/go/server/secrets_service.go index c054fb247..e9c4b09f7 100644 --- a/go/server/secrets_service.go +++ b/go/server/secrets_service.go @@ -107,7 +107,7 @@ 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,7 +123,12 @@ 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()) + scopeKind, scopeID, err := resolveSecretScope(msg.GetScope(), callerID, role) + if err != nil { + return nil, err + } + + declErr := s.store.DeclareSecret(ctx, callerID, msg.GetName(), scopeKind, scopeID, delivery, kind, msg.GetProvider(), msg.GetHost()) switch { case declErr == nil: // Fresh declaration. @@ -157,9 +162,10 @@ func (s *secretsService) SetSecret( // 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 { + // Roll back at the RESOLVED coordinate (D9): a fresh declaration lands + // wherever resolveSecretScope placed it, so the rollback must target the + // same coordinate, not a hardcoded tenant one. + if delErr := s.store.DeleteSecretDeclaration(ctx, callerID, msg.GetName(), scopeKind, scopeID); delErr != nil { slog.ErrorContext(ctx, "rolling back secret declaration after failed write", "err", delErr) } } @@ -216,7 +222,7 @@ 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,6 +230,10 @@ func (s *secretsService) DeleteSecret( return nil, connect.NewError(connect.CodeUnavailable, errNoResolver) } name := req.Msg.GetName() + scopeKind, scopeID, err := resolveSecretScope(req.Msg.GetScope(), callerID, role) + if err != nil { + return nil, err + } // 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 @@ -235,9 +245,7 @@ func (s *secretsService) DeleteSecret( 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 err := s.store.DeleteSecretDeclaration(ctx, callerID, name, scopeKind, scopeID); err != nil { if errors.Is(err, store.ErrNotFound) { return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("secret %q", name)) } @@ -419,29 +427,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. @@ -507,3 +523,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..08ae1468d 100644 --- a/go/server/secrets_service_pgtest_test.go +++ b/go/server/secrets_service_pgtest_test.go @@ -97,8 +97,12 @@ type secretsFixture struct { userToken string agentToken string userID store.AccountID + agentID store.AccountID adminToken string - resolver *recordingResolver + // 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. + st *store.Store + 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 @@ -156,6 +160,8 @@ func newSecretsFixture(t *testing.T) secretsFixture { agentToken: agentTok, adminToken: adminTok, userID: user.ID, + agentID: agent.ID, + st: st, resolver: resolver, serverResolver: serverResolver, signaler: signaler, @@ -179,6 +185,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) @@ -643,3 +663,145 @@ 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) + } + } +} + +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 +} 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 {} From 3694f3ee0996da05d1df9bba269e2b8e21a28db2 Mon Sep 17 00:00:00 2001 From: mintaka Date: Sat, 12 Sep 2026 23:08:57 -0400 Subject: [PATCH 2/3] feat(server): move user-secret values into Postgres (RIG-3655) Completes the T5 cutover. The scope selector previously reached only the value-free declaration registry, so the value path stayed name-keyed: a user setting a name another user already held overwrote their value, and two rows for one name made buildManifest emit a duplicate TOML key, failing FetchSecrets for every live session. SetSecret and DeleteSecret now go through the DB-backed StoreResolver at the resolved coordinate. The upsert is atomic, so the declare-then-set trio and its rollback are gone, and DeclareSecret with its InsertSecret query goes with them. Boot resolves the master key and builds the resolver from it, still failing closed when the key is absent. FetchSecrets resolves per agent, collapsing scope precedence in SQL. With the upsert the sole writer, the value columns are NOT NULL. ServeConfig.SecretProvider carries the provider URI both resolvers read, reachable in production via --secret-provider or COMPASS_SECRET_PROVIDER; empty keeps the SDK default chain. Tests set the same field rather than a test-only knob. The isolation tests now assert through the production delivery path instead of a store query no production code calls, which is what let the clobber hide. Refs RIG-3655 Co-authored-by: Matt Wilkinson --- go/cmd/compass-server/main.go | 6 + go/internal/runnerhub/commands_test.go | 8 +- go/internal/runnerhub/handler.go | 74 +++-- go/internal/runnerhub/helpers_test.go | 5 +- go/internal/runnerhub/relay_comms.go | 48 ++-- go/internal/runnerhub/secrets_test.go | 24 +- go/internal/secrets/secrets.go | 2 +- go/internal/store/db/querier.go | 21 +- go/internal/store/db/secrets.sql.go | 42 +-- go/internal/store/migrations/0001_init.sql | 11 +- go/internal/store/queries/secrets.sql | 12 +- go/internal/store/secrets.go | 53 +--- go/internal/store/secrets_test.go | 90 ++++--- go/internal/store/server_secrets.go | 4 +- .../store/server_secrets_pgtest_test.go | 4 +- go/internal/store/updated_at_pgtest_test.go | 22 +- go/server/network_door.go | 2 +- go/server/network_door_test.go | 26 ++ go/server/otel_emission_pgtest_test.go | 4 +- go/server/secrets_service.go | 165 +++++------- go/server/secrets_service_pgtest_test.go | 252 ++++++++++-------- go/server/serve.go | 109 +++++--- go/server/serve_forge_pgtest_test.go | 14 +- go/server/serve_pgtest_test.go | 36 +-- 24 files changed, 525 insertions(+), 509 deletions(-) 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/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 700e05b4b..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 scope coordinate the - // caller resolved (D9); the value columns stay NULL. Retained for the SetSecret - // caller, removed with it when the upsert becomes the sole writer. - 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 1e44a47e4..ff66b8ddf 100644 --- a/go/internal/store/db/secrets.sql.go +++ b/go/internal/store/db/secrets.sql.go @@ -78,52 +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, scope_id, delivery, kind, provider, host, declared_by) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8) +SELECT EXISTS (SELECT 1 FROM user_accounts WHERE account_id = $1) ` -type InsertSecretParams struct { - Name string - ScopeKind int16 - ScopeID 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 scope coordinate the -// caller resolved (D9); the value columns stay NULL. Retained for the SetSecret -// caller, removed with it when the upsert becomes the sole writer. -func (q *Queries) InsertSecret(ctx context.Context, arg InsertSecretParams) error { - _, err := q.db.Exec(ctx, insertSecret, - arg.Name, - arg.ScopeKind, - arg.ScopeID, - 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 83558c865..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 scope coordinate the --- caller resolved (D9); the value columns stay NULL. Retained for the SetSecret --- caller, removed with it when the upsert becomes the sole writer. --- name: InsertSecret :exec -INSERT INTO secrets (name, scope_kind, scope_id, delivery, kind, provider, host, declared_by) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8); +-- 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 846e4ca5e..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,57 +71,6 @@ type SecretDeclaration struct { UpdatedAt time.Time } -// DeclareSecret adds a names-only registry row. 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 is ErrConflict at that COORDINATE: the PK is -// (name, scope_kind, scope_id), so one name may be declared once per scope. 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. -func (s *Store) DeclareSecret(ctx context.Context, actor AccountID, name string, scopeKind int16, scopeID 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 := validateScopeShape(scopeKind, scopeID); err != nil { - return err - } - if err := s.q.InsertSecret(ctx, db.InsertSecretParams{ - Name: name, - ScopeKind: scopeKind, - ScopeID: scopeID, - 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 62022dc25..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", SecretScopeTenant, "", 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", SecretScopeTenant, "", 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", SecretScopeTenant, "", 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", SecretScopeTenant, "", 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", SecretScopeTenant, "", 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, SecretScopeTenant, "", 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", SecretScopeTenant, "", 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", SecretScopeTenant, "", 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", SecretScopeTenant, "", 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, SecretScopeTenant, "", 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 a3ff65aa1..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, SecretScopeTenant, "", 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", SecretScopeTenant, "", 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 d97532c58..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", SecretScopeTenant, "", 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 e9c4b09f7..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,23 +90,17 @@ 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], @@ -128,49 +126,16 @@ func (s *secretsService) SetSecret( return nil, err } - declErr := s.store.DeclareSecret(ctx, callerID, msg.GetName(), scopeKind, scopeID, 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)) - } - - // 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 { - // Roll back at the RESOLVED coordinate (D9): a fresh declaration lands - // wherever resolveSecretScope placed it, so the rollback must target the - // same coordinate, not a hardcoded tenant one. - if delErr := s.store.DeleteSecretDeclaration(ctx, callerID, msg.GetName(), scopeKind, scopeID); 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 @@ -215,9 +180,13 @@ 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], @@ -230,26 +199,21 @@ func (s *secretsService) DeleteSecret( return nil, connect.NewError(connect.CodeUnavailable, errNoResolver) } name := req.Msg.GetName() + 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 } - // 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)) - } - if err := s.store.DeleteSecretDeclaration(ctx, callerID, name, scopeKind, scopeID); err != nil { + // 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 @@ -474,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") } diff --git a/go/server/secrets_service_pgtest_test.go b/go/server/secrets_service_pgtest_test.go index 08ae1468d..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 @@ -100,13 +104,19 @@ type secretsFixture struct { agentID store.AccountID adminToken string // 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. - st *store.Store - 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. + // 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 } @@ -141,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) @@ -163,11 +174,28 @@ func newSecretsFixture(t *testing.T) secretsFixture { 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, @@ -206,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"]) } } @@ -266,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() @@ -278,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)) + 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"]) } -} - -// 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) - } - - // The failed fresh write must leave no orphaned declaration behind. - resp, err := f.client.ListSecrets(ctx, listReq(f.userToken)) - if err != nil { - t.Fatalf("ListSecrets = %v, want success", err) - } - for _, s := range resp.Msg.GetSecrets() { - if s.GetName() == "DB_URL" { - t.Fatal("declaration survived a failed fresh write — orphan left behind") - } - } - - // 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)) + // One row, not two: the re-set updated in place. + recs, err := f.st.SecretRecordsForAgent(ctx, f.agentID) if err != nil { - t.Fatalf("ListSecrets after recovery = %v, want success", err) + t.Fatalf("SecretRecordsForAgent: %v", err) } - var found bool - for _, s := range resp.Msg.GetSecrets() { - if s.GetName() == "DB_URL" { - found = true + n := 0 + for _, r := range recs { + if r.Name == "DB_URL" { + n++ } } - 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) } } @@ -343,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 @@ -373,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() @@ -410,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] { @@ -466,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. @@ -793,6 +775,45 @@ func TestUserScopeResetDoesNotRetireTheTenantRow(t *testing.T) { } } +// 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) @@ -805,3 +826,20 @@ func resolvedNames(t *testing.T, ctx context.Context, f secretsFixture, agent st } 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) From 6ceb365ca5b6757ba85c8552d2ab4654a5b5af04 Mon Sep 17 00:00:00 2001 From: mintaka Date: Sun, 13 Sep 2026 16:13:24 -0400 Subject: [PATCH 3/3] ci(e2e): stage the secretspec cdylib and seed a master key T5 resolves the master key unconditionally at boot, so the e2e stack's compass-server now fails closed before serving: the job staged neither the cdylib the secrets read path dlopens nor a resolvable key, and every leg failed in shared fixture stand-up. Stages the cdylib with the pgtest job's recipe and seeds an obviously-fake key into a dotenv the podman user can read, reaching the spawned server through the provider env it already inherits. Refs RIG-3655 Co-authored-by: Matt Wilkinson --- .github/workflows/ci.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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