mirrorneuron-cli 1.2.7__tar.gz → 1.2.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. {mirrorneuron_cli-1.2.7/mirrorneuron_cli.egg-info → mirrorneuron_cli-1.2.8}/PKG-INFO +1 -1
  2. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8/mirrorneuron_cli.egg-info}/PKG-INFO +1 -1
  3. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/model_cmds.py +17 -1
  4. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/server_cmds.py +77 -10
  5. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_model_cmds.py +34 -0
  6. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_server_cmds.py +33 -0
  7. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/.github/workflows/ci.yml +0 -0
  8. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/.github/workflows/release.yml +0 -0
  9. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/.gitignore +0 -0
  10. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/.python-version +0 -0
  11. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/AGENTS.md +0 -0
  12. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/LICENSE +0 -0
  13. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/README.md +0 -0
  14. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/RELEASE.md +0 -0
  15. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mirrorneuron_cli.egg-info/SOURCES.txt +0 -0
  16. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mirrorneuron_cli.egg-info/dependency_links.txt +0 -0
  17. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mirrorneuron_cli.egg-info/entry_points.txt +0 -0
  18. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mirrorneuron_cli.egg-info/requires.txt +0 -0
  19. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mirrorneuron_cli.egg-info/top_level.txt +0 -0
  20. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/__init__.py +0 -0
  21. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/banner.py +0 -0
  22. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/config.py +0 -0
  23. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/error_handler.py +0 -0
  24. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/__init__.py +0 -0
  25. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/artifacts.py +0 -0
  26. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/backup_cmds.py +0 -0
  27. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/blueprint_cmds.py +0 -0
  28. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/blueprint_models.py +0 -0
  29. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/blueprint_observability.py +0 -0
  30. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/blueprint_repository.py +0 -0
  31. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/blueprint_resources.py +0 -0
  32. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/bundles.py +0 -0
  33. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/deployment_cmds.py +0 -0
  34. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/event_relay.py +0 -0
  35. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/job_cmds.py +0 -0
  36. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/resource_cmds.py +0 -0
  37. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/run_cmds.py +0 -0
  38. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/run_logs.py +0 -0
  39. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/run_manifest.py +0 -0
  40. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/runtime_health.py +0 -0
  41. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/schedule_cmds.py +0 -0
  42. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/service_cmds.py +0 -0
  43. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/skill_dependencies.py +0 -0
  44. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/skill_runtime.py +0 -0
  45. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/sys_cmds.py +0 -0
  46. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/ui.py +0 -0
  47. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/workflow_progress.py +0 -0
  48. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/libs/workflow_validation.py +0 -0
  49. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/logging_config.py +0 -0
  50. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/main.py +0 -0
  51. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/runtime_mode.py +0 -0
  52. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/runtime_state.py +0 -0
  53. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/schemas/workflow_manifest.schema.json +0 -0
  54. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/sdk_path.py +0 -0
  55. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/shared.py +0 -0
  56. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/terminal.py +0 -0
  57. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/mn_cli/update_cmds.py +0 -0
  58. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/pyproject.toml +0 -0
  59. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/scripts/check-release-artifacts.sh +0 -0
  60. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/scripts/make-release-zip.sh +0 -0
  61. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/scripts/validate-version-tag.sh +0 -0
  62. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/setup.cfg +0 -0
  63. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/conftest.py +0 -0
  64. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_backup_cmds.py +0 -0
  65. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_blueprint_cmds.py +0 -0
  66. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_blueprint_repository.py +0 -0
  67. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_blueprint_resources.py +0 -0
  68. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_deployment_cmds.py +0 -0
  69. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_docker_network_integration.py +0 -0
  70. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_job_cmds.py +0 -0
  71. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_main.py +0 -0
  72. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_repo_hygiene.py +0 -0
  73. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_resource_cmds.py +0 -0
  74. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_run_cmds.py +0 -0
  75. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_run_helpers.py +0 -0
  76. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_runtime_health.py +0 -0
  77. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_runtime_mode.py +0 -0
  78. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_runtime_state.py +0 -0
  79. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_schedule_cmds.py +0 -0
  80. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_service_cmds.py +0 -0
  81. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_shared.py +0 -0
  82. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_sys_cmds.py +0 -0
  83. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_terminal.py +0 -0
  84. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_ui.py +0 -0
  85. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_update_cmds.py +0 -0
  86. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/tests/test_workflow_validation.py +0 -0
  87. {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.8}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mirrorneuron-cli
3
- Version: 1.2.7
3
+ Version: 1.2.8
4
4
  Summary: MirrorNeuron CLI
5
5
  License-Expression: MIT
6
6
  Classifier: Programming Language :: Python :: 3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mirrorneuron-cli
3
- Version: 1.2.7
3
+ Version: 1.2.8
4
4
  Summary: MirrorNeuron CLI
5
5
  License-Expression: MIT
6
6
  Classifier: Programming Language :: Python :: 3
@@ -250,7 +250,7 @@ def install_model_entry(
250
250
  target = docker_model_name(entry)
251
251
  if _docker_model_cli_available():
252
252
  _ensure_runner(compatibility.backend, compatibility.accelerator)
253
- _docker(["model", "pull", target], timeout=900, stream=True)
253
+ _docker_model_pull(target)
254
254
  run_command = ["model", "run", "--detach"]
255
255
  resolved_context = context_size or entry.get("context_size")
256
256
  if resolved_context and _docker_model_run_supports_context_size():
@@ -269,6 +269,22 @@ def install_model_entry(
269
269
  }
270
270
 
271
271
 
272
+ def _docker_model_pull(target: str, *, attempts: int = 2) -> None:
273
+ last_error: RuntimeError | None = None
274
+ for attempt in range(1, attempts + 1):
275
+ try:
276
+ _docker(["model", "pull", target], timeout=900, stream=True)
277
+ return
278
+ except RuntimeError as exc:
279
+ if _model_installed(target):
280
+ return
281
+ last_error = exc
282
+ if attempt < attempts:
283
+ console.print("[yellow]Docker model pull failed; retrying once...[/yellow]")
284
+ if last_error is not None:
285
+ raise last_error
286
+
287
+
272
288
  def remove_model_ref(model: str, *, force: bool = False) -> None:
273
289
  if _docker_model_cli_available():
274
290
  command = ["model", "rm"]
@@ -112,6 +112,10 @@ RUNTIME_MODELS_OVERRIDE_FILE = "docker-compose.models.yml"
112
112
  DEFAULT_LLM_MODEL_RUNNER_MODEL = "ai/gemma4:E2B"
113
113
  DEFAULT_CONTEXT_MODEL_RUNNER_MODEL = "hf.co/homerquan/mn-context-engine-model-v-Q4_K_M"
114
114
  DEFAULT_MEMBRANE_REPO = "MirrorNeuronLab/Membrane"
115
+ DEFAULT_MEMBRANE_ENGINE_IMAGE_REPOSITORY = (
116
+ "us-central1-docker.pkg.dev/mirrorneuron-public-packages/"
117
+ "mirrorneuron-runtime/membrane-context-engine"
118
+ )
115
119
  CONTEXT_ENGINE_SERVICE = "membrane-context-engine"
116
120
  CONTEXT_ENGINE_CONTAINER = "mirror-neuron-context-engine"
117
121
  CONTEXT_ENGINE_MODEL_CONTAINER = "mirror-neuron-context-engine-model"
@@ -2000,14 +2004,23 @@ def ensure_context_engine_runtime(*, force: bool = False) -> dict[str, str]:
2000
2004
  )
2001
2005
 
2002
2006
  env = _runtime_base_env(True)
2003
- source_dir = _ensure_context_engine_source(env)
2004
2007
  profiles = _compose_profiles_with(env.get("COMPOSE_PROFILES"), "context")
2005
2008
  model = str(env.get("MN_CONTEXT_MODEL_RUNNER_MODEL") or DEFAULT_CONTEXT_MODEL_RUNNER_MODEL)
