From 17146a8917ae0999dfe38d1b6582eb74ae7542db Mon Sep 17 00:00:00 2001 From: egerev Date: Mon, 13 Apr 2026 21:09:21 +0800 Subject: [PATCH 1/2] fix: reduce health and cli secret leakage --- packages/api/alayaos_api/routers/health.py | 6 ++- packages/api/tests/test_routers_health.py | 46 ++++++++++++++++++++++ packages/cli-go/internal/cmd/setup.go | 37 ++++++++++------- packages/cli-go/internal/cmd/setup_test.go | 23 +++++++++++ packages/core/alayaos_core/config.py | 1 + 5 files changed, 98 insertions(+), 15 deletions(-) diff --git a/packages/api/alayaos_api/routers/health.py b/packages/api/alayaos_api/routers/health.py index 45e1bb8..e08c3b3 100644 --- a/packages/api/alayaos_api/routers/health.py +++ b/packages/api/alayaos_api/routers/health.py @@ -7,6 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from alayaos_api.deps import get_session +from alayaos_core.config import Settings router = APIRouter(tags=["health"]) @@ -18,6 +19,7 @@ async def health_live(): @router.get("/health/ready") async def health_ready(session: Annotated[AsyncSession, Depends(get_session)]): + settings = Settings() checks = {} # Database @@ -57,4 +59,6 @@ async def health_ready(session: Annotated[AsyncSession, Depends(get_session)]): ok_checks = [v for v in checks.values() if v != "unavailable"] overall = "ok" if ok_checks and all(v == "ok" for v in ok_checks) else "degraded" - return {"status": overall, "checks": checks, "first_run": first_run} + if settings.HEALTH_READY_VERBOSE: + return {"status": overall, "checks": checks, "first_run": first_run} + return {"status": overall} diff --git a/packages/api/tests/test_routers_health.py b/packages/api/tests/test_routers_health.py index 6a114f8..6f9ea29 100644 --- a/packages/api/tests/test_routers_health.py +++ b/packages/api/tests/test_routers_health.py @@ -8,6 +8,13 @@ from alayaos_api.routers.health import router +def make_scalar_result(value): + result = MagicMock() + result.scalar_one.return_value = value + result.scalar_one_or_none.return_value = value + return result + + def make_test_app(): app = FastAPI() @@ -43,3 +50,42 @@ def test_health_live_no_auth_required() -> None: client = TestClient(app) response = client.get("/health/live") assert response.status_code == 200 + + +def test_health_ready_redacts_details_by_default(monkeypatch) -> None: + monkeypatch.delenv("ALAYA_HEALTH_READY_VERBOSE", raising=False) + app, session_mock = make_test_app() + session_mock.execute.side_effect = [ + MagicMock(), + make_scalar_result("0004"), + make_scalar_result(1), + make_scalar_result(0), + ] + + client = TestClient(app) + response = client.get("/health/ready") + + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + +def test_health_ready_includes_checks_when_verbose(monkeypatch) -> None: + monkeypatch.setenv("ALAYA_HEALTH_READY_VERBOSE", "true") + app, session_mock = make_test_app() + session_mock.execute.side_effect = [ + MagicMock(), + make_scalar_result("0004"), + make_scalar_result(1), + make_scalar_result(0), + ] + + client = TestClient(app) + response = client.get("/health/ready") + + assert response.status_code == 200 + body = response.json() + assert body["status"] == "ok" + assert body["checks"]["database"] == "ok" + assert body["checks"]["migrations"] == "ok" + assert body["checks"]["seeds"] == "ok" + assert body["first_run"] is True diff --git a/packages/cli-go/internal/cmd/setup.go b/packages/cli-go/internal/cmd/setup.go index be8d200..3cdb66f 100644 --- a/packages/cli-go/internal/cmd/setup.go +++ b/packages/cli-go/internal/cmd/setup.go @@ -13,6 +13,9 @@ import ( ) var setupProfile string +var setupShowSecret bool + +const storedKeyPlaceholder = "" var setupCmd = &cobra.Command{ Use: "setup", @@ -49,20 +52,7 @@ var setupAgentCmd = &cobra.Command{ fmt.Println("API key created and stored.") apiKey = newKey } - switch strings.ToLower(setupProfile) { - case "claude-code": - fmt.Printf("# Add to .claude/settings.json:\n") - fmt.Printf(`{"mcpServers":{"alaya":{"command":"alaya","args":["mcp"],"env":{"ALAYA_SERVER_URL":"%s","ALAYA_API_KEY":"%s"}}}}`, baseURL, apiKey) - fmt.Println() - case "codex": - fmt.Printf("export ALAYA_SERVER_URL=%s\nexport ALAYA_API_KEY=%s\n", baseURL, apiKey) - case "cursor": - fmt.Printf("# Add to .cursor/mcp.json:\n") - fmt.Printf(`{"mcpServers":{"alaya":{"command":"alaya","args":["mcp"],"env":{"ALAYA_SERVER_URL":"%s","ALAYA_API_KEY":"%s"}}}}`, baseURL, apiKey) - fmt.Println() - default: - fmt.Printf("ALAYA_SERVER_URL=%s\nALAYA_API_KEY=%s\nALAYA_API_BASE=%s/api/v1\n", baseURL, apiKey, baseURL) - } + fmt.Print(renderAgentSetup(setupProfile, baseURL, apiKey, setupShowSecret)) return nil }, } @@ -71,6 +61,25 @@ func init() { rootCmd.AddCommand(setupCmd) setupCmd.AddCommand(setupAgentCmd) setupAgentCmd.Flags().StringVar(&setupProfile, "profile", "generic", "Agent profile (claude-code|codex|cursor|generic)") + setupAgentCmd.Flags().BoolVar(&setupShowSecret, "show-secret", false, "Print the API key in generated output") +} + +func renderAgentSetup(profile, baseURL, apiKey string, showSecret bool) string { + renderedKey := storedKeyPlaceholder + if showSecret { + renderedKey = apiKey + } + + switch strings.ToLower(profile) { + case "claude-code": + return fmt.Sprintf("# Add to .claude/settings.json:\n{\"mcpServers\":{\"alaya\":{\"command\":\"alaya\",\"args\":[\"mcp\"],\"env\":{\"ALAYA_SERVER_URL\":\"%s\",\"ALAYA_API_KEY\":\"%s\"}}}}\n", baseURL, renderedKey) + case "codex": + return fmt.Sprintf("export ALAYA_SERVER_URL=%s\nexport ALAYA_API_KEY=%s\n", baseURL, renderedKey) + case "cursor": + return fmt.Sprintf("# Add to .cursor/mcp.json:\n{\"mcpServers\":{\"alaya\":{\"command\":\"alaya\",\"args\":[\"mcp\"],\"env\":{\"ALAYA_SERVER_URL\":\"%s\",\"ALAYA_API_KEY\":\"%s\"}}}}\n", baseURL, renderedKey) + default: + return fmt.Sprintf("ALAYA_SERVER_URL=%s\nALAYA_API_KEY=%s\nALAYA_API_BASE=%s/api/v1\n", baseURL, renderedKey, baseURL) + } } // createAPIKeyViaBootstrap calls POST /api-keys using the bootstrap key and returns the raw key. diff --git a/packages/cli-go/internal/cmd/setup_test.go b/packages/cli-go/internal/cmd/setup_test.go index 0c50f2b..4d024c2 100644 --- a/packages/cli-go/internal/cmd/setup_test.go +++ b/packages/cli-go/internal/cmd/setup_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "strings" "testing" ) @@ -61,3 +62,25 @@ func TestCreateAPIKeyViaBootstrap_MissingRawKey(t *testing.T) { t.Fatal("expected error when raw_key is missing from response") } } + +func TestRenderAgentSetup_RedactsSecretByDefault(t *testing.T) { + output := renderAgentSetup("generic", "https://alaya.example", "ak_super_secret", false) + + if !strings.Contains(output, "ALAYA_SERVER_URL=https://alaya.example") { + t.Fatalf("expected server URL in output, got %q", output) + } + if !strings.Contains(output, "") { + t.Fatalf("expected redaction placeholder in output, got %q", output) + } + if strings.Contains(output, "ak_super_secret") { + t.Fatalf("expected secret to be redacted, got %q", output) + } +} + +func TestRenderAgentSetup_IncludesSecretWhenOptedIn(t *testing.T) { + output := renderAgentSetup("codex", "https://alaya.example", "ak_super_secret", true) + + if !strings.Contains(output, "ALAYA_API_KEY=ak_super_secret") { + t.Fatalf("expected secret in output, got %q", output) + } +} diff --git a/packages/core/alayaos_core/config.py b/packages/core/alayaos_core/config.py index 6ad8007..a47cbaa 100644 --- a/packages/core/alayaos_core/config.py +++ b/packages/core/alayaos_core/config.py @@ -16,6 +16,7 @@ class Settings(BaseSettings): DB_POOL_TIMEOUT: int = 30 DB_ECHO: bool = False LOG_LEVEL: str = "INFO" + HEALTH_READY_VERBOSE: bool = False # LLM Provider EXTRACTION_LLM_PROVIDER: str = "anthropic" # anthropic|openai|ollama|vllm From 0a479d3c0387eeacf716c9349ebc92f358773d27 Mon Sep 17 00:00:00 2001 From: egerev Date: Mon, 13 Apr 2026 22:08:19 +0800 Subject: [PATCH 2/2] fix: harden health and setup secret handling --- packages/api/alayaos_api/routers/health.py | 8 ++++- packages/api/tests/test_routers_health.py | 32 ++++++++++++++++++ packages/cli-go/internal/cmd/setup.go | 22 +++++++++++-- packages/cli-go/internal/cmd/setup_test.go | 38 ++++++++++++++++++++-- 4 files changed, 94 insertions(+), 6 deletions(-) diff --git a/packages/api/alayaos_api/routers/health.py b/packages/api/alayaos_api/routers/health.py index e08c3b3..dfcbb45 100644 --- a/packages/api/alayaos_api/routers/health.py +++ b/packages/api/alayaos_api/routers/health.py @@ -1,5 +1,6 @@ """Health check endpoints.""" +from functools import lru_cache from typing import Annotated from fastapi import APIRouter, Depends @@ -12,6 +13,11 @@ router = APIRouter(tags=["health"]) +@lru_cache +def get_settings() -> Settings: + return Settings() + + @router.get("/health/live") async def health_live(): return {"status": "ok"} @@ -19,7 +25,7 @@ async def health_live(): @router.get("/health/ready") async def health_ready(session: Annotated[AsyncSession, Depends(get_session)]): - settings = Settings() + settings = get_settings() checks = {} # Database diff --git a/packages/api/tests/test_routers_health.py b/packages/api/tests/test_routers_health.py index 6f9ea29..e7a157d 100644 --- a/packages/api/tests/test_routers_health.py +++ b/packages/api/tests/test_routers_health.py @@ -1,10 +1,12 @@ """Tests for health endpoints.""" +from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock from fastapi import FastAPI from fastapi.testclient import TestClient +from alayaos_api.routers import health from alayaos_api.routers.health import router @@ -54,6 +56,7 @@ def test_health_live_no_auth_required() -> None: def test_health_ready_redacts_details_by_default(monkeypatch) -> None: monkeypatch.delenv("ALAYA_HEALTH_READY_VERBOSE", raising=False) + health.get_settings.cache_clear() app, session_mock = make_test_app() session_mock.execute.side_effect = [ MagicMock(), @@ -71,6 +74,7 @@ def test_health_ready_redacts_details_by_default(monkeypatch) -> None: def test_health_ready_includes_checks_when_verbose(monkeypatch) -> None: monkeypatch.setenv("ALAYA_HEALTH_READY_VERBOSE", "true") + health.get_settings.cache_clear() app, session_mock = make_test_app() session_mock.execute.side_effect = [ MagicMock(), @@ -89,3 +93,31 @@ def test_health_ready_includes_checks_when_verbose(monkeypatch) -> None: assert body["checks"]["migrations"] == "ok" assert body["checks"]["seeds"] == "ok" assert body["first_run"] is True + + +def test_health_ready_uses_cached_settings(monkeypatch) -> None: + settings_factory = MagicMock(return_value=SimpleNamespace(HEALTH_READY_VERBOSE=False)) + monkeypatch.setattr(health, "Settings", settings_factory) + health.get_settings.cache_clear() + + app, session_mock = make_test_app() + session_mock.execute.side_effect = [ + MagicMock(), + make_scalar_result("0004"), + make_scalar_result(1), + make_scalar_result(0), + MagicMock(), + make_scalar_result("0004"), + make_scalar_result(1), + make_scalar_result(0), + ] + + client = TestClient(app) + first = client.get("/health/ready") + second = client.get("/health/ready") + + assert first.status_code == 200 + assert second.status_code == 200 + assert settings_factory.call_count == 1 + + health.get_settings.cache_clear() diff --git a/packages/cli-go/internal/cmd/setup.go b/packages/cli-go/internal/cmd/setup.go index 3cdb66f..5a76244 100644 --- a/packages/cli-go/internal/cmd/setup.go +++ b/packages/cli-go/internal/cmd/setup.go @@ -52,6 +52,9 @@ var setupAgentCmd = &cobra.Command{ fmt.Println("API key created and stored.") apiKey = newKey } + if err := warnShowSecret(cmd, setupShowSecret); err != nil { + return err + } fmt.Print(renderAgentSetup(setupProfile, baseURL, apiKey, setupShowSecret)) return nil }, @@ -82,6 +85,14 @@ func renderAgentSetup(profile, baseURL, apiKey string, showSecret bool) string { } } +func warnShowSecret(cmd *cobra.Command, showSecret bool) error { + if !showSecret { + return nil + } + _, err := fmt.Fprintln(cmd.ErrOrStderr(), "Warning: --show-secret prints the API key to stdout. Use only in a trusted terminal.") + return err +} + // createAPIKeyViaBootstrap calls POST /api-keys using the bootstrap key and returns the raw key. func createAPIKeyViaBootstrap(baseURL, bootstrapKey string) (string, error) { c := client.New(baseURL, bootstrapKey) @@ -93,13 +104,20 @@ func createAPIKeyViaBootstrap(baseURL, bootstrapKey string) (string, error) { return "", err } var resp struct { + Data struct { + RawKey string `json:"raw_key"` + } `json:"data"` RawKey string `json:"raw_key"` } if err := json.Unmarshal(data, &resp); err != nil { return "", fmt.Errorf("parse key response: %w", err) } - if resp.RawKey == "" { + rawKey := resp.Data.RawKey + if rawKey == "" { + rawKey = resp.RawKey + } + if rawKey == "" { return "", fmt.Errorf("server did not return raw_key in response") } - return resp.RawKey, nil + return rawKey, nil } diff --git a/packages/cli-go/internal/cmd/setup_test.go b/packages/cli-go/internal/cmd/setup_test.go index 4d024c2..4c3febc 100644 --- a/packages/cli-go/internal/cmd/setup_test.go +++ b/packages/cli-go/internal/cmd/setup_test.go @@ -1,11 +1,14 @@ package cmd import ( + "bytes" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" + + "github.com/spf13/cobra" ) func TestCreateAPIKeyViaBootstrap_Success(t *testing.T) { @@ -19,7 +22,9 @@ func TestCreateAPIKeyViaBootstrap_Success(t *testing.T) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ - "raw_key": "ak_newly_created_key", + "data": map[string]interface{}{ + "raw_key": "ak_newly_created_key", + }, }) })) defer ts.Close() @@ -50,9 +55,10 @@ func TestCreateAPIKeyViaBootstrap_MissingRawKey(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - // Response missing raw_key field json.NewEncoder(w).Encode(map[string]interface{}{ - "id": "key-uuid", + "data": map[string]interface{}{ + "id": "key-uuid", + }, }) })) defer ts.Close() @@ -84,3 +90,29 @@ func TestRenderAgentSetup_IncludesSecretWhenOptedIn(t *testing.T) { t.Fatalf("expected secret in output, got %q", output) } } + +func TestWarnShowSecret_PrintsToStderr(t *testing.T) { + cmd := &cobra.Command{} + var stderr bytes.Buffer + cmd.SetErr(&stderr) + + if err := warnShowSecret(cmd, true); err != nil { + t.Fatalf("warnShowSecret() error: %v", err) + } + if !strings.Contains(stderr.String(), "--show-secret prints the API key to stdout") { + t.Fatalf("expected warning on stderr, got %q", stderr.String()) + } +} + +func TestWarnShowSecret_SkipsWhenDisabled(t *testing.T) { + cmd := &cobra.Command{} + var stderr bytes.Buffer + cmd.SetErr(&stderr) + + if err := warnShowSecret(cmd, false); err != nil { + t.Fatalf("warnShowSecret() error: %v", err) + } + if stderr.Len() != 0 { + t.Fatalf("expected no stderr output, got %q", stderr.String()) + } +}