Deprecate Streamlit credential path (#5836)

This commit is contained in:
Suchintan 2026-05-06 23:57:48 -04:00 committed by GitHub
parent dddbe89fb7
commit dc8c86867c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 258 additions and 91 deletions

View file

@ -11,7 +11,8 @@
**/.vscode **/.vscode
*.env* *.env*
!/.env !/.env
# Streamlit ignores # Generated local deployment credentials
**/.skyvern
**/secrets*.toml **/secrets*.toml
# Skyvern # Skyvern
@ -25,4 +26,4 @@ skyvern-frontend/node_modules
.dockerignore .dockerignore
.gitignore .gitignore
Dockerfile Dockerfile

3
.gitignore vendored
View file

@ -172,7 +172,8 @@ postgres-data
files/ files/
temp/ temp/
# Streamlit ignores # Generated local deployment credentials
.skyvern/
**/secrets*.toml **/secrets*.toml
## Frontend ## Frontend

View file

@ -1,5 +0,0 @@
[theme]
# The preset Streamlit theme that your custom theme inherits from.
# One of "light" or "dark".
base = "dark"

View file

@ -34,7 +34,8 @@ services:
- ./videos:/data/videos - ./videos:/data/videos
- ./har:/data/har - ./har:/data/har
- ./log:/data/log - ./log:/data/log
- ./.streamlit:/app/.streamlit # Generated credentials allow the UI to pick up the local API key on first startup.
- ./.skyvern:/app/.skyvern
# Uncomment the following two lines if you want to connect to any local changes # Uncomment the following two lines if you want to connect to any local changes
# - ./skyvern:/app/skyvern # - ./skyvern:/app/skyvern
# - ./alembic:/app/alembic # - ./alembic:/app/alembic
@ -130,10 +131,11 @@ services:
postgres: postgres:
condition: service_healthy condition: service_healthy
healthcheck: healthcheck:
test: ["CMD", "test", "-f", "/app/.streamlit/secrets.toml"] test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/v1/heartbeat', timeout=5)"]
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 5 retries: 24
start_period: 180s
skyvern-ui: skyvern-ui:
image: public.ecr.aws/skyvern/skyvern-ui:latest image: public.ecr.aws/skyvern/skyvern-ui:latest
restart: on-failure restart: on-failure
@ -144,7 +146,8 @@ services:
- ./artifacts:/data/artifacts - ./artifacts:/data/artifacts
- ./videos:/data/videos - ./videos:/data/videos
- ./har:/data/har - ./har:/data/har
- ./.streamlit:/app/.streamlit # Generated credentials allow the UI to pick up the local API key on first startup.
- ./.skyvern:/app/.skyvern
# User secrets and frontend config live in skyvern-frontend/.env # User secrets and frontend config live in skyvern-frontend/.env
# (loaded via env_file). No inline environment for the UI service. # (loaded via env_file). No inline environment for the UI service.
env_file: env_file:

View file

@ -56,7 +56,7 @@ Copy the frontend environment file:
cp skyvern-frontend/.env.example skyvern-frontend/.env cp skyvern-frontend/.env.example skyvern-frontend/.env
``` ```
The default values work for local development. You'll update `VITE_SKYVERN_API_KEY` after the first startup. The default values work for local development. Docker Compose mounts the generated local credentials into the UI container, so you can leave `VITE_SKYVERN_API_KEY` as `YOUR_API_KEY` for the first startup.
### 4. Start the services ### 4. Start the services
@ -84,10 +84,10 @@ All three services should show `healthy` in the STATUS column. The `skyvern` con
Once healthy, retrieve your API key: Once healthy, retrieve your API key:
```bash ```bash
cat .streamlit/secrets.toml cat .skyvern/credentials.toml
``` ```
This file is auto-generated by Skyvern on first startup. The `.streamlit` path is a legacy artifact. The credentials inside are standard Skyvern API keys. This file is auto-generated by Skyvern on first startup. The credentials inside are standard Skyvern API keys.
You'll see output like: You'll see output like:
@ -100,21 +100,7 @@ configs = [
The `host` value uses the Docker-internal hostname `skyvern`. From your machine, use `http://localhost:8000` instead. You only need the `cred` value. This is your API key. The `host` value uses the Docker-internal hostname `skyvern`. From your machine, use `http://localhost:8000` instead. You only need the `cred` value. This is your API key.
### 6. Update frontend configuration ### 6. Verify the installation
Add your API key to the frontend environment:
```bash skyvern-frontend/.env
VITE_SKYVERN_API_KEY=eyJhbGciOiJIUzI1...
```
Restart the UI to pick up the change:
```bash
docker compose restart skyvern-ui
```
### 7. Verify the installation
Open [http://localhost:8080](http://localhost:8080) in your browser. You should see the Skyvern dashboard. Open [http://localhost:8080](http://localhost:8080) in your browser. You should see the Skyvern dashboard.
@ -162,7 +148,7 @@ Docker Compose mounts several directories for persistent storage:
| `./videos` | `/data/videos` | Browser session recordings | | `./videos` | `/data/videos` | Browser session recordings |
| `./har` | `/data/har` | HTTP Archive files for debugging | | `./har` | `/data/har` | HTTP Archive files for debugging |
| `./log` | `/data/log` | Application logs | | `./log` | `/data/log` | Application logs |
| `./.streamlit` | `/app/.streamlit` | Generated API credentials | | `./.skyvern` | `/app/.skyvern` | Generated API credentials |
--- ---
@ -280,7 +266,7 @@ This deletes all data including task history, credentials, and recordings.
```bash ```bash
docker compose down -v docker compose down -v
rm -rf postgres-data artifacts videos har log .streamlit rm -rf postgres-data artifacts videos har log .skyvern
docker compose up -d docker compose up -d
``` ```
@ -342,7 +328,7 @@ Common causes:
The API key is missing, malformed, or doesn't match the organization. Verify: The API key is missing, malformed, or doesn't match the organization. Verify:
1. The `x-api-key` header is included in your request 1. The `x-api-key` header is included in your request
2. The key matches exactly what's in `.streamlit/secrets.toml` 2. The key matches exactly what's in `.skyvern/credentials.toml`
3. No extra whitespace or newlines in the key 3. No extra whitespace or newlines in the key
```bash ```bash
@ -359,9 +345,9 @@ The API key format is invalid (JWT decode failed). This usually means:
Regenerate credentials by resetting the installation: Regenerate credentials by resetting the installation:
```bash ```bash
rm -rf .streamlit rm -rf .skyvern
docker compose restart skyvern docker compose restart skyvern
cat .streamlit/secrets.toml # Get new key cat .skyvern/credentials.toml # Get new key
``` ```
</Accordion> </Accordion>
@ -371,7 +357,7 @@ Check that `skyvern-frontend/.env` has the correct values:
```bash ```bash
VITE_API_BASE_URL=http://localhost:8000/api/v1 VITE_API_BASE_URL=http://localhost:8000/api/v1
VITE_WSS_BASE_URL=ws://localhost:8000/api/v1 VITE_WSS_BASE_URL=ws://localhost:8000/api/v1
VITE_SKYVERN_API_KEY=<your-key-from-secrets.toml> VITE_SKYVERN_API_KEY=<your-key-from-credentials.toml>
``` ```
After updating, restart the UI: After updating, restart the UI:

View file

@ -198,7 +198,7 @@ The backend pod takes 1-2 minutes to become ready as it runs database migrations
Wait for the backend pod to show `1/1` in the `READY` column of `kubectl get pods -n skyvern` before running this command. The API key file is generated during startup and won't exist until the pod is ready. Wait for the backend pod to show `1/1` in the `READY` column of `kubectl get pods -n skyvern` before running this command. The API key file is generated during startup and won't exist until the pod is ready.
```bash ```bash
kubectl exec -n skyvern deployment/skyvern-backend -- cat /app/.streamlit/secrets.toml kubectl exec -n skyvern deployment/skyvern-backend -- cat /app/.skyvern/credentials.toml
``` ```
Copy the `cred` value and update `frontend/frontend-secrets.yaml`: Copy the `cred` value and update `frontend/frontend-secrets.yaml`:
@ -434,7 +434,7 @@ This removes all resources in the `skyvern` namespace.
To clean up host storage (if using hostPath): To clean up host storage (if using hostPath):
```bash ```bash
rm -rf /data/artifacts /data/videos /data/har /data/log /app/.streamlit rm -rf /data/artifacts /data/videos /data/har /data/log /app/.skyvern
``` ```
--- ---

View file

@ -62,13 +62,15 @@ if [ "$run_migration" = true ]; then
alembic check alembic check
fi fi
if [ ! -f ".streamlit/secrets.toml" ]; then SKYVERN_CREDENTIALS_FILE="${SKYVERN_CREDENTIALS_FILE:-/app/.skyvern/credentials.toml}"
mkdir -p "$(dirname "$SKYVERN_CREDENTIALS_FILE")"
if [ ! -f "$SKYVERN_CREDENTIALS_FILE" ]; then
echo "Creating organization and API token..." echo "Creating organization and API token..."
org_output=$(python scripts/create_organization.py Skyvern-Open-Source) org_output=$(python scripts/create_organization.py Skyvern-Open-Source)
api_token=$(echo "$org_output" | awk '/token=/{gsub(/.*token='\''|'\''.*/, ""); print}') api_token=$(echo "$org_output" | awk '/token=/{gsub(/.*token='\''|'\''.*/, ""); print}')
# Update the secrets-open-source.toml file echo -e "[skyvern]\nconfigs = [\n {\"env\" = \"local\", \"host\" = \"http://skyvern:8000/api/v1\", \"orgs\" = [{name=\"Skyvern\", cred=\"$api_token\"}]}\n]" > "$SKYVERN_CREDENTIALS_FILE"
echo -e "[skyvern]\nconfigs = [\n {\"env\" = \"local\", \"host\" = \"http://skyvern:8000/api/v1\", \"orgs\" = [{name=\"Skyvern\", cred=\"$api_token\"}]}\n]" > .streamlit/secrets.toml echo "$SKYVERN_CREDENTIALS_FILE file updated with organization details."
echo ".streamlit/secrets.toml file updated with organization details."
fi fi
_kill_xvfb_on_term() { _kill_xvfb_on_term() {

View file

@ -8,18 +8,19 @@ VITE_WSS_BASE_URL="${VITE_WSS_BASE_URL:-ws://localhost:8000/api/v1}"
VITE_ARTIFACT_API_BASE_URL="${VITE_ARTIFACT_API_BASE_URL:-http://localhost:9090}" VITE_ARTIFACT_API_BASE_URL="${VITE_ARTIFACT_API_BASE_URL:-http://localhost:9090}"
# Priority for VITE_SKYVERN_API_KEY: # Priority for VITE_SKYVERN_API_KEY:
# 1. .streamlit/secrets.toml (generated by the backend on first run) # 1. Environment variable (from .env file or docker-compose environment),
# 2. Environment variable (from .env file or docker-compose environment),
# but only if it looks like a real key (not a placeholder) # but only if it looks like a real key (not a placeholder)
SECRETS_KEY=$(sed -n 's/.*cred\s*=\s*"\([^"]*\)".*/\1/p' .streamlit/secrets.toml 2>/dev/null || echo "") # 2. Generated credentials file from the backend first-run setup
if [ -n "$SECRETS_KEY" ]; then SKYVERN_CREDENTIALS_FILE="${SKYVERN_CREDENTIALS_FILE:-/app/.skyvern/credentials.toml}"
VITE_SKYVERN_API_KEY="$SECRETS_KEY" GENERATED_KEY=$(sed -n 's/.*cred\s*=\s*"\([^"]*\)".*/\1/p' "$SKYVERN_CREDENTIALS_FILE" 2>/dev/null || echo "")
echo "Using VITE_SKYVERN_API_KEY from .streamlit/secrets.toml" if [ -n "$VITE_SKYVERN_API_KEY" ] && [ "$VITE_SKYVERN_API_KEY" != "YOUR_API_KEY" ]; then
elif [ -n "$VITE_SKYVERN_API_KEY" ] && [ "$VITE_SKYVERN_API_KEY" != "YOUR_API_KEY" ]; then
VITE_SKYVERN_API_KEY=$(echo "$VITE_SKYVERN_API_KEY" | xargs) VITE_SKYVERN_API_KEY=$(echo "$VITE_SKYVERN_API_KEY" | xargs)
echo "Using VITE_SKYVERN_API_KEY from environment variable" echo "Using VITE_SKYVERN_API_KEY from environment variable"
elif [ -n "$GENERATED_KEY" ]; then
VITE_SKYVERN_API_KEY="$GENERATED_KEY"
echo "Using VITE_SKYVERN_API_KEY from $SKYVERN_CREDENTIALS_FILE"
else else
echo "WARNING: No VITE_SKYVERN_API_KEY found in .streamlit/secrets.toml or environment" echo "WARNING: No VITE_SKYVERN_API_KEY found in environment or $SKYVERN_CREDENTIALS_FILE"
VITE_SKYVERN_API_KEY="" VITE_SKYVERN_API_KEY=""
fi fi
@ -37,4 +38,3 @@ find /app/dist -name "*.js" -exec sed -i \
node localServer.js & node localServer.js &
node artifactServer.js & node artifactServer.js &
wait wait

View file

@ -31,11 +31,12 @@ spec:
mountPath: /data/har mountPath: /data/har
- name: log - name: log
mountPath: /data/log mountPath: /data/log
- name: streamlit - name: credentials
mountPath: /app/.streamlit mountPath: /app/.skyvern
readinessProbe: readinessProbe:
exec: httpGet:
command: ["test", "-f", "/app/.streamlit/secrets.toml"] path: /api/v1/heartbeat
port: 8000
initialDelaySeconds: 5 initialDelaySeconds: 5
periodSeconds: 5 periodSeconds: 5
timeoutSeconds: 5 timeoutSeconds: 5
@ -57,7 +58,7 @@ spec:
hostPath: hostPath:
path: /data/log path: /data/log
type: DirectoryOrCreate type: DirectoryOrCreate
- name: streamlit - name: credentials
hostPath: hostPath:
path: /app/.streamlit path: /app/.skyvern
type: DirectoryOrCreate type: DirectoryOrCreate

View file

@ -29,8 +29,8 @@ spec:
mountPath: /data/videos mountPath: /data/videos
- name: har - name: har
mountPath: /data/har mountPath: /data/har
- name: streamlit - name: credentials
mountPath: /app/.streamlit mountPath: /app/.skyvern
volumes: volumes:
- name: artifacts - name: artifacts
hostPath: hostPath:
@ -44,7 +44,7 @@ spec:
hostPath: hostPath:
path: /data/har path: /data/har
type: DirectoryOrCreate type: DirectoryOrCreate
- name: streamlit - name: credentials
hostPath: hostPath:
path: /app/.streamlit path: /app/.skyvern
type: DirectoryOrCreate type: DirectoryOrCreate

View file

@ -7,6 +7,7 @@ import importlib.util
import json import json
import os import os
import platform import platform
import re
import shutil import shutil
import socket import socket
import subprocess import subprocess
@ -25,6 +26,9 @@ from skyvern.cli.console import console
doctor_app = typer.Typer(help="Check Skyvern installation health.") doctor_app = typer.Typer(help="Check Skyvern installation health.")
GENERATED_CREDENTIALS_FILE = Path(".skyvern/credentials.toml")
LEGACY_STREAMLIT_CREDENTIALS_FILE = Path(".streamlit/secrets.toml")
@dataclass @dataclass
class CheckResult: class CheckResult:
@ -460,9 +464,27 @@ def _check_llm_config() -> CheckResult:
) )
def _read_credential_file(path: Path) -> str:
if not path.exists():
return ""
m = re.search(r'(?<![A-Za-z0-9_])cred\s*=\s*"([^"]*)"', path.read_text())
return m.group(1) if m else ""
def _read_generated_credential() -> str:
return _read_credential_file(GENERATED_CREDENTIALS_FILE)
def _read_legacy_streamlit_credential() -> str:
return _read_credential_file(LEGACY_STREAMLIT_CREDENTIALS_FILE)
def _check_api_key_consistency() -> CheckResult: def _check_api_key_consistency() -> CheckResult:
"""Check that API keys are consistent across backend .env, frontend .env, and secrets.toml.""" """Check that local API keys are consistent.
import re
Generated Docker credentials are only a fallback for compose startup; backend .env
remains the preferred source when present.
"""
from dotenv import dotenv_values from dotenv import dotenv_values
@ -470,35 +492,34 @@ def _check_api_key_consistency() -> CheckResult:
backend_env = resolve_backend_env_path() backend_env = resolve_backend_env_path()
frontend_env = Path("skyvern-frontend/.env") frontend_env = Path("skyvern-frontend/.env")
secrets_toml = Path(".streamlit/secrets.toml")
backend_key = dotenv_values(backend_env).get("SKYVERN_API_KEY", "") if backend_env.exists() else "" backend_key = dotenv_values(backend_env).get("SKYVERN_API_KEY", "") if backend_env.exists() else ""
frontend_raw = dotenv_values(frontend_env).get("VITE_SKYVERN_API_KEY", "") if frontend_env.exists() else "" frontend_raw = dotenv_values(frontend_env).get("VITE_SKYVERN_API_KEY", "") if frontend_env.exists() else ""
frontend_key = "" if frontend_raw in ("", "YOUR_API_KEY") else frontend_raw frontend_key = "" if frontend_raw in ("", "YOUR_API_KEY") else frontend_raw
generated_key = _read_generated_credential()
legacy_key = _read_legacy_streamlit_credential()
secrets_key = "" # Docker Compose can inject the generated credentials file into the UI
if secrets_toml.exists(): # without requiring VITE_SKYVERN_API_KEY in skyvern-frontend/.env.
m = re.search(r'cred\s*=\s*"([^"]*)"', secrets_toml.read_text()) # Legacy installs may only have the API key in the old compatibility file;
if m: # treat it as a migration source.
secrets_key = m.group(1) canonical = backend_key or generated_key or legacy_key
canonical = secrets_key or backend_key
if not canonical: if not canonical:
return CheckResult( return CheckResult(
name="API Key Consistency", name="API Key Consistency",
status="warn", status="warn",
detail="No API key found in backend .env or .streamlit/secrets.toml", detail="No API key found in backend .env or generated credentials",
hint="Run `skyvern init` or `skyvern quickstart` to generate an API key", hint="Run `skyvern init` or `skyvern quickstart` to generate an API key",
) )
mismatches: list[str] = [] mismatches: list[str] = []
if backend_key and backend_key != canonical: if not backend_key and not generated_key:
mismatches.append("backend .env differs from secrets.toml") mismatches.append("SKYVERN_API_KEY not set in backend .env")
if not frontend_env.exists(): if not frontend_env.exists():
mismatches.append("skyvern-frontend/.env missing") mismatches.append("skyvern-frontend/.env missing")
elif not frontend_key: elif not frontend_key and not generated_key:
mismatches.append("VITE_SKYVERN_API_KEY not set in frontend .env") mismatches.append("VITE_SKYVERN_API_KEY not set in frontend .env")
elif frontend_key != canonical: elif frontend_key and frontend_key != canonical:
mismatches.append("frontend .env differs from backend") mismatches.append("frontend .env differs from backend")
if mismatches: if mismatches:
@ -509,7 +530,51 @@ def _check_api_key_consistency() -> CheckResult:
hint="Run `skyvern doctor --fix` to sync API keys", hint="Run `skyvern doctor --fix` to sync API keys",
) )
return CheckResult(name="API Key Consistency", status="ok", detail="Keys consistent across all config files") return CheckResult(name="API Key Consistency", status="ok", detail="Backend and frontend API keys are consistent")
def _check_legacy_streamlit_secrets() -> CheckResult:
if not LEGACY_STREAMLIT_CREDENTIALS_FILE.exists():
return CheckResult(name="Legacy Streamlit Secrets", status="ok", detail="not present")
from dotenv import dotenv_values
from skyvern.utils.env_paths import resolve_backend_env_path
backend_env = resolve_backend_env_path()
backend_key = dotenv_values(backend_env).get("SKYVERN_API_KEY", "") if backend_env.exists() else ""
legacy_key = _read_legacy_streamlit_credential()
if not legacy_key:
return CheckResult(
name="Legacy Streamlit Secrets",
status="warn",
detail=".streamlit/secrets.toml exists but no cred value was found",
hint="Remove the file, or run `skyvern doctor --fix` to remove the deprecated file",
)
if not backend_key:
return CheckResult(
name="Legacy Streamlit Secrets",
status="warn",
detail=".streamlit/secrets.toml has a legacy API key but backend .env is missing SKYVERN_API_KEY",
hint="Run `skyvern doctor --fix` to migrate the key into .env and skyvern-frontend/.env",
)
if legacy_key != backend_key:
return CheckResult(
name="Legacy Streamlit Secrets",
status="warn",
detail=".streamlit/secrets.toml is deprecated and differs from backend .env",
hint="Run `skyvern doctor --fix` to remove the deprecated file",
)
return CheckResult(
name="Legacy Streamlit Secrets",
status="warn",
detail=".streamlit/secrets.toml matches backend .env; deprecated compatibility file only",
hint="Run `skyvern doctor --fix` to remove the deprecated file",
)
def _redact_password(db_string: str) -> str: def _redact_password(db_string: str) -> str:
@ -576,6 +641,7 @@ _CHECKS = [
_check_docker, _check_docker,
_check_llm_config, _check_llm_config,
_check_api_key_consistency, _check_api_key_consistency,
_check_legacy_streamlit_secrets,
_check_playwright_browser, _check_playwright_browser,
_check_port_8000, _check_port_8000,
_check_api_connectivity, _check_api_connectivity,
@ -603,6 +669,8 @@ def _try_fix(result: CheckResult) -> bool:
return _fix_install_playwright() return _fix_install_playwright()
if result.name == "API Key Consistency" and result.status == "error": if result.name == "API Key Consistency" and result.status == "error":
return _fix_api_key_consistency() return _fix_api_key_consistency()
if result.name == "Legacy Streamlit Secrets" and result.status == "warn":
return _fix_legacy_streamlit_secrets()
if result.name == "Docker" and "not running" in result.detail: if result.name == "Docker" and "not running" in result.detail:
console.print(" [yellow]→ Please start Docker Desktop manually[/yellow]") console.print(" [yellow]→ Please start Docker Desktop manually[/yellow]")
return False return False
@ -671,8 +739,6 @@ def _fix_start_postgres() -> bool:
def _fix_api_key_consistency() -> bool: def _fix_api_key_consistency() -> bool:
import re
from dotenv import dotenv_values, set_key from dotenv import dotenv_values, set_key
from skyvern.utils.env_paths import resolve_backend_env_path from skyvern.utils.env_paths import resolve_backend_env_path
@ -680,23 +746,22 @@ def _fix_api_key_consistency() -> bool:
backend_env = resolve_backend_env_path() backend_env = resolve_backend_env_path()
frontend_env = Path("skyvern-frontend/.env") frontend_env = Path("skyvern-frontend/.env")
frontend_example = Path("skyvern-frontend/.env.example") frontend_example = Path("skyvern-frontend/.env.example")
secrets_toml = Path(".streamlit/secrets.toml")
backend_key = dotenv_values(backend_env).get("SKYVERN_API_KEY", "") if backend_env.exists() else "" backend_key = dotenv_values(backend_env).get("SKYVERN_API_KEY", "") if backend_env.exists() else ""
secrets_key = "" generated_key = _read_generated_credential()
if secrets_toml.exists(): legacy_key = _read_legacy_streamlit_credential()
m = re.search(r'cred\s*=\s*"([^"]*)"', secrets_toml.read_text())
if m:
secrets_key = m.group(1)
canonical = secrets_key or backend_key canonical = backend_key or generated_key or legacy_key
if not canonical: if not canonical:
console.print(" [yellow]→ No source API key found to sync from[/yellow]") console.print(" [yellow]→ No source API key found to sync from[/yellow]")
return False return False
if not frontend_env.exists() and frontend_example.exists(): if not backend_env.exists():
import shutil backend_env.touch()
set_key(str(backend_env), "SKYVERN_API_KEY", canonical)
if not frontend_env.exists() and frontend_example.exists():
shutil.copy(frontend_example, frontend_env) shutil.copy(frontend_example, frontend_env)
console.print(" [cyan]Created skyvern-frontend/.env from .env.example[/cyan]") console.print(" [cyan]Created skyvern-frontend/.env from .env.example[/cyan]")
@ -705,8 +770,51 @@ def _fix_api_key_consistency() -> bool:
return False return False
set_key(str(frontend_env), "VITE_SKYVERN_API_KEY", canonical) set_key(str(frontend_env), "VITE_SKYVERN_API_KEY", canonical)
source = "secrets.toml" if secrets_key else "backend .env" source = (
console.print(f" [green]✅ Synced VITE_SKYVERN_API_KEY in skyvern-frontend/.env (from {source})[/green]") "backend .env"
if backend_key
else ".skyvern/credentials.toml"
if generated_key
else "legacy .streamlit/secrets.toml"
)
console.print(f" [green]✅ Synced local API key across backend and frontend (from {source})[/green]")
return True
def _fix_legacy_streamlit_secrets() -> bool:
"""Best-effort migration for old .streamlit/secrets.toml installs."""
from dotenv import dotenv_values, set_key
from skyvern.utils.env_paths import resolve_backend_env_path
backend_env = resolve_backend_env_path()
frontend_env = Path("skyvern-frontend/.env")
frontend_example = Path("skyvern-frontend/.env.example")
backend_key = dotenv_values(backend_env).get("SKYVERN_API_KEY", "") if backend_env.exists() else ""
legacy_key = _read_legacy_streamlit_credential()
if not LEGACY_STREAMLIT_CREDENTIALS_FILE.exists():
return False
if not legacy_key:
console.print(
" [yellow]→ Legacy .streamlit/secrets.toml has no cred value; leaving it for inspection[/yellow]"
)
return False
if not backend_key and legacy_key:
if not backend_env.exists():
backend_env.touch()
set_key(str(backend_env), "SKYVERN_API_KEY", legacy_key)
if not frontend_env.exists() and frontend_example.exists():
shutil.copy(frontend_example, frontend_env)
if frontend_env.exists():
set_key(str(frontend_env), "VITE_SKYVERN_API_KEY", legacy_key)
console.print(" [green]✅ Migrated legacy .streamlit API key into .env files[/green]")
LEGACY_STREAMLIT_CREDENTIALS_FILE.unlink()
console.print(" [green]✅ Removed deprecated .streamlit/secrets.toml[/green]")
return True return True

View file

@ -711,7 +711,7 @@ class Settings(BaseSettings):
def execute_all_steps(self) -> bool: def execute_all_steps(self) -> bool:
""" """
This provides the functionality to execute steps one by one through the Streamlit UI. This provides the functionality to execute steps one by one through the local UI.
***Value is always True if ENV is not local.*** ***Value is always True if ENV is not local.***
:return: True if env is not local, else the value of EXECUTE_ALL_STEPS :return: True if env is not local, else the value of EXECUTE_ALL_STEPS

View file

@ -0,0 +1,70 @@
from __future__ import annotations
from pathlib import Path
import pytest
from skyvern.cli import doctor
def _prepare_workspace(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
monkeypatch.chdir(tmp_path)
frontend = tmp_path / "skyvern-frontend"
frontend.mkdir()
(frontend / ".env.example").write_text("VITE_SKYVERN_API_KEY=YOUR_API_KEY\n")
return tmp_path
def _write_legacy_secret(tmp_path: Path, body: str) -> Path:
legacy = tmp_path / ".streamlit" / "secrets.toml"
legacy.parent.mkdir()
legacy.write_text(body)
return legacy
def test_legacy_streamlit_check_is_ok_when_file_missing(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
_prepare_workspace(tmp_path, monkeypatch)
result = doctor._check_legacy_streamlit_secrets()
assert result.status == "ok"
assert result.detail == "not present"
def test_legacy_streamlit_fix_preserves_unparseable_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
_prepare_workspace(tmp_path, monkeypatch)
legacy = _write_legacy_secret(tmp_path, '[general]\nnot_cred = "keep-me"\n')
result = doctor._check_legacy_streamlit_secrets()
assert result.status == "warn"
assert "no cred value" in result.detail
assert doctor._fix_legacy_streamlit_secrets() is False
assert legacy.exists()
def test_legacy_streamlit_fix_migrates_only_parseable_key(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
_prepare_workspace(tmp_path, monkeypatch)
legacy = _write_legacy_secret(tmp_path, '[general]\ncred = "legacy-key"\n')
result = doctor._check_legacy_streamlit_secrets()
assert result.status == "warn"
assert "backend .env is missing" in result.detail
assert doctor._fix_legacy_streamlit_secrets() is True
assert not legacy.exists()
assert "SKYVERN_API_KEY='legacy-key'" in (tmp_path / ".env").read_text()
assert "VITE_SKYVERN_API_KEY='legacy-key'" in (tmp_path / "skyvern-frontend" / ".env").read_text()
def test_legacy_streamlit_fix_removes_matching_deprecated_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
_prepare_workspace(tmp_path, monkeypatch)
legacy = _write_legacy_secret(tmp_path, '[general]\ncred = "same-key"\n')
(tmp_path / ".env").write_text('SKYVERN_API_KEY="same-key"\n')
result = doctor._check_legacy_streamlit_secrets()
assert result.status == "warn"
assert "deprecated compatibility file" in result.detail
assert doctor._fix_legacy_streamlit_secrets() is True
assert not legacy.exists()