2009
+ engine_image = _context_engine_release_image(env)
2010
+ use_engine_image = _context_engine_image_mode_enabled(env, engine_image)
2006
2011
  updates = {
2007
2012
  "COMPOSE_PROFILES": profiles,
2008
- "MEMBRANE_DIR": str(source_dir),
2009
2013
  "MN_CONTEXT_MODEL_RUNNER_MODEL": model,
2010
2014
  }
2015
+ source_dir: Path | None = None
2016
+ if use_engine_image:
2017
+ updates["ENGINE_IMAGE"] = engine_image
2018
+ updates["MN_MEMBRANE_ENGINE_IMAGE"] = engine_image
2019
+ _remove_env_file_keys(RUNTIME_COMPOSE_ENV, {"MEMBRANE_DIR"})
2020
+ env.pop("MEMBRANE_DIR", None)
2021
+ else:
2022
+ source_dir = _ensure_context_engine_source(env)
2023
+ updates["MEMBRANE_DIR"] = str(source_dir)
2011
2024
  _write_env_file_values(RUNTIME_COMPOSE_ENV, updates)
2012
2025
  env.update(updates)
2013
2026
 
@@ -2017,14 +2030,24 @@ def ensure_context_engine_runtime(*, force: bool = False) -> dict[str, str]:
2017
2030
 
2018
2031
  already_running = _docker_container_running(CONTEXT_ENGINE_CONTAINER)
2019
2032
  if force or not already_running:
2033
+ if use_engine_image:
2034
+ subprocess.run(
2035
+ runtime_compose_cmd("pull", CONTEXT_ENGINE_SERVICE),
2036
+ check=True,
2037
+ stdout=subprocess.DEVNULL,
2038
+ env=env,
2039
+ )
2040
+ up_args = ("up", "-d", "--no-build", CONTEXT_ENGINE_SERVICE)
2041
+ else:
2042
+ subprocess.run(
2043
+ runtime_compose_cmd("build", CONTEXT_ENGINE_SERVICE),
2044
+ check=True,
2045
+ stdout=subprocess.DEVNULL,
2046
+ env=env,
2047
+ )
2048
+ up_args = ("up", "-d", CONTEXT_ENGINE_SERVICE)
2020
2049
  subprocess.run(
2021
- runtime_compose_cmd("build", CONTEXT_ENGINE_SERVICE),
2022
- check=True,
2023
- stdout=subprocess.DEVNULL,
2024
- env=env,
2025
- )
2026
- subprocess.run(
2027
- runtime_compose_cmd("up", "-d", CONTEXT_ENGINE_SERVICE),
2050
+ runtime_compose_cmd(*up_args),
2028
2051
  check=True,
2029
2052
  stdout=subprocess.DEVNULL,
2030
2053
  env=env,
@@ -2038,8 +2061,8 @@ def ensure_context_engine_runtime(*, force: bool = False) -> dict[str, str]:
2038
2061
  "service": CONTEXT_ENGINE_SERVICE,
2039
2062
  "container": CONTEXT_ENGINE_CONTAINER,
2040
2063
  "model": model,
2041
- "membrane_dir": str(source_dir),
2042
2064
  "compose_profiles": profiles,
2065
+ **({"engine_image": engine_image} if use_engine_image else {"membrane_dir": str(source_dir)}),
2043
2066
  }
2044
2067
 
2045
2068
  def _compose_profiles_with(value: object, required_profile: str) -> str:
@@ -2058,6 +2081,50 @@ def _compose_profiles_with(value: object, required_profile: str) -> str:
2058
2081
  profiles.append(required_profile)
2059
2082
  return ",".join(profiles)
2060
2083
 
