Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions go/cmd/compass-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -267,6 +268,7 @@ type serveFlags struct {
s3Region *string
s3UseTLS *bool
stateDir *string
secretProvider *string
adminHandle *string
corsAllowedOrigin *string
publicURL *string
Expand Down Expand Up @@ -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 "+
Expand Down
47 changes: 41 additions & 6 deletions go/cmd/compass/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 <NAME>",
Short: "Declare a secret and write its value (value read from stdin, admin)",
Expand All @@ -69,6 +72,7 @@ func newSecretSetCmd() *cobra.Command {
kind: kind,
provider: provider,
host: host,
scope: scope,
}, cmd.InOrStdin(), cmd.OutOrStdout())
},
}
Expand All @@ -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
}

Expand All @@ -102,7 +108,8 @@ func newSecretListCmd() *cobra.Command {

// newSecretDeleteCmd builds `secret delete <NAME>`: DeleteSecret and confirm.
func newSecretDeleteCmd() *cobra.Command {
return &cobra.Command{
var scope string
cmd := &cobra.Command{
Use: "delete <NAME>",
Short: "Delete a declared secret (admin)",
Args: cobra.ExactArgs(1),
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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)
}
Expand Down Expand Up @@ -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
}
16 changes: 8 additions & 8 deletions go/cmd/compass/secret_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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",
},
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
}
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
Loading
Loading