diff --git a/api/commands.py b/api/commands.py index 11db0e0398..dbd353858b 100644 --- a/api/commands.py +++ b/api/commands.py @@ -1395,6 +1395,60 @@ def file_usage( click.echo(click.style(f"Use --offset {offset + limit} to see next page", fg="white")) +@click.command("setup-sandbox-system-config", help="Setup system-level sandbox provider configuration.") +@click.option( + "--provider-type", prompt=True, type=click.Choice(["e2b", "docker", "local"]), help="Sandbox provider type" +) +@click.option("--config", prompt=True, help='Configuration JSON (e.g., {"api_key": "xxx"} for e2b)') +def setup_sandbox_system_config(provider_type: str, config: str): + """ + Setup system-level sandbox provider configuration. + + Examples: + flask setup-sandbox-system-config --provider-type e2b --config '{"api_key": "e2b_xxx"}' + flask setup-sandbox-system-config --provider-type docker --config '{"docker_sock": "unix:///var/run/docker.sock"}' + flask setup-sandbox-system-config --provider-type local --config '{}' + """ + from models.sandbox import SandboxProviderSystemConfig + from services.sandbox.sandbox_provider_service import PROVIDER_CONFIG_MODELS + + try: + click.echo(click.style(f"Validating config: {config}", fg="yellow")) + config_dict = TypeAdapter(dict[str, Any]).validate_json(config) + click.echo(click.style("Config validated successfully.", fg="green")) + + click.echo(click.style(f"Validating config schema for provider type: {provider_type}", fg="yellow")) + model_class = PROVIDER_CONFIG_MODELS.get(provider_type) + if model_class: + model_class.model_validate(config_dict) + click.echo(click.style("Config schema validated successfully.", fg="green")) + + click.echo(click.style("Encrypting config...", fg="yellow")) + click.echo(click.style(f"Using SECRET_KEY: `{dify_config.SECRET_KEY}`", fg="yellow")) + encrypted_config = encrypt_system_params(config_dict) + click.echo(click.style("Config encrypted successfully.", fg="green")) + except Exception as e: + click.echo(click.style(f"Error validating/encrypting config: {str(e)}", fg="red")) + return + + deleted_count = db.session.query(SandboxProviderSystemConfig).filter_by(provider_type=provider_type).delete() + if deleted_count > 0: + click.echo( + click.style( + f"Deleted {deleted_count} existing system config for provider type: {provider_type}", fg="yellow" + ) + ) + + system_config = SandboxProviderSystemConfig( + provider_type=provider_type, + encrypted_config=encrypted_config, + ) + db.session.add(system_config) + db.session.commit() + click.echo(click.style(f"Sandbox system config setup successfully. id: {system_config.id}", fg="green")) + click.echo(click.style(f"Provider type: {provider_type}", fg="green")) + + @click.command("setup-system-tool-oauth-client", help="Setup system tool oauth client.") @click.option("--provider", prompt=True, help="Provider name") @click.option("--client-params", prompt=True, help="Client Params") diff --git a/api/extensions/ext_commands.py b/api/extensions/ext_commands.py index daa3756dba..960113d51e 100644 --- a/api/extensions/ext_commands.py +++ b/api/extensions/ext_commands.py @@ -23,6 +23,7 @@ def init_app(app: DifyApp): reset_encrypt_key_pair, reset_password, setup_datasource_oauth_client, + setup_sandbox_system_config, setup_system_tool_oauth_client, setup_system_trigger_oauth_client, transform_datasource_credentials, @@ -49,6 +50,7 @@ def init_app(app: DifyApp): clear_orphaned_file_records, remove_orphaned_files_on_storage, file_usage, + setup_sandbox_system_config, setup_system_tool_oauth_client, setup_system_trigger_oauth_client, cleanup_orphaned_draft_variables, diff --git a/api/services/sandbox/sandbox_provider_service.py b/api/services/sandbox/sandbox_provider_service.py index fb749ae0ff..baf4d36e8d 100644 --- a/api/services/sandbox/sandbox_provider_service.py +++ b/api/services/sandbox/sandbox_provider_service.py @@ -120,29 +120,24 @@ class SandboxProviderService: @classmethod def list_providers(cls, tenant_id: str) -> list[SandboxProviderInfo]: - available_types = cls.get_available_provider_types() result: list[SandboxProviderInfo] = [] with Session(db.engine, expire_on_commit=False) as session: - tenant_configs = session.query(SandboxProvider).filter(SandboxProvider.tenant_id == tenant_id).all() - tenant_config_map = {cfg.provider_type: cfg for cfg in tenant_configs} + tenant_configs = { + cfg.provider_type: cfg + for cfg in session.query(SandboxProvider).filter(SandboxProvider.tenant_id == tenant_id).all() + } + system_defaults = {cfg.provider_type for cfg in session.query(SandboxProviderSystemConfig).all()} - system_defaults = session.query(SandboxProviderSystemConfig).all() - system_default_map = {cfg.provider_type: cfg for cfg in system_defaults} - - for provider_type in available_types: + for provider_type in cls.get_available_provider_types(): + tenant_config = tenant_configs.get(provider_type) + schema = PROVIDER_CONFIG_SCHEMAS.get(provider_type, []) metadata = PROVIDER_METADATA.get(provider_type, {}) - config_schema = PROVIDER_CONFIG_SCHEMAS.get(provider_type, []) - - tenant_config = tenant_config_map.get(provider_type) - system_default = system_default_map.get(provider_type) config: Mapping[str, Any] = {} if tenant_config and tenant_config.config: - schema = PROVIDER_CONFIG_SCHEMAS.get(provider_type, []) encrypter, _ = create_sandbox_config_encrypter(tenant_id, schema, provider_type) - decrypted = encrypter.decrypt(tenant_config.config) - config = masked_config(schema, decrypted) + config = masked_config(schema, encrypter.decrypt(tenant_config.config)) result.append( SandboxProviderInfo( @@ -150,11 +145,11 @@ class SandboxProviderService: label=metadata.get("label", provider_type), description=metadata.get("description", ""), icon=metadata.get("icon", provider_type), - is_system_configured=system_default is not None, + is_system_configured=provider_type in system_defaults and tenant_config is None, is_tenant_configured=tenant_config is not None, is_active=tenant_config.is_active if tenant_config else False, config=config, - config_schema=[{"name": c.name, "type": c.type.value} for c in config_schema], + config_schema=[{"name": c.name, "type": c.type.value} for c in schema], ) ) @@ -243,9 +238,6 @@ class SandboxProviderService: if not config: return {"result": "success"} - if config.is_active: - raise ValueError("Cannot delete config for the active provider. Switch to another provider first.") - session.delete(config) session.commit()