2084
+ def _normalized_release_image_tag(value: object) -> str:
2085
+ tag = str(value or "").strip()
2086
+ if not tag:
2087
+ return ""
2088
+ return tag if tag.startswith("v") else f"v{tag}"
2089
+
2090
+ def _context_engine_release_image(env: dict[str, str]) -> str:
2091
+ explicit = str(
2092
+ os.getenv("MN_MEMBRANE_ENGINE_IMAGE")
2093
+ or env.get("MN_MEMBRANE_ENGINE_IMAGE")
2094
+ or os.getenv("MN_CONTEXT_ENGINE_IMAGE")
2095
+ or env.get("MN_CONTEXT_ENGINE_IMAGE")
2096
+ or os.getenv("ENGINE_IMAGE")
2097
+ or env.get("ENGINE_IMAGE")
2098
+ or ""
2099
+ ).strip()
2100
+ if explicit:
2101
+ return explicit
2102
+
2103
+ tag = _normalized_release_image_tag(
2104
+ os.getenv("MN_MEMBRANE_ENGINE_IMAGE_TAG")
2105
+ or env.get("MN_MEMBRANE_ENGINE_IMAGE_TAG")
2106
+ or os.getenv("MN_RUNTIME_MODULE_VERSION")
2107
+ or env.get("MN_RUNTIME_MODULE_VERSION")
2108
+ or os.getenv("MN_PACKAGE_VERSION")
2109
+ or env.get("MN_PACKAGE_VERSION")
2110
+ )
2111
+ if not tag:
2112
+ return ""
2113
+ repository = str(
2114
+ os.getenv("MN_MEMBRANE_ENGINE_IMAGE_REPOSITORY")
2115
+ or env.get("MN_MEMBRANE_ENGINE_IMAGE_REPOSITORY")
2116
+ or DEFAULT_MEMBRANE_ENGINE_IMAGE_REPOSITORY
2117
+ ).strip().rstrip("/")
2118
+ return f"{repository}:{tag}" if repository else ""
2119
+
2120
+ def _context_engine_image_mode_enabled(env: dict[str, str], image: str) -> bool:
2121
+ mode = str(os.getenv("MN_MEMBRANE_SOURCE_MODE") or env.get("MN_MEMBRANE_SOURCE_MODE") or "").strip().lower()
2122
+ if mode in {"source", "git", "checkout", "local"}:
2123
+ return False
2124
+ if mode in {"image", "docker", "gar", "release"}:
2125
+ return bool(image)
2126
+ return bool(image and image != "mirror-neuron-memory-engine:latest")
2127
+
2061
2128
  def _context_engine_git_url(env: dict[str, str]) -> str:
2062
2129
  explicit = str(os.getenv("MN_MEMBRANE_GIT_URL") or env.get("MN_MEMBRANE_GIT_URL") or "").strip()
2063
2130
  if explicit:
@@ -128,6 +128,38 @@ def test_model_install_streams_pull_progress(mocker):
128
128
  assert pull_kwargs["capture_output"] is False
129
129
 
130
130
 
131
+ def test_model_install_retries_transient_pull_failure(mocker):
132
+ calls = []
133
+ pull_attempts = 0
134
+
135
+ def fake_run(command, **kwargs):
136
+ nonlocal pull_attempts
137
+ calls.append(command)
138
+ if command[:4] == ["docker", "model", "status", "--json"]:
139
+ return _completed(command, stdout=json.dumps({"running": True, "backends": {"llama.cpp": "Running"}}))
140
+ if command[:4] == ["docker", "model", "run", "--help"]:
141
+ return _completed(command, stdout="Options:\n")
142
+ if command == ["docker", "model", "pull", "ai/gemma4:E2B"]:
143
+ pull_attempts += 1
144
+ if pull_attempts == 1:
145
+ return _completed(command, returncode=1, stderr="writing blob: blob digest mismatch")
146
+ if command == ["docker", "model", "inspect", "ai/gemma4:E2B"]:
147
+ return _completed(command, returncode=1)
148
+ return _completed(command)
149
+
150
+ mocker.patch("subprocess.run", side_effect=fake_run)
151
+ mocker.patch(
152
+ "mn_sdk.model_runtime.detect_host_hardware",
153
+ return_value=HostHardwareProfile("darwin", "arm64", total_memory_gb=16, unified_memory_gb=16, has_apple_silicon=True),
154
+ )
155
+
156
+ result = runner.invoke(app, ["model", "install", "gemma4:e2b"])
157
+
158
+ assert result.exit_code == 0
159
+ assert calls.count(["docker", "model", "pull", "ai/gemma4:E2B"]) == 2
160
+ assert ["docker", "model", "run", "--detach", "ai/gemma4:E2B"] in calls
161
+
162
+
131
163
  def test_model_install_persists_manual_ownership_record(mocker):
