From f062d4b19ecae2b1227aa9cd7751b66c528ca408 Mon Sep 17 00:00:00 2001 From: Homer Quan Date: Mon, 18 May 2026 13:40:53 -0400 Subject: [PATCH] Gate gRPC startup behind API flag --- README.md | 4 +-- lib/mirror_neuron/application.ex | 27 ++++++++++++------ tests/unit/application_test.exs | 47 ++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 tests/unit/application_test.exs diff --git a/README.md b/README.md index 2569174..e4ca549 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ Runtime configuration is read from environment variables in `config/runtime.exs` | `MN_NODE_GPU` | Auto-detected | Optional override for whether this runtime node advertises GPU capacity. | | `MN_CORE_HOST` | `localhost` | Host/IP used by the gRPC listener. | | `MN_GRPC_PORT` | `50051` | gRPC port. | -| `MN_API_ENABLED` | `true` | Enables API-related runtime config. | +| `MN_API_ENABLED` | `true` | Enables API-related runtime config, including the gRPC control-plane listener. Set to `false` to prevent the listener from starting. | | `MN_API_PORT` | `4000` | Core API config port. The separate `mn-api` package uses its own defaults. | | `MN_TEMP_DIR` | `/tmp/mirror_neuron` | Temporary runtime directory. | | `MN_OPENSHELL_BIN` | `openshell` | OpenShell executable path or command name. | @@ -297,7 +297,7 @@ MirrorNeuron Core includes protobuf definitions and generated Elixir modules for - `proto/cluster.proto` - `proto/observability.proto` -Generated modules live under `lib/mirror_neuron_grpc/`. +Generated modules live under `lib/mirror_neuron_grpc/`. The gRPC listener is controlled by `MN_API_ENABLED` and binds to `MN_CORE_HOST`, which defaults to loopback-only `localhost`. The separate REST API package is maintained in [`mn-api`](https://github.com/MirrorNeuronLab/mn-api). The Python SDK is maintained in [`mn-python-sdk`](https://github.com/MirrorNeuronLab/mn-python-sdk). diff --git a/lib/mirror_neuron/application.ex b/lib/mirror_neuron/application.ex index 34a1dc2..2f5c757 100644 --- a/lib/mirror_neuron/application.ex +++ b/lib/mirror_neuron/application.ex @@ -27,20 +27,13 @@ defmodule MirrorNeuron.Application do role = node_role() - grpc_port = String.to_integer(System.get_env("MN_GRPC_PORT", "50051")) - grpc_host = System.get_env("MN_CORE_HOST", "localhost") - grpc_bind_opts = grpc_bind_opts(grpc_host) - common_children = [ {Registry, keys: :duplicate, name: MirrorNeuron.Runtime.EventRegistry}, {Cluster.Supervisor, [topologies, [name: MirrorNeuron.ClusterSupervisor]]}, MirrorNeuron.Redis, - MirrorNeuron.Persistence.Retention, - {GRPC.Server.Supervisor, - [endpoint: MirrorNeuron.Grpc.Endpoint, port: grpc_port, start_server: true] ++ - grpc_bind_opts} - ] + MirrorNeuron.Persistence.Retention + ] ++ grpc_child_specs() children = case role do @@ -73,6 +66,22 @@ defmodule MirrorNeuron.Application do System.get_env("MN_NODE_ROLE", "runtime") end + @doc false + def grpc_child_specs do + if Config.boolean("MN_API_ENABLED", :api_enabled) do + grpc_port = String.to_integer(System.get_env("MN_GRPC_PORT", "50051")) + grpc_host = System.get_env("MN_CORE_HOST", "localhost") + + [ + {GRPC.Server.Supervisor, + [endpoint: MirrorNeuron.Grpc.Endpoint, port: grpc_port, start_server: true] ++ + grpc_bind_opts(grpc_host)} + ] + else + [] + end + end + defp grpc_bind_opts(host) when host in ["", "localhost"] do [ip: {127, 0, 0, 1}] end diff --git a/tests/unit/application_test.exs b/tests/unit/application_test.exs new file mode 100644 index 0000000..e02345a --- /dev/null +++ b/tests/unit/application_test.exs @@ -0,0 +1,47 @@ +defmodule MirrorNeuron.ApplicationTest do + use ExUnit.Case, async: false + + setup do + previous_api_enabled_env = System.get_env("MN_API_ENABLED") + previous_core_host_env = System.get_env("MN_CORE_HOST") + previous_grpc_port_env = System.get_env("MN_GRPC_PORT") + previous_api_enabled_config = Application.get_env(:mirror_neuron, :api_enabled) + + on_exit(fn -> + restore_env("MN_API_ENABLED", previous_api_enabled_env) + restore_env("MN_CORE_HOST", previous_core_host_env) + restore_env("MN_GRPC_PORT", previous_grpc_port_env) + restore_config(:api_enabled, previous_api_enabled_config) + end) + + :ok + end + + test "does not start the gRPC control plane when the API is disabled" do + System.put_env("MN_API_ENABLED", "false") + + assert MirrorNeuron.Application.grpc_child_specs() == [] + end + + test "starts the gRPC control plane on loopback when the API is enabled" do + System.put_env("MN_API_ENABLED", "true") + System.put_env("MN_GRPC_PORT", "50055") + System.delete_env("MN_CORE_HOST") + + assert [ + {GRPC.Server.Supervisor, + [ + endpoint: MirrorNeuron.Grpc.Endpoint, + port: 50055, + start_server: true, + ip: {127, 0, 0, 1} + ]} + ] = MirrorNeuron.Application.grpc_child_specs() + end + + defp restore_env(name, nil), do: System.delete_env(name) + defp restore_env(name, value), do: System.put_env(name, value) + + defp restore_config(key, nil), do: Application.delete_env(:mirror_neuron, key) + defp restore_config(key, value), do: Application.put_env(:mirror_neuron, key, value) +end