From 4846d23f45708bd7c64b9f67a6ea8fa8a78265a3 Mon Sep 17 00:00:00 2001 From: relay Date: Wed, 29 Jul 2026 15:00:55 -0400 Subject: [PATCH 1/3] fix(cli): survive agent-relay credential masking in session and workspace reads Co-Authored-By: Claude Fable 5 --- cmd/relayfile-cli/main.go | 78 +++++++++++++++++--- cmd/relayfile-cli/main_test.go | 126 +++++++++++++++++++++++++++++---- 2 files changed, 182 insertions(+), 22 deletions(-) diff --git a/cmd/relayfile-cli/main.go b/cmd/relayfile-cli/main.go index e9f6f199..4b239d34 100644 --- a/cmd/relayfile-cli/main.go +++ b/cmd/relayfile-cli/main.go @@ -1249,8 +1249,16 @@ func runAgentRelayLogin(stdin io.Reader, stdout io.Writer, noOpen bool) error { func cloudCredentialsFromAgentRelay() (cloudCredentials, error) { var session agentRelayCloudSession - if err := runAgentRelayJSON([]string{"cloud", "session", "--json"}, &session); err != nil { - return cloudCredentials{}, err + // agent-relay masks accessToken in `cloud session --json` unless + // --reveal-token is passed; CLIs predating the flag reject it as an + // unknown option. Ask for the raw token first, fall back for older CLIs. + if err := runAgentRelayJSON([]string{"cloud", "session", "--json", "--reveal-token"}, &session); err != nil { + if !strings.Contains(err.Error(), "unknown option") { + return cloudCredentials{}, err + } + if err := runAgentRelayJSON([]string{"cloud", "session", "--json"}, &session); err != nil { + return cloudCredentials{}, err + } } apiURL := strings.TrimRight(strings.TrimSpace(session.APIURL), "/") accessToken := strings.TrimSpace(session.AccessToken) @@ -1266,6 +1274,9 @@ func cloudCredentialsFromAgentRelay() (cloudCredentials, error) { if accessToken == "" { return cloudCredentials{}, errors.New("agent-relay cloud session --json did not include an accessToken") } + if strings.Contains(accessToken, "…") { + return cloudCredentials{}, errors.New("agent-relay cloud session --json returned a masked accessToken; upgrade the agent-relay CLI or re-run `agent-relay cloud login`") + } return cloudCredentials{ APIURL: apiURL, AccessToken: accessToken, @@ -1275,8 +1286,16 @@ func cloudCredentialsFromAgentRelay() (cloudCredentials, error) { func activeWorkspaceFromAgentRelay() (agentRelayActiveWorkspace, error) { var workspace agentRelayActiveWorkspace - if err := runAgentRelayJSON([]string{"workspace", "active", "--json"}, &workspace); err != nil { - return agentRelayActiveWorkspace{}, classifyAgentRelayActiveWorkspaceError(err) + // agent-relay masks workspace keys in `workspace active --json` unless + // --reveal-secrets is passed; CLIs predating the flag reject it as an + // unknown option. Ask for raw keys first, fall back for older CLIs. + if err := runAgentRelayJSON([]string{"workspace", "active", "--json", "--reveal-secrets"}, &workspace); err != nil { + if !strings.Contains(err.Error(), "unknown option") { + return agentRelayActiveWorkspace{}, classifyAgentRelayActiveWorkspaceError(err) + } + if err := runAgentRelayJSON([]string{"workspace", "active", "--json"}, &workspace); err != nil { + return agentRelayActiveWorkspace{}, classifyAgentRelayActiveWorkspaceError(err) + } } if strings.TrimSpace(workspace.RelayfileWorkspaceID) == "" { return agentRelayActiveWorkspace{}, errors.New("agent-relay workspace active --json did not include relayfileWorkspaceId") @@ -1300,12 +1319,19 @@ func classifyAgentRelayActiveWorkspaceError(err error) error { !strings.Contains(normalized, "404") { return err } - match := agentRelayWorkspaceKeyInResolverError.FindStringSubmatch(detail) - if len(match) != 2 { - return err + workspaceKey := "" + if match := agentRelayWorkspaceKeyInResolverError.FindStringSubmatch(detail); len(match) == 2 { + if unescaped, unescapeErr := url.QueryUnescape(match[1]); unescapeErr == nil { + workspaceKey = unescaped + } + } + if !strings.HasPrefix(workspaceKey, "rk_live_") || strings.Contains(workspaceKey, "…") { + // agent-relay redacts credentials in its error output, so the key can + // no longer be read out of the message; fall back to the environment + // and the shared workspace store. + workspaceKey = activeWorkspaceKeyFromAgentRelayStore() } - workspaceKey, unescapeErr := url.QueryUnescape(match[1]) - if unescapeErr != nil || !strings.HasPrefix(workspaceKey, "rk_live_") { + if !strings.HasPrefix(workspaceKey, "rk_live_") { return err } @@ -1331,6 +1357,40 @@ func classifyAgentRelayActiveWorkspaceError(err error) error { } } +// activeWorkspaceKeyFromAgentRelayStore resolves the active workspace key the +// way agent-relay itself does: canonical env vars first, then the shared +// workspace store at $AGENT_RELAY_HOME (default ~/.agentworkforce/relay) +// /workspaces.json. Returns "" when no key can be resolved. +func activeWorkspaceKeyFromAgentRelayStore() string { + for _, name := range []string{"AGENT_RELAY_WORKSPACE_KEY", "RELAY_WORKSPACE_KEY", "RELAY_API_KEY"} { + if value := strings.TrimSpace(os.Getenv(name)); strings.HasPrefix(value, "rk_live_") { + return value + } + } + dir := strings.TrimSpace(os.Getenv("AGENT_RELAY_HOME")) + if dir == "" { + home, err := os.UserHomeDir() + if err != nil { + return "" + } + dir = filepath.Join(home, ".agentworkforce", "relay") + } + data, err := os.ReadFile(filepath.Join(dir, "workspaces.json")) + if err != nil { + return "" + } + var store struct { + Active string `json:"active"` + Workspaces map[string]struct { + Key string `json:"key"` + } `json:"workspaces"` + } + if json.Unmarshal(data, &store) != nil || store.Active == "" { + return "" + } + return strings.TrimSpace(store.Workspaces[store.Active].Key) +} + func validateRelaycastWorkspaceKey(ctx context.Context, workspaceKey string) (string, int, error) { baseURL := firstNonEmpty(os.Getenv("RELAYCAST_BASE_URL"), os.Getenv("RELAY_BASE_URL"), defaultRelaycastAPIURL) endpoint := strings.TrimRight(baseURL, "/") + "/v1/workspace" diff --git a/cmd/relayfile-cli/main_test.go b/cmd/relayfile-cli/main_test.go index 022fc6b9..8d8cc32f 100644 --- a/cmd/relayfile-cli/main_test.go +++ b/cmd/relayfile-cli/main_test.go @@ -2885,11 +2885,11 @@ fi func installFakeAgentRelaySession(t *testing.T, apiURL, accessToken, name, cloudWorkspaceID, relayfileWorkspaceID string) { t.Helper() body := fmt.Sprintf(` -if [ "$*" = "cloud session --json" ]; then +if [ "$*" = "cloud session --json --reveal-token" ] || [ "$*" = "cloud session --json" ]; then echo '{"apiUrl":%q,"accessToken":%q}' exit 0 fi -if [ "$*" = "workspace active --json" ]; then +if [ "$*" = "workspace active --json --reveal-secrets" ] || [ "$*" = "workspace active --json" ]; then echo '{"name":%q,"cloudWorkspaceId":%q,"relayfileWorkspaceId":%q}' exit 0 fi @@ -2899,6 +2899,49 @@ exit 2 installFakeAgentRelay(t, body) } +func TestCloudCredentialsFallBackWhenRevealTokenUnsupported(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + clearRelayfileEnv(t) + installFakeAgentRelay(t, ` +if [ "$*" = "cloud session --json --reveal-token" ]; then + echo "error: unknown option '--reveal-token'" >&2 + exit 1 +fi +if [ "$*" = "cloud session --json" ]; then + echo '{"apiUrl":"https://cloud.test","accessToken":"cld_at_raw_fallback_token"}' + exit 0 +fi +echo "unexpected args: $*" >&2 +exit 2 +`) + + creds, err := cloudCredentialsFromAgentRelay() + if err != nil { + t.Fatalf("cloudCredentialsFromAgentRelay failed: %v", err) + } + if creds.AccessToken != "cld_at_raw_fallback_token" { + t.Fatalf("unexpected access token: %q", creds.AccessToken) + } +} + +func TestCloudCredentialsRejectMaskedAccessToken(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + clearRelayfileEnv(t) + installFakeAgentRelay(t, ` +if [ "$*" = "cloud session --json --reveal-token" ] || [ "$*" = "cloud session --json" ]; then + echo '{"apiUrl":"https://cloud.test","accessToken":"cld_at_…oken"}' + exit 0 +fi +echo "unexpected args: $*" >&2 +exit 2 +`) + + _, err := cloudCredentialsFromAgentRelay() + if err == nil || !strings.Contains(err.Error(), "masked accessToken") { + t.Fatalf("expected masked accessToken error, got %v", err) + } +} + func relayfileCLITestFixture(t *testing.T, name string) string { t.Helper() content, err := os.ReadFile(filepath.Join("testdata", name)) @@ -2912,6 +2955,63 @@ func agentRelayResolver404Error(workspaceKey, body string) error { return errors.New("Workspace resolve failed at /api/v1/workspaces/active?key=" + workspaceKey + ": 404 " + body) } +func TestActiveWorkspaceClassifierFallsBackToStoreWhenErrorRedacted(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + clearRelayfileEnv(t) + for _, name := range []string{"AGENT_RELAY_WORKSPACE_KEY", "RELAY_WORKSPACE_KEY", "RELAY_API_KEY", "AGENT_RELAY_HOME"} { + t.Setenv(name, "") + } + + const workspaceKey = "rk_live_messaging_only_redacted" + var validationCalls int + relaycast := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + validationCalls++ + if got := r.Header.Get("Authorization"); got != "Bearer "+workspaceKey { + t.Fatalf("unexpected Relaycast Authorization: %q", got) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"ok":true,"data":{"id":"rc_123","name":"chat-only"}}`)) + })) + defer relaycast.Close() + t.Setenv("RELAYCAST_BASE_URL", relaycast.URL) + + storeDir := filepath.Join(os.Getenv("HOME"), ".agentworkforce", "relay") + if err := os.MkdirAll(storeDir, 0o700); err != nil { + t.Fatalf("mkdir workspace store dir failed: %v", err) + } + store := `{"active":"demo","workspaces":{"demo":{"key":"` + workspaceKey + `"}}}` + if err := os.WriteFile(filepath.Join(storeDir, "workspaces.json"), []byte(store), 0o600); err != nil { + t.Fatalf("write workspace store failed: %v", err) + } + + // A CLI that redacts credentials emits a masked key the resolver-error + // regex cannot capture; the classifier must fall back to the store. + resolverFailure := agentRelayResolver404Error("rk_live_…cted", relayfileCLITestFixture(t, "cloud-workspace-not-found.json")) + installFakeAgentRelay(t, fmt.Sprintf(` +if [ "$*" = "workspace active --json --reveal-secrets" ] || [ "$*" = "workspace active --json" ]; then + printf '%%s\n' %s >&2 + exit 1 +fi +echo "unexpected args: $*" >&2 +exit 2 +`, strconv.Quote(resolverFailure.Error()))) + + _, err := activeWorkspaceFromAgentRelay() + if err == nil { + t.Fatal("expected messaging-only workspace error") + } + var messagingOnly *agentRelayMessagingOnlyWorkspaceError + if !errors.As(err, &messagingOnly) { + t.Fatalf("expected agentRelayMessagingOnlyWorkspaceError, got %T: %v", err, err) + } + if messagingOnly.Name != "chat-only" { + t.Fatalf("messaging-only workspace name = %q, want chat-only", messagingOnly.Name) + } + if validationCalls != 1 { + t.Fatalf("expected exactly one Relaycast validation call, got %d", validationCalls) + } +} + func TestActiveWorkspaceFromAgentRelayReportsMessagingOnlyWorkspace(t *testing.T) { t.Setenv("HOME", t.TempDir()) clearRelayfileEnv(t) @@ -2936,7 +3036,7 @@ func TestActiveWorkspaceFromAgentRelayReportsMessagingOnlyWorkspace(t *testing.T t.Setenv("RELAYCAST_BASE_URL", relaycast.URL) resolverFailure := agentRelayResolver404Error(workspaceKey, relayfileCLITestFixture(t, "cloud-workspace-not-found.json")) installFakeAgentRelay(t, fmt.Sprintf(` -if [ "$*" = "workspace active --json" ]; then +if [ "$*" = "workspace active --json --reveal-secrets" ] || [ "$*" = "workspace active --json" ]; then printf '%%s\n' %s >&2 exit 1 fi @@ -3060,7 +3160,7 @@ func TestActiveWorkspaceFromAgentRelayDoesNotMisclassifyInvalidKey(t *testing.T) defer relaycast.Close() t.Setenv("RELAYCAST_BASE_URL", relaycast.URL) installFakeAgentRelay(t, ` -if [ "$*" = "workspace active --json" ]; then +if [ "$*" = "workspace active --json --reveal-secrets" ] || [ "$*" = "workspace active --json" ]; then echo 'Workspace resolve failed at /api/v1/workspaces/`+workspaceKey+`/resolve: 404 Workspace not found' >&2 exit 1 fi @@ -3137,11 +3237,11 @@ if [ "$*" = "cloud login --no-open" ]; then echo "agent-relay login ok" exit 0 fi -if [ "$*" = "cloud session --json" ]; then +if [ "$*" = "cloud session --json --reveal-token" ] || [ "$*" = "cloud session --json" ]; then echo '{"apiUrl":"`+cloud.URL+`","accessToken":"cld_access"}' exit 0 fi -if [ "$*" = "workspace active --json" ]; then +if [ "$*" = "workspace active --json --reveal-secrets" ] || [ "$*" = "workspace active --json" ]; then printf '%%s\n' %s >&2 exit 1 fi @@ -5200,11 +5300,11 @@ if [ "$*" = "cloud login --no-open" ]; then echo "agent-relay login ok" exit 0 fi -if [ "$*" = "cloud session --json" ]; then +if [ "$*" = "cloud session --json --reveal-token" ] || [ "$*" = "cloud session --json" ]; then echo '{"apiUrl":"`+server.URL+`","accessToken":"cld_new"}' exit 0 fi -if [ "$*" = "workspace active --json" ]; then +if [ "$*" = "workspace active --json --reveal-secrets" ] || [ "$*" = "workspace active --json" ]; then echo '{"name":"demo","cloudWorkspaceId":"ws_123","relayfileWorkspaceId":"ws_123"}' exit 0 fi @@ -5633,7 +5733,7 @@ func TestRefreshDelegatedCredentialsSurfacesRemintFailure(t *testing.T) { t.Setenv("HOME", t.TempDir()) clearRelayfileEnv(t) installFakeAgentRelay(t, ` -if [ "$*" = "cloud session --json" ]; then +if [ "$*" = "cloud session --json --reveal-token" ] || [ "$*" = "cloud session --json" ]; then echo "no active cloud session" >&2 exit 2 fi @@ -5794,7 +5894,7 @@ func TestEnsureCloudCredentialsUsesAgentRelaySession(t *testing.T) { t.Fatalf("saveCloudCredentials failed: %v", err) } installFakeAgentRelay(t, ` -if [ "$*" = "cloud session --json" ]; then +if [ "$*" = "cloud session --json --reveal-token" ] || [ "$*" = "cloud session --json" ]; then echo '{"apiUrl":"https://relay-cloud.test","accessToken":"agent_cloud_token"}' exit 0 fi @@ -5832,7 +5932,7 @@ if [ "$*" = "--version" ]; then echo "8.3.7" exit 0 fi -if [ "$*" = "cloud session --json" ]; then +if [ "$*" = "cloud session --json --reveal-token" ] || [ "$*" = "cloud session --json" ]; then touch %q echo '{"apiUrl":"https://relay-cloud.test","accessToken":"agent_cloud_token"}' exit 0 @@ -5930,7 +6030,7 @@ func TestWorkspaceCurrentPrefersAgentRelayActiveWorkspace(t *testing.T) { t.Fatalf("setDefaultWorkspace failed: %v", err) } installFakeAgentRelay(t, ` -if [ "$*" = "workspace active --json" ]; then +if [ "$*" = "workspace active --json --reveal-secrets" ] || [ "$*" = "workspace active --json" ]; then echo '{"name":"canonical","cloudWorkspaceId":"rw_cloud","relayfileWorkspaceId":"rw_relayfile","relaycastWorkspaceId":"rw_relaycast","relayauthWorkspaceId":"rw_relayauth"}' exit 0 fi @@ -5961,7 +6061,7 @@ func TestResolveWorkspaceUsesAgentRelayRelayfileWorkspaceID(t *testing.T) { t.Fatalf("setDefaultWorkspace failed: %v", err) } installFakeAgentRelay(t, ` -if [ "$*" = "workspace active --json" ]; then +if [ "$*" = "workspace active --json --reveal-secrets" ] || [ "$*" = "workspace active --json" ]; then echo '{"name":"canonical","cloudWorkspaceId":"rw_cloud","relayfileWorkspaceId":"rw_relayfile"}' exit 0 fi From ee9e7056d426e5537cd85c4436c5c6f7f60b9865 Mon Sep 17 00:00:00 2001 From: relayfile Date: Wed, 29 Jul 2026 21:48:35 -0400 Subject: [PATCH 2/3] fix(cli): reject masked workspace keys before fallback Agent Relay's redacted workspace-key display retains the rk_live_ prefix. That made a failure value observationally identical to a usable credential under the previous prefix-only check, so a masked environment value could block fallback to a valid workspaces.json key. Normalize external candidates to an empty sentinel unless they match the complete credential shape, and cover both masked-env/store fallback and the legacy reveal-secrets unknown-option retry. --- cmd/relayfile-cli/main.go | 23 ++++++++++++++++++----- cmd/relayfile-cli/main_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/cmd/relayfile-cli/main.go b/cmd/relayfile-cli/main.go index 4b239d34..a3e327a7 100644 --- a/cmd/relayfile-cli/main.go +++ b/cmd/relayfile-cli/main.go @@ -64,6 +64,7 @@ var relayfileVersion = relayfileDefaultVersion var relayIntegrationBindingsMu sync.Mutex var agentRelayWorkspaceKeyInResolverError = regexp.MustCompile(`(?:[?&]key=|/api/v1/workspaces/)(rk_live_[A-Za-z0-9_-]+)`) +var usableAgentRelayWorkspaceKeyPattern = regexp.MustCompile(`^rk_live_[A-Za-z0-9_-]+$`) // defaultJoinScopes are the scopes minted for every delegated-credential // workspace join. ops:read is required for writeback op-status polling @@ -1322,16 +1323,16 @@ func classifyAgentRelayActiveWorkspaceError(err error) error { workspaceKey := "" if match := agentRelayWorkspaceKeyInResolverError.FindStringSubmatch(detail); len(match) == 2 { if unescaped, unescapeErr := url.QueryUnescape(match[1]); unescapeErr == nil { - workspaceKey = unescaped + workspaceKey = usableAgentRelayWorkspaceKey(unescaped) } } - if !strings.HasPrefix(workspaceKey, "rk_live_") || strings.Contains(workspaceKey, "…") { + if workspaceKey == "" { // agent-relay redacts credentials in its error output, so the key can // no longer be read out of the message; fall back to the environment // and the shared workspace store. workspaceKey = activeWorkspaceKeyFromAgentRelayStore() } - if !strings.HasPrefix(workspaceKey, "rk_live_") { + if workspaceKey == "" { return err } @@ -1357,13 +1358,25 @@ func classifyAgentRelayActiveWorkspaceError(err error) error { } } +// usableAgentRelayWorkspaceKey converts external key-shaped input into either a +// validated workspace key or the empty sentinel. Agent Relay's masked display +// values deliberately retain the rk_live_ prefix, so prefix checks alone cannot +// distinguish them from usable credentials. +func usableAgentRelayWorkspaceKey(value string) string { + value = strings.TrimSpace(value) + if !usableAgentRelayWorkspaceKeyPattern.MatchString(value) { + return "" + } + return value +} + // activeWorkspaceKeyFromAgentRelayStore resolves the active workspace key the // way agent-relay itself does: canonical env vars first, then the shared // workspace store at $AGENT_RELAY_HOME (default ~/.agentworkforce/relay) // /workspaces.json. Returns "" when no key can be resolved. func activeWorkspaceKeyFromAgentRelayStore() string { for _, name := range []string{"AGENT_RELAY_WORKSPACE_KEY", "RELAY_WORKSPACE_KEY", "RELAY_API_KEY"} { - if value := strings.TrimSpace(os.Getenv(name)); strings.HasPrefix(value, "rk_live_") { + if value := usableAgentRelayWorkspaceKey(os.Getenv(name)); value != "" { return value } } @@ -1388,7 +1401,7 @@ func activeWorkspaceKeyFromAgentRelayStore() string { if json.Unmarshal(data, &store) != nil || store.Active == "" { return "" } - return strings.TrimSpace(store.Workspaces[store.Active].Key) + return usableAgentRelayWorkspaceKey(store.Workspaces[store.Active].Key) } func validateRelaycastWorkspaceKey(ctx context.Context, workspaceKey string) (string, int, error) { diff --git a/cmd/relayfile-cli/main_test.go b/cmd/relayfile-cli/main_test.go index 8d8cc32f..2c59521d 100644 --- a/cmd/relayfile-cli/main_test.go +++ b/cmd/relayfile-cli/main_test.go @@ -2849,6 +2849,10 @@ func clearRelayfileEnv(t *testing.T) { t.Setenv("RELAYCAST_BASE_URL", "") t.Setenv("RELAY_BASE_URL", "") t.Setenv("AGENT_RELAY_BIN", "") + t.Setenv("AGENT_RELAY_WORKSPACE_KEY", "") + t.Setenv("RELAY_WORKSPACE_KEY", "") + t.Setenv("RELAY_API_KEY", "") + t.Setenv("AGENT_RELAY_HOME", "") } func installFakeAgentRelay(t *testing.T, scriptBody string) string { @@ -2942,6 +2946,31 @@ exit 2 } } +func TestActiveWorkspaceFallsBackWhenRevealSecretsUnsupported(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + clearRelayfileEnv(t) + installFakeAgentRelay(t, ` +if [ "$*" = "workspace active --json --reveal-secrets" ]; then + echo "error: unknown option '--reveal-secrets'" >&2 + exit 1 +fi +if [ "$*" = "workspace active --json" ]; then + echo '{"name":"legacy","cloudWorkspaceId":"rw_cloud","relayfileWorkspaceId":"rw_relayfile"}' + exit 0 +fi +echo "unexpected args: $*" >&2 +exit 2 +`) + + workspace, err := activeWorkspaceFromAgentRelay() + if err != nil { + t.Fatalf("activeWorkspaceFromAgentRelay failed: %v", err) + } + if workspace.Name != "legacy" || workspace.CloudWorkspaceID != "rw_cloud" || workspace.RelayfileWorkspaceID != "rw_relayfile" { + t.Fatalf("unexpected workspace fallback result: %+v", workspace) + } +} + func relayfileCLITestFixture(t *testing.T, name string) string { t.Helper() content, err := os.ReadFile(filepath.Join("testdata", name)) @@ -2961,6 +2990,9 @@ func TestActiveWorkspaceClassifierFallsBackToStoreWhenErrorRedacted(t *testing.T for _, name := range []string{"AGENT_RELAY_WORKSPACE_KEY", "RELAY_WORKSPACE_KEY", "RELAY_API_KEY", "AGENT_RELAY_HOME"} { t.Setenv(name, "") } + // A masked environment value must normalize to the empty sentinel so it + // cannot block fallback to the usable key in the shared workspace store. + t.Setenv("AGENT_RELAY_WORKSPACE_KEY", "rk_live_…masked") const workspaceKey = "rk_live_messaging_only_redacted" var validationCalls int @@ -3127,6 +3159,7 @@ func TestClassifyAgentRelayActiveWorkspaceErrorReportsUnexpectedRelaycastStatus( } func TestClassifyAgentRelayActiveWorkspaceErrorPreservesRegexMiss(t *testing.T) { + t.Setenv("HOME", t.TempDir()) clearRelayfileEnv(t) var validationCalls int From 08c9c3a7d675d575ac70636bd5c953baab9a8805 Mon Sep 17 00:00:00 2001 From: relayfile Date: Wed, 29 Jul 2026 21:56:53 -0400 Subject: [PATCH 3/3] test(cli): codify the workspace-key trust boundary Agent Relay intentionally keeps masked workspace keys credential-shaped so humans and tools can recognize a redacted credential; that display contract belongs to Relay and must not be tightened from relayfile. Prove instead that relayfile normalizes masked input to its empty sentinel before store fallback. Also isolate the regex-miss test from the operator's Agent Relay environment and workspaces.json: that path was read-only, but previously could read a live host key and forward it to the loopback test server. --- cmd/relayfile-cli/main.go | 7 ++++--- cmd/relayfile-cli/main_test.go | 16 +++++++++++----- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/cmd/relayfile-cli/main.go b/cmd/relayfile-cli/main.go index a3e327a7..a46952c9 100644 --- a/cmd/relayfile-cli/main.go +++ b/cmd/relayfile-cli/main.go @@ -1359,9 +1359,10 @@ func classifyAgentRelayActiveWorkspaceError(err error) error { } // usableAgentRelayWorkspaceKey converts external key-shaped input into either a -// validated workspace key or the empty sentinel. Agent Relay's masked display -// values deliberately retain the rk_live_ prefix, so prefix checks alone cannot -// distinguish them from usable credentials. +// validated workspace key or the empty sentinel. Agent Relay intentionally +// keeps masked values recognizable by retaining the rk_live_ prefix, so +// relayfile must enforce usability at its own trust boundary instead of trying +// to change or outguess Relay's display contract. func usableAgentRelayWorkspaceKey(value string) string { value = strings.TrimSpace(value) if !usableAgentRelayWorkspaceKeyPattern.MatchString(value) { diff --git a/cmd/relayfile-cli/main_test.go b/cmd/relayfile-cli/main_test.go index 2c59521d..06eb7b03 100644 --- a/cmd/relayfile-cli/main_test.go +++ b/cmd/relayfile-cli/main_test.go @@ -2849,10 +2849,6 @@ func clearRelayfileEnv(t *testing.T) { t.Setenv("RELAYCAST_BASE_URL", "") t.Setenv("RELAY_BASE_URL", "") t.Setenv("AGENT_RELAY_BIN", "") - t.Setenv("AGENT_RELAY_WORKSPACE_KEY", "") - t.Setenv("RELAY_WORKSPACE_KEY", "") - t.Setenv("RELAY_API_KEY", "") - t.Setenv("AGENT_RELAY_HOME", "") } func installFakeAgentRelay(t *testing.T, scriptBody string) string { @@ -2992,7 +2988,11 @@ func TestActiveWorkspaceClassifierFallsBackToStoreWhenErrorRedacted(t *testing.T } // A masked environment value must normalize to the empty sentinel so it // cannot block fallback to the usable key in the shared workspace store. - t.Setenv("AGENT_RELAY_WORKSPACE_KEY", "rk_live_…masked") + const maskedWorkspaceKey = "rk_live_…masked" + if got := usableAgentRelayWorkspaceKey(maskedWorkspaceKey); got != "" { + t.Fatalf("masked workspace key normalized to %q, want empty sentinel", got) + } + t.Setenv("AGENT_RELAY_WORKSPACE_KEY", maskedWorkspaceKey) const workspaceKey = "rk_live_messaging_only_redacted" var validationCalls int @@ -3159,8 +3159,14 @@ func TestClassifyAgentRelayActiveWorkspaceErrorReportsUnexpectedRelaycastStatus( } func TestClassifyAgentRelayActiveWorkspaceErrorPreservesRegexMiss(t *testing.T) { + // A regex miss falls through to Agent Relay's shared workspace store. + // Isolate every store input so this test cannot read an operator's live key. t.Setenv("HOME", t.TempDir()) clearRelayfileEnv(t) + for _, name := range []string{"AGENT_RELAY_WORKSPACE_KEY", "RELAY_WORKSPACE_KEY", "RELAY_API_KEY"} { + t.Setenv(name, "") + } + t.Setenv("AGENT_RELAY_HOME", t.TempDir()) var validationCalls int relaycast := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {