{% endif %}
+ {% if param.from_env and param.is_secret %}
+ {# Secret from env: don't expose value in HTML, show masked placeholder #}
+
+ {% elif param.from_env %}
+ {# Non-secret from env: show the value, green border #}
+
+ {% else %}
+ {% endif %}
{% endmacro %}
diff --git a/src/supervaizer/admin/workbench_routes.py b/src/supervaizer/admin/workbench_routes.py
index 26731be..69794a6 100644
--- a/src/supervaizer/admin/workbench_routes.py
+++ b/src/supervaizer/admin/workbench_routes.py
@@ -71,16 +71,19 @@ def get_job_cases(job: Job) -> List[Any]:
return list(Cases().get_job_cases(job.id).values())
-def get_agent_parameters_from_env(agent: Agent) -> Dict[str, str]:
- """Pre-fill parameter values from environment variables."""
- values = {}
+def get_agent_parameters_from_env(agent: Agent) -> Dict[str, Dict[str, str]]:
+ """Pre-fill parameter values from environment variables.
+
+ Returns dict of {name: {"value": str, "from_env": bool}}.
+ """
+ values: Dict[str, Dict[str, str]] = {}
if agent.parameters_setup:
for name, param in agent.parameters_setup.definitions.items():
env_val = os.environ.get(name, "")
if env_val:
- values[name] = env_val
+ values[name] = {"value": env_val, "from_env": True}
elif param.value:
- values[name] = param.value
+ values[name] = {"value": param.value, "from_env": False}
return values
@@ -138,12 +141,14 @@ async def workbench_page(request: Request, slug: str) -> Response:
if agent.parameters_setup:
env_values = get_agent_parameters_from_env(agent)
for name, param in agent.parameters_setup.definitions.items():
+ env_info = env_values.get(name, {})
parameters.append({
"name": param.name,
"description": param.description,
"is_required": param.is_required,
"is_secret": param.is_secret,
- "value": env_values.get(name, ""),
+ "value": env_info.get("value", ""),
+ "from_env": env_info.get("from_env", False),
})
job_fields = []
@@ -200,10 +205,16 @@ async def workbench_start_job(request: Request, slug: str) -> Response:
parameters = body.get("parameters", {})
fields = body.get("fields", {})
- # Set parameters via os.environ (matching agent convention)
+ # Set parameters via os.environ (matching agent convention).
+ # Fall back to env values for empty fields (local mode pre-fill).
if agent.parameters_setup:
- for name, value in parameters.items():
- if name in agent.parameters_setup.definitions:
+ env_values = get_agent_parameters_from_env(agent)
+ for name in agent.parameters_setup.definitions:
+ value = parameters.get(name, "")
+ if not value:
+ env_info = env_values.get(name, {})
+ value = env_info.get("value", "")
+ if value:
agent.parameters_setup.definitions[name].set_value(value)
# Create job context and job (Job.__init__ auto-registers in Jobs() singleton)
@@ -219,9 +230,23 @@ async def workbench_start_job(request: Request, slug: str) -> Response:
)
# Build agent_parameters as list of dicts (matching AbstractJob.agent_parameters type)
+ # Merge submitted values with env fallbacks
+ merged_params = {}
+ if agent.parameters_setup:
+ for name in agent.parameters_setup.definitions:
+ value = parameters.get(name, "")
+ if not value:
+ env_info = env_values.get(name, {})
+ value = env_info.get("value", "")
+ if value:
+ merged_params[name] = value
+ # Also include any extra params not in definitions
+ for k, v in parameters.items():
+ if v and k not in merged_params:
+ merged_params[k] = v
agent_params_list = (
- [{"name": k, "value": v} for k, v in parameters.items()]
- if parameters
+ [{"name": k, "value": v} for k, v in merged_params.items()]
+ if merged_params
else None
)
diff --git a/src/supervaizer/cli.py b/src/supervaizer/cli.py
index 6493396..e62d9ff 100644
--- a/src/supervaizer/cli.py
+++ b/src/supervaizer/cli.py
@@ -7,10 +7,7 @@
import asyncio
import os
import shutil
-import signal
-import subprocess
import sys
-from typing import Any
from pathlib import Path
from typing import Optional
from supervaizer.deploy.cli import deploy_app
@@ -150,6 +147,22 @@ def start(
),
) -> None:
"""Start the Supervaizer Controller server."""
+ # In local mode, load .env file so agent parameters are available
+ if local:
+ env_file = os.path.join(os.getcwd(), ".env")
+ if os.path.isfile(env_file):
+ with open(env_file) as f:
+ for line in f:
+ line = line.strip()
+ if not line or line.startswith("#") or "=" not in line:
+ continue
+ key, _, value = line.partition("=")
+ key = key.strip()
+ value = value.strip()
+ # Don't override already-set env vars
+ if key and key not in os.environ:
+ os.environ[key] = value
+
# Set environment variables for the server configuration
os.environ["SUPERVAIZER_HOST"] = host
os.environ["SUPERVAIZER_PORT"] = str(port)
@@ -163,12 +176,16 @@ def start(
if local:
os.environ["SUPERVAIZER_LOCAL_MODE"] = "true"
+ # In local mode, force public_url to localhost unless explicitly provided
+ if public_url is None:
+ public_url = f"http://{host}:{port}"
+ os.environ["SUPERVAIZER_PUBLIC_URL"] = public_url
console.print(
f"[bold green]Starting Supervaizer Controller v{VERSION}[/] (local test mode)"
)
console.print("[dim]No Studio registration — agents run locally[/]")
api_key_display = os.environ.get("SUPERVAIZER_API_KEY") or "local-dev"
- base = public_url or f"http://{host}:{port}"
+ base = public_url
console.print(
f"[bold]API:[/] {base}/docs [bold]Admin/Workbench:[/] {base}/admin/"
)
@@ -179,12 +196,11 @@ def start(
os.environ.get("SUPERVAIZER_SCRIPT_PATH") or "supervaizer_control.py"
)
+ use_fallback_server = False
if not os.path.exists(script_path):
if local:
- # Use fallback script (Hello World only)
- import supervaizer.examples.local_server as fallback_module
-
- script_path = fallback_module.__file__
+ # Use fallback: launch a Server with Hello World agent directly
+ use_fallback_server = True
else:
console.print(f"[bold red]Error:[/] {script_path} not found")
console.print(
@@ -194,22 +210,81 @@ def start(
if not local:
console.print(f"[bold green]Starting Supervaizer Controller v{VERSION}[/]")
+
+ if use_fallback_server:
+ # No control script found — create a minimal Server with Hello World agent
+ console.print(
+ "[dim]No control script found — using built-in Hello World agent[/]"
+ )
+ from supervaizer.server import Server as _Server
+
+ server_instance = _Server(
+ agents=[],
+ supervisor_account=None,
+ a2a_endpoints=True,
+ admin_interface=True,
+ host=host,
+ port=port,
+ public_url=os.environ.get("SUPERVAIZER_PUBLIC_URL"),
+ debug=debug,
+ reload=reload,
+ environment=environment,
+ api_key=None,
+ )
+ server_instance.launch(log_level=log_level)
+ return
+
console.print(f"Loading configuration from [bold]{script_path}[/]")
- # Execute the script in a new Python process with proper signal handling
+ # Import the control script as a module and auto-launch the Server if needed.
+ # This allows scripts that only *define* a Server (without an `if __name__`
+ # guard calling launch()) to still work via `supervaizer start`.
+ import importlib.util
+
+ spec = importlib.util.spec_from_file_location("supervaizer_control", script_path)
+ if spec is None or spec.loader is None:
+ console.print(f"[bold red]Error:[/] Could not load {script_path} as a module")
+ sys.exit(1)
+
+ module = importlib.util.module_from_spec(spec)
+
+ # Add the script's directory to sys.path so relative imports work
+ script_dir = os.path.dirname(os.path.abspath(script_path))
+ if script_dir not in sys.path:
+ sys.path.insert(0, script_dir)
+
+ spec.loader.exec_module(module)
+
+ # Look for a Server instance that hasn't been launched yet.
+ # If the script already called launch() (via __main__ guard), uvicorn would
+ # be running and we'd never reach this point. So if we're here, we need to
+ # find the Server and call launch().
+ from supervaizer.server import Server as _Server
- def signal_handler(signum: int, frame: Any) -> None:
- # Send the signal to the subprocess
- if "process" in globals():
- globals()["process"].terminate()
- sys.exit(0)
+ server_instance = None
+ for attr_name in dir(module):
+ obj = getattr(module, attr_name, None)
+ if isinstance(obj, _Server):
+ server_instance = obj
+ break
- signal.signal(signal.SIGINT, signal_handler)
- signal.signal(signal.SIGTERM, signal_handler)
+ if server_instance is None:
+ console.print(
+ "[bold red]Error:[/] No Server instance found in the control script. "
+ "Define a variable of type supervaizer.Server in your script."
+ )
+ sys.exit(1)
+
+ # Override Server attributes with CLI values.
+ # Python default arguments (os.getenv in Server.__init__) are evaluated at
+ # class definition time, so if the module was already imported before the CLI
+ # set env vars, the Server instance has stale defaults.
+ server_instance.host = host
+ server_instance.port = port
+ if public_url is not None:
+ server_instance.public_url = public_url
- process = subprocess.Popen([sys.executable, script_path])
- globals()["process"] = process
- process.wait()
+ server_instance.launch(log_level=log_level)
def _create_instructions_file(
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 4007c17..d221e2f 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -65,28 +65,30 @@ def test_start_with_default_script_missing(self, runner: CliRunner) -> None:
def test_start_local_sets_env_and_runs_script(
self, runner: CliRunner, temp_script: str
) -> None:
- """--local sets SUPERVAIZER_LOCAL_MODE and runs the script normally."""
- with patch("subprocess.Popen") as mock_popen:
- mock_process = Mock()
- mock_process.wait.return_value = 0
- mock_popen.return_value = mock_process
+ """--local sets SUPERVAIZER_LOCAL_MODE and imports the script, finds Server, calls launch()."""
+ with patch("supervaizer.server.Server.launch") as mock_launch:
+ # Write a real control script that creates a Server instance
+ with open(temp_script, "w") as f:
+ f.write(
+ "from supervaizer.server import Server\n"
+ "from supervaizer.examples.local_server import get_default_local_agent\n"
+ "sv_server = Server(agents=[get_default_local_agent()])\n"
+ )
result = runner.invoke(app, ["start", "--local", temp_script])
assert "local test mode" in result.stdout
- mock_popen.assert_called_once()
assert os.environ.get("SUPERVAIZER_LOCAL_MODE") == "true"
+ mock_launch.assert_called_once()
def test_start_local_without_script_uses_fallback(self, runner: CliRunner) -> None:
- """--local without script_path and no supervaizer_control.py uses fallback."""
- with patch("subprocess.Popen") as mock_popen:
- mock_process = Mock()
- mock_process.wait.return_value = 0
- mock_popen.return_value = mock_process
- with patch("supervaizer.cli.os.path.exists", return_value=False):
- result = runner.invoke(app, ["start", "--local"])
- assert "local test mode" in result.stdout
- mock_popen.assert_called_once()
- call_args = mock_popen.call_args[0][0]
- assert "local_server.py" in call_args[1]
+ """--local without script_path and no supervaizer_control.py uses built-in fallback."""
+ with (
+ patch("supervaizer.server.Server.launch") as mock_launch,
+ patch("supervaizer.cli.os.path.exists", return_value=False),
+ ):
+ result = runner.invoke(app, ["start", "--local"])
+ assert "local test mode" in result.stdout
+ assert "built-in Hello World agent" in result.stdout
+ mock_launch.assert_called_once()
class TestCLIInstall: