Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/flow/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,26 +109,39 @@ def deploy(
model_t2v: str = typer.Option(None, help="T2V model id"),
model_i2v: str = typer.Option(None, help="I2V model id"),
region: str = typer.Option(None, help="Region (AWS/GCP)"),
modal_token_id: str = typer.Option(
None, help="Modal token id (else uses ambient modal auth / ~/.modal.toml)"
),
modal_token_secret: str = typer.Option(
None, help="Modal token secret (passed per-invocation, not via global env)"
),
config_path: str = typer.Option("config/config.toml", help="Path to config file"),
) -> None:
"""Deploy the GPU backend to a provider as a named compute instance.

A token alone can't generate — this deploys the open-source Wan backend into
your account and reports the endpoint URL to register. CLI flags override the
deploy section of your config file.
deploy section of your config file. Modal credentials may be passed as args
(for unattended/multi-account deploys) or left to ambient modal auth.
"""
from flow.config import load_config
from flow.deploy import DeploySpec, available_providers, get_deployer

d = load_config(config_path).deploy
provider = provider or d.provider
credentials: dict = {}
if modal_token_id:
credentials["token_id"] = modal_token_id
if modal_token_secret:
credentials["token_secret"] = modal_token_secret
spec = DeploySpec(
name=name or d.name,
gpu=gpu or d.gpu,
model_t2v=model_t2v or d.model_t2v,
model_i2v=model_i2v or d.model_i2v,
region=region or d.region,
scaledown_window=d.scaledown_window,
credentials=credentials,
)

try:
Expand Down
5 changes: 5 additions & 0 deletions src/flow/deploy/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ class DeploySpec:
model_i2v: str = "Wan-AI/Wan2.2-I2V-A14B-Diffusers"
region: str = ""
scaledown_window: int = 300
# Provider credentials passed *per invocation* (e.g. Modal token_id/secret).
# Deployers inject these into the deploy subprocess's env only — never the
# ambient process env — so a shared host can deploy for many accounts
# concurrently without leaking tokens between deploys.
credentials: dict = field(default_factory=dict)
extra: dict = field(default_factory=dict)


Expand Down
10 changes: 10 additions & 0 deletions src/flow/deploy/modal_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ def deploy(self, spec: DeploySpec) -> DeployResult:
"FLOW_MODEL_I2V": spec.model_i2v,
"FLOW_GPU_SCALEDOWN": str(spec.scaledown_window),
}
# Per-invocation Modal auth, if provided as args, scoped to THIS
# subprocess only (never the ambient env) — so a shared host can deploy
# for many accounts concurrently. If omitted, ambient modal auth
# (~/.modal.toml or existing MODAL_TOKEN_* env) is used as before.
creds = spec.credentials or {}
if creds.get("token_id"):
env["MODAL_TOKEN_ID"] = creds["token_id"]
if creds.get("token_secret"):
env["MODAL_TOKEN_SECRET"] = creds["token_secret"]

try:
proc = subprocess.run(
["modal", "deploy", str(server)],
Expand Down
34 changes: 34 additions & 0 deletions tests/test_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,40 @@ def test_modal_deploy_fails_cleanly_without_cli(monkeypatch):
assert "modal CLI not found" in res.detail


def test_modal_deploy_injects_token_per_invocation(monkeypatch):
"""Credentials passed as args land in the subprocess env only, never os.environ."""
import os
import types

monkeypatch.setattr(
"flow.deploy.modal_deploy.shutil.which", lambda _: "/usr/bin/modal"
)
captured: dict = {}

def fake_run(cmd, env=None, **kw):
captured["env"] = env
return types.SimpleNamespace(
returncode=0,
stdout="Created web endpoint => https://ws--a100.modal.run",
stderr="",
)

monkeypatch.setattr("flow.deploy.modal_deploy.subprocess.run", fake_run)

res = ModalDeployer().deploy(
DeploySpec(name="a100", credentials={"token_id": "ti", "token_secret": "ts"})
)

assert res.status == "deployed"
assert res.endpoint_url == "https://ws--a100.modal.run"
# Token is scoped to the deploy subprocess...
assert captured["env"]["MODAL_TOKEN_ID"] == "ti"
assert captured["env"]["MODAL_TOKEN_SECRET"] == "ts"
assert captured["env"]["FLOW_GPU_APP_NAME"] == "a100"
# ...and never leaks into the ambient process env.
assert "MODAL_TOKEN_ID" not in os.environ


def test_aws_gcp_are_honest_scaffolds():
for provider in ("aws", "gcp"):
res = get_deployer(provider).deploy(DeploySpec(name=f"{provider}-inst"))
Expand Down
Loading