From 43d1c8d50699dd3f0a64569adeaa76b9a4dd5e84 Mon Sep 17 00:00:00 2001 From: Isaac Miller Date: Mon, 8 Dec 2025 11:29:20 -0500 Subject: [PATCH 1/3] Feature verified working --- examples/blog-tools/.dockerignore | 7 +++++++ examples/blog-tools/Dockerfile | 4 +--- examples/code-review-agent/.dockerignore | 7 +++++++ examples/code-review-agent/Dockerfile | 4 +--- src/dspy_cli/commands/serve.py | 2 +- src/dspy_cli/server/app.py | 10 +++++----- src/dspy_cli/server/auth.py | 8 ++++---- src/dspy_cli/server/runner.py | 2 +- src/dspy_cli/server/ui.py | 5 +++-- src/dspy_cli/templates/.dockerignore.template | 3 +-- src/dspy_cli/templates/Dockerfile.template | 5 ++--- src/dspy_cli/templates/ui/static/script.js | 8 +++++++- src/dspy_cli/templates/ui/static/style.css | 20 +++++++++++++++++++ src/dspy_cli/templates/ui/templates.py | 9 +++++++-- 14 files changed, 67 insertions(+), 27 deletions(-) create mode 100644 examples/blog-tools/.dockerignore create mode 100644 examples/code-review-agent/.dockerignore diff --git a/examples/blog-tools/.dockerignore b/examples/blog-tools/.dockerignore new file mode 100644 index 0000000..9ca07ae --- /dev/null +++ b/examples/blog-tools/.dockerignore @@ -0,0 +1,7 @@ +.git +.env +__pycache__ +*.pyc +.vscode +.mypy_cache +logs/ \ No newline at end of file diff --git a/examples/blog-tools/Dockerfile b/examples/blog-tools/Dockerfile index 01a91e3..1b38e6b 100644 --- a/examples/blog-tools/Dockerfile +++ b/examples/blog-tools/Dockerfile @@ -9,8 +9,6 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv COPY . . RUN uv sync --no-dev -RUN uv tool install dspy-cli - EXPOSE 8000 -CMD ["uv", "run", "dspy-cli", "serve", "--host", "0.0.0.0", "--port", "8000", "--auth"] +CMD ["uv", "run", "dspy-cli", "serve", "--host", "0.0.0.0", "--port", "8000", "--auth", "--no-reload"] diff --git a/examples/code-review-agent/.dockerignore b/examples/code-review-agent/.dockerignore new file mode 100644 index 0000000..9ca07ae --- /dev/null +++ b/examples/code-review-agent/.dockerignore @@ -0,0 +1,7 @@ +.git +.env +__pycache__ +*.pyc +.vscode +.mypy_cache +logs/ \ No newline at end of file diff --git a/examples/code-review-agent/Dockerfile b/examples/code-review-agent/Dockerfile index 01a91e3..3c62a77 100644 --- a/examples/code-review-agent/Dockerfile +++ b/examples/code-review-agent/Dockerfile @@ -9,8 +9,6 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv COPY . . RUN uv sync --no-dev -RUN uv tool install dspy-cli - EXPOSE 8000 -CMD ["uv", "run", "dspy-cli", "serve", "--host", "0.0.0.0", "--port", "8000", "--auth"] +CMD ["uv", "run", "dspy-cli", "serve", "--host", "0.0.0.0", "--port", "8000", "--auth", "--no-reload"] \ No newline at end of file diff --git a/src/dspy_cli/commands/serve.py b/src/dspy_cli/commands/serve.py index 0f15439..0a5ff8f 100644 --- a/src/dspy_cli/commands/serve.py +++ b/src/dspy_cli/commands/serve.py @@ -101,7 +101,7 @@ def _exec_clean(target_python: Path, args: list[str]) -> NoReturn: @click.option( "--auth/--no-auth", default=False, - help="Enable API authentication via DSPY_API_TOKEN (default: disabled)", + help="Enable API authentication via DSPY_API_KEY (default: disabled)", ) def serve(port, host, logs_dir, reload, save_openapi, openapi_format, python, system, mcp, auth): """Start an HTTP API server that exposes your DSPy programs. diff --git a/src/dspy_cli/server/app.py b/src/dspy_cli/server/app.py index ce1f43d..81142ae 100644 --- a/src/dspy_cli/server/app.py +++ b/src/dspy_cli/server/app.py @@ -33,7 +33,7 @@ def create_app( package_name: Python package name for modules logs_dir: Directory for log files enable_ui: Whether to enable the web UI (always True, kept for compatibility) - enable_auth: Whether to enable API authentication via DSPY_API_TOKEN + enable_auth: Whether to enable API authentication via DSPY_API_KEY Returns: Configured FastAPI application @@ -187,7 +187,7 @@ async def program_metrics(program_name: str): logger.warning(f"Static directory not found: {static_dir}") # Create UI routes - create_ui_routes(app, modules, config, logs_dir) + create_ui_routes(app, modules, config, logs_dir, auth_enabled=enable_auth) logger.info("UI routes registered") # Setup authentication if enabled @@ -204,11 +204,11 @@ async def program_metrics(program_name: str): # Auto-generate a token and log it (Jupyter-style) token = generate_token() import os as os_module - os_module.environ["DSPY_API_TOKEN"] = token + os_module.environ["DSPY_API_KEY"] = token logger.warning("=" * 60) - logger.warning("DSPY_API_TOKEN not set. Generated temporary token:") + logger.warning("DSPY_API_KEY not set. Generated temporary token:") logger.warning(f" {token}") - logger.warning("Set DSPY_API_TOKEN as an environment secret for a persistent token.") + logger.warning("Set DSPY_API_KEY as an environment secret for a persistent token.") logger.warning("=" * 60) # Add auth routes (login/logout) diff --git a/src/dspy_cli/server/auth.py b/src/dspy_cli/server/auth.py index a541107..88f2b9f 100644 --- a/src/dspy_cli/server/auth.py +++ b/src/dspy_cli/server/auth.py @@ -1,7 +1,7 @@ """Authentication module for DSPy API server. Provides lightweight bearer token + session cookie authentication. -- API clients use: Authorization: Bearer +- API clients use: Authorization: Bearer - Browser UI uses: Login form -> signed HttpOnly session cookie """ @@ -23,7 +23,7 @@ SESSION_MAX_AGE = 7 * 24 * 3600 # 7 days # Environment variable names -ENV_API_TOKEN = "DSPY_API_TOKEN" +ENV_API_TOKEN = "DSPY_API_KEY" ENV_AUTH_ENABLED = "DSPY_CLI_AUTH_ENABLED" # Paths that don't require authentication @@ -244,11 +244,11 @@ async def login_form(request: Request, error: str | None = None): {error_html}
- +
-

The token is set via the DSPY_API_TOKEN environment variable.

+

The token is set via the DSPY_API_KEY environment variable.

diff --git a/src/dspy_cli/server/runner.py b/src/dspy_cli/server/runner.py index b45ecba..b1636c3 100644 --- a/src/dspy_cli/server/runner.py +++ b/src/dspy_cli/server/runner.py @@ -260,7 +260,7 @@ def notify_cli(msg: str, level: str = "info"): click.echo(f" • {Path.cwd() / 'dspy.config.yaml'}") click.echo() if auth: - token = os.environ.get("DSPY_API_TOKEN") + token = os.environ.get("DSPY_API_KEY") if token: click.echo(click.style("Authentication: ENABLED", fg="green")) click.echo(" API clients: Authorization: Bearer ") diff --git a/src/dspy_cli/server/ui.py b/src/dspy_cli/server/ui.py index e978266..fa83191 100644 --- a/src/dspy_cli/server/ui.py +++ b/src/dspy_cli/server/ui.py @@ -61,7 +61,7 @@ def get_recent_logs(logs_dir: Path, program_name: str, limit: int = 50) -> List[ return logs -def create_ui_routes(app, modules: List[Any], config: Dict, logs_dir: Path): +def create_ui_routes(app, modules: List[Any], config: Dict, logs_dir: Path, auth_enabled: bool = False): """Create UI routes for the FastAPI application. Args: @@ -69,6 +69,7 @@ def create_ui_routes(app, modules: List[Any], config: Dict, logs_dir: Path): modules: List of DiscoveredModule objects config: Configuration dictionary logs_dir: Directory containing log files + auth_enabled: Whether authentication is enabled """ from dspy_cli.templates.ui.templates import render_index, render_program @@ -91,7 +92,7 @@ async def program_page(program_name: str): if not module: raise HTTPException(status_code=404, detail=f"Program '{program_name}' not found") - html = render_program(module, config, program_name) + html = render_program(module, config, program_name, auth_enabled=auth_enabled) return HTMLResponse(content=html) @app.get("/api/logs/{program_name}") diff --git a/src/dspy_cli/templates/.dockerignore.template b/src/dspy_cli/templates/.dockerignore.template index dd23309..9ca07ae 100644 --- a/src/dspy_cli/templates/.dockerignore.template +++ b/src/dspy_cli/templates/.dockerignore.template @@ -1,6 +1,5 @@ .git -# TODO(Isaac): Design better way to manage secrets -# .env +.env __pycache__ *.pyc .vscode diff --git a/src/dspy_cli/templates/Dockerfile.template b/src/dspy_cli/templates/Dockerfile.template index 01a91e3..be079d0 100644 --- a/src/dspy_cli/templates/Dockerfile.template +++ b/src/dspy_cli/templates/Dockerfile.template @@ -9,8 +9,7 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv COPY . . RUN uv sync --no-dev -RUN uv tool install dspy-cli - EXPOSE 8000 -CMD ["uv", "run", "dspy-cli", "serve", "--host", "0.0.0.0", "--port", "8000", "--auth"] +CMD ["uv", "run", "dspy-cli", "serve", "--host", "0.0.0.0", "--port", "8000", "--auth", "--no-reload"] + diff --git a/src/dspy_cli/templates/ui/static/script.js b/src/dspy_cli/templates/ui/static/script.js index 291f85a..dc5d193 100644 --- a/src/dspy_cli/templates/ui/static/script.js +++ b/src/dspy_cli/templates/ui/static/script.js @@ -360,7 +360,13 @@ async function copyApiCall(programName) { // Generate curl command const url = `${window.location.protocol}//${window.location.host}/${programName}`; const jsonData = JSON.stringify(data, null, 2); - const curlCommand = `curl -X POST ${url} \\\n -H "Content-Type: application/json" \\\n -d '${jsonData}'`; + + // Include Authorization header placeholder if auth is enabled + let headers = '-H "Content-Type: application/json"'; + if (typeof authEnabled !== 'undefined' && authEnabled) { + headers += ' \\\n -H "Authorization: Bearer "'; + } + const curlCommand = `curl -X POST ${url} \\\n ${headers} \\\n -d '${jsonData}'`; // Copy to clipboard with fallback try { diff --git a/src/dspy_cli/templates/ui/static/style.css b/src/dspy_cli/templates/ui/static/style.css index ecaec7c..462da0a 100644 --- a/src/dspy_cli/templates/ui/static/style.css +++ b/src/dspy_cli/templates/ui/static/style.css @@ -472,6 +472,26 @@ section h3 { opacity: 0.85; } +.copy-btn-wrapper { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 6px; +} + +.auth-hint { + font-size: 0.75rem; + color: var(--text-tertiary); +} + +.auth-hint code { + background: var(--bg-code); + padding: 1px 4px; + border-radius: 3px; + font-family: monospace; + font-size: 0.7rem; +} + .refresh-btn { background: var(--bg-secondary); color: var(--text-primary); diff --git a/src/dspy_cli/templates/ui/templates.py b/src/dspy_cli/templates/ui/templates.py index 04be9b0..eab0ae8 100644 --- a/src/dspy_cli/templates/ui/templates.py +++ b/src/dspy_cli/templates/ui/templates.py @@ -155,13 +155,14 @@ def render_index(modules: List[Any], config: Dict) -> str: """ -def render_program(module: Any, config: Dict, program_name: str) -> str: +def render_program(module: Any, config: Dict, program_name: str, auth_enabled: bool = False) -> str: """Render the program detail page with form and logs. Args: module: DiscoveredModule object config: Configuration dictionary program_name: Name of the program + auth_enabled: Whether authentication is enabled Returns: HTML string for the program page @@ -340,7 +341,10 @@ def render_program(module: Any, config: Dict, program_name: str) -> str: {form_fields}
- + + + {'Replace <DSPY_API_KEY> with your API key' if auth_enabled else ''} +
@@ -398,6 +402,7 @@ def render_program(module: Any, config: Dict, program_name: str) -> str: // Initialize the program page const programName = "{program_name}"; + const authEnabled = {'true' if auth_enabled else 'false'}; initProgramPage(programName); From 213e8a0e884e7c539673eb0be257029ea3bf45ea Mon Sep 17 00:00:00 2001 From: Isaac Miller Date: Mon, 8 Dec 2025 11:38:15 -0500 Subject: [PATCH 2/3] update docs for auth --- docs/commands/serve.md | 14 +++++++++++++- docs/configuration.md | 1 + docs/deployment.md | 24 +++++++++++++++++++----- docs/getting-started.md | 2 +- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/docs/commands/serve.md b/docs/commands/serve.md index 6b369e9..d497bc1 100644 --- a/docs/commands/serve.md +++ b/docs/commands/serve.md @@ -38,6 +38,7 @@ Starts an HTTP server for local testing or production deployment. Discovers DSPy | `--python` | auto-detect | Path to Python interpreter | | `--system` | disabled | Use system Python instead of venv | | `--mcp` | disabled | Enable MCP server at `/mcp` | +| `--auth` | disabled | Require API authentication via `DSPY_API_KEY` | ## Auto-Discovery @@ -56,6 +57,8 @@ Every deployment provides: | `/` | POST | Execute module with JSON request body | | `/programs` | GET | List all discovered modules and their schemas | | `/openapi.json` | GET | OpenAPI specification | +| `/login` | GET/POST | Browser login (when `--auth` enabled) | +| `/health` | GET | Health check (always open, even with `--auth`) | With `--mcp` enabled: @@ -98,11 +101,20 @@ Response: ### Production Mode ```bash -dspy-cli serve --no-reload --host 0.0.0.0 --port 8000 +dspy-cli serve --no-reload --host 0.0.0.0 --port 8000 --auth ``` Disables hot reload and binds to all network interfaces for production deployment. +With `--auth` enabled: + +- Set `DSPY_API_KEY` in the environment. +- API clients must send `Authorization: Bearer `. +- The browser UI uses `/login` with the same key. +- `/health` remains open for health checks. + +If `DSPY_API_KEY` is not set, a temporary key is generated and logged at startup. + ## Error Responses **Validation error (422):** diff --git a/docs/configuration.md b/docs/configuration.md index 3b9b692..7518920 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -59,6 +59,7 @@ In `.env`: ```bash OPENAI_API_KEY=sk-... ANTHROPIC_API_KEY=sk-ant-... +DSPY_API_KEY=your-api-key # Optional: HTTP auth when running `dspy-cli serve --auth` ``` Reference in config: diff --git a/docs/deployment.md b/docs/deployment.md index 914887e..8bc4d1d 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -28,7 +28,7 @@ flyctl auth login # Launch and configure flyctl launch -flyctl secrets set OPENAI_API_KEY=sk-proj-... +flyctl secrets set OPENAI_API_KEY=sk-proj-... DSPY_API_KEY=your-api-key flyctl deploy ``` @@ -38,7 +38,9 @@ Verify: ```bash curl -X POST "https://your-app.fly.dev/SummarizerPredict" \ - -d '{"blog_post": "Test"}' -H "Content-Type: application/json" + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-api-key" \ + -d '{"blog_post": "Test"}' ``` ## Docker Deployment @@ -49,9 +51,14 @@ Compatible with any platform that can serve arbitrary dockerfiles. ```bash docker build -t my-app:latest . -docker run -p 8000:8000 -e OPENAI_API_KEY=sk-... my-app:latest +docker run -p 8000:8000 \ + -e OPENAI_API_KEY=sk-... \ + -e DSPY_API_KEY=your-api-key \ + my-app:latest ``` +> The generated Dockerfile runs `dspy-cli serve --auth` by default, so `DSPY_API_KEY` is required. + ### Push to Registry **Docker Hub:** @@ -89,7 +96,7 @@ gcloud run deploy my-app \ --source . \ --region us-central1 \ --allow-unauthenticated \ - --set-env-vars OPENAI_API_KEY=sk-proj-... + --set-env-vars OPENAI_API_KEY=sk-proj-...,DSPY_API_KEY=your-api-key ``` ## Environment Variables @@ -104,6 +111,7 @@ Use `.env` file (auto-loaded by `dspy-cli serve`, excluded via `.gitignore`): # .env - Do not commit OPENAI_API_KEY=sk-proj-... ANTHROPIC_API_KEY=sk-ant-... +DSPY_API_KEY=your-api-key # Optional: required when running with --auth ``` ### Production Secrets @@ -116,6 +124,10 @@ Configure via platform secret managers: | **Google Cloud Run** | `--set-env-vars KEY=value` or Secret Manager | | **AWS** | Systems Manager Parameter Store / Secrets Manager | +When you first create a project, if you do not have a `DSPY_API_KEY`, one will be generated and you can see it in the logs. + +It is recommended to change this key immediately after deployment to a secure value via your secret manager. + ### Secrets Best Practices **Required:** @@ -137,9 +149,11 @@ Configure via platform secret managers: Verify service availability: ```bash -curl https://my-app.fly.dev/openapi.json +curl https://my-app.fly.dev/health ``` +When authentication is enabled, use `/health` (always open) or include `Authorization: Bearer ` for other endpoints. + ## Logs **Fly.io:** diff --git a/docs/getting-started.md b/docs/getting-started.md index c3a7538..bfc4d95 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -86,7 +86,7 @@ Or use interactive UI at `http://localhost:8000`. ## 5. Deploy -See [Deployment Guide](deployment.md) for Fly.io, Docker, AWS, GCP, and more. +See [Deployment Guide](deployment.md) for Fly.io, Docker, AWS, GCP, and more. Production Docker deployments enable authentication by default; set `DSPY_API_KEY` in your environment and use `Authorization: Bearer ` when calling your API. ## Development From f9ff0099919e27bcf97d9f16f958846d0a62c664 Mon Sep 17 00:00:00 2001 From: Isaac Miller Date: Mon, 8 Dec 2025 13:11:07 -0500 Subject: [PATCH 3/3] add tooltip for auth --- examples/blog-tools/openapi.json | 129 --------------------- src/dspy_cli/templates/ui/static/style.css | 39 ++++++- src/dspy_cli/templates/ui/templates.py | 2 +- 3 files changed, 34 insertions(+), 136 deletions(-) diff --git a/examples/blog-tools/openapi.json b/examples/blog-tools/openapi.json index fd2b9f0..16004ea 100644 --- a/examples/blog-tools/openapi.json +++ b/examples/blog-tools/openapi.json @@ -598,139 +598,10 @@ } } } - }, - "/login": { - "get": { - "summary": "Login Form", - "description": "Render the login form.", - "operationId": "login_form_login_get", - "parameters": [ - { - "name": "error", - "in": "query", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Error" - } - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "text/html": { - "schema": { - "type": "string" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - }, - "post": { - "summary": "Login", - "description": "Process login form submission.", - "operationId": "login_login_post", - "requestBody": { - "required": true, - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_login_post" - } - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/logout": { - "post": { - "summary": "Logout", - "description": "Log out by clearing the session cookie.", - "operationId": "logout_logout_post", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } - } - } - } - }, - "/health": { - "get": { - "summary": "Health", - "description": "Health check endpoint (always open).", - "operationId": "health_health_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } - } - } - } } }, "components": { "schemas": { - "Body_login_login_post": { - "properties": { - "token_input": { - "type": "string", - "title": "Token Input" - } - }, - "type": "object", - "required": [ - "token_input" - ], - "title": "Body_login_login_post" - }, "HTTPValidationError": { "properties": { "detail": { diff --git a/src/dspy_cli/templates/ui/static/style.css b/src/dspy_cli/templates/ui/static/style.css index 462da0a..a5b63f6 100644 --- a/src/dspy_cli/templates/ui/static/style.css +++ b/src/dspy_cli/templates/ui/static/style.css @@ -473,25 +473,52 @@ section h3 { } .copy-btn-wrapper { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 6px; + position: relative; + display: inline-block; } .auth-hint { + visibility: hidden; + opacity: 0; + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + margin-bottom: 8px; + padding: 8px 12px; + background: var(--text-heading); + color: var(--bg-primary); font-size: 0.75rem; - color: var(--text-tertiary); + white-space: nowrap; + border-radius: 6px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + transition: opacity 0.2s, visibility 0.2s; + z-index: 100; +} + +.auth-hint::after { + content: ''; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + border: 6px solid transparent; + border-top-color: var(--text-heading); } .auth-hint code { - background: var(--bg-code); + background: rgba(255, 255, 255, 0.15); padding: 1px 4px; border-radius: 3px; font-family: monospace; font-size: 0.7rem; } +.copy-btn-wrapper:hover .auth-hint { + visibility: visible; + opacity: 1; +} + .refresh-btn { background: var(--bg-secondary); color: var(--text-primary); diff --git a/src/dspy_cli/templates/ui/templates.py b/src/dspy_cli/templates/ui/templates.py index eab0ae8..9015f85 100644 --- a/src/dspy_cli/templates/ui/templates.py +++ b/src/dspy_cli/templates/ui/templates.py @@ -343,7 +343,7 @@ def render_program(module: Any, config: Dict, program_name: str, auth_enabled: b - {'Replace <DSPY_API_KEY> with your API key' if auth_enabled else ''} + {f'Replace <DSPY_API_KEY> with your API key' if auth_enabled else ''}