132
164
  def fake_run(command, **kwargs):
133
165
  if command[:4] == ["docker", "model", "status", "--json"]:
@@ -293,6 +325,8 @@ def test_model_install_failure_does_not_record_manual_ownership(mocker, tmp_path
293
325
  return _completed(command, stdout=json.dumps({"running": True, "backends": {"llama.cpp": "Running"}}))
294
326
  if command[:3] == ["docker", "model", "pull"]:
295
327
  return _completed(command, returncode=1, stderr="pull failed")
328
+ if command[:3] == ["docker", "model", "inspect"]:
329
+ return _completed(command, returncode=1)
296
330
  return _completed(command)
297
331
 
298
332
  mocker.patch("subprocess.run", side_effect=fake_run)
@@ -588,6 +588,39 @@ def test_ensure_context_engine_runtime_persists_profile_and_starts_compose(mocke
588
588
  assert run.call_args_list[0].args[0] == runtime_compose_cmd("build", "membrane-context-engine")
589
589
  assert run.call_args_list[1].args[0] == runtime_compose_cmd("up", "-d", "membrane-context-engine")
590
590
 
591
+ def test_ensure_context_engine_runtime_uses_release_image_without_source_clone(mocker):
592
+ server_cmds.RUNTIME_COMPOSE_ENV.parent.mkdir(parents=True, exist_ok=True)
593
+ server_cmds.RUNTIME_COMPOSE_ENV.write_text(
594
+ "COMPOSE_PROJECT_NAME=mirror-neuron\n"
595
+ "COMPOSE_PROFILES=openshell\n"
596
+ "MN_RUNTIME_MODULE_VERSION=1.2.7\n"
597
+ "MEMBRANE_DIR=/private/membrane\n",
598
+ encoding="utf-8",
599
+ )
600
+ server_cmds.RUNTIME_COMPOSE_FILE.write_text("services: {}\n", encoding="utf-8")
601
+ ensure_source = mocker.patch("mn_cli.server_cmds._ensure_context_engine_source")
602
+ mocker.patch("mn_cli.server_cmds._ensure_docker_model_runner")
603
+ mocker.patch("mn_cli.server_cmds._remove_non_mirror_neuron_container")
604
+ mocker.patch("mn_cli.server_cmds._docker_container_running", return_value=False)
605
+ run = mocker.patch("mn_cli.server_cmds.subprocess.run")
606
+
607
+ result = server_cmds.ensure_context_engine_runtime()
608
+
609
+ env = server_cmds._read_env_file(server_cmds.RUNTIME_COMPOSE_ENV)
610
+ expected_image = (
611
+ "us-central1-docker.pkg.dev/mirrorneuron-public-packages/"
612
+ "mirrorneuron-runtime/membrane-context-engine:v1.2.7"
613
+ )
614
+ assert env["COMPOSE_PROFILES"] == "openshell,context"
615
+ assert env["ENGINE_IMAGE"] == expected_image
616
+ assert env["MN_MEMBRANE_ENGINE_IMAGE"] == expected_image
617
+ assert "MEMBRANE_DIR" not in env
618
+ assert result["status"] == "started"
619
+ assert result["engine_image"] == expected_image
620
+ ensure_source.assert_not_called()
621
+ assert run.call_args_list[0].args[0] == runtime_compose_cmd("pull", "membrane-context-engine")
622
+ assert run.call_args_list[1].args[0] == runtime_compose_cmd("up", "-d", "--no-build", "membrane-context-engine")
623
+
591
624
  def test_ensure_context_engine_runtime_skips_compose_when_already_running(mocker, tmp_path):
592
625
  membrane_dir = tmp_path / "Membrane"
593
626
  membrane_dir.mkdir()