mirrorneuron-cli 1.2.7__tar.gz → 1.2.15__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.
- {mirrorneuron_cli-1.2.7/mirrorneuron_cli.egg-info → mirrorneuron_cli-1.2.15}/PKG-INFO +1 -1
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15/mirrorneuron_cli.egg-info}/PKG-INFO +1 -1
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/model_cmds.py +40 -3
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/skill_dependencies.py +12 -1
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/sys_cmds.py +11 -5
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/server_cmds.py +194 -17
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_model_cmds.py +69 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_run_cmds.py +24 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_run_helpers.py +3 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_server_cmds.py +124 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/.github/workflows/ci.yml +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/.github/workflows/release.yml +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/.gitignore +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/.python-version +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/AGENTS.md +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/LICENSE +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/README.md +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/RELEASE.md +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mirrorneuron_cli.egg-info/SOURCES.txt +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mirrorneuron_cli.egg-info/dependency_links.txt +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mirrorneuron_cli.egg-info/entry_points.txt +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mirrorneuron_cli.egg-info/requires.txt +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mirrorneuron_cli.egg-info/top_level.txt +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/__init__.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/banner.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/config.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/error_handler.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/__init__.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/artifacts.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/backup_cmds.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/blueprint_cmds.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/blueprint_models.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/blueprint_observability.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/blueprint_repository.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/blueprint_resources.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/bundles.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/deployment_cmds.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/event_relay.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/job_cmds.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/resource_cmds.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/run_cmds.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/run_logs.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/run_manifest.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/runtime_health.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/schedule_cmds.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/service_cmds.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/skill_runtime.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/ui.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/workflow_progress.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/libs/workflow_validation.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/logging_config.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/main.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/runtime_mode.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/runtime_state.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/schemas/workflow_manifest.schema.json +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/sdk_path.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/shared.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/terminal.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/update_cmds.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/pyproject.toml +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/scripts/check-release-artifacts.sh +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/scripts/make-release-zip.sh +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/scripts/validate-version-tag.sh +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/setup.cfg +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/conftest.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_backup_cmds.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_blueprint_cmds.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_blueprint_repository.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_blueprint_resources.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_deployment_cmds.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_docker_network_integration.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_job_cmds.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_main.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_repo_hygiene.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_resource_cmds.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_runtime_health.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_runtime_mode.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_runtime_state.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_schedule_cmds.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_service_cmds.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_shared.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_sys_cmds.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_terminal.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_ui.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_update_cmds.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/tests/test_workflow_validation.py +0 -0
- {mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/uv.lock +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import os
|
|
4
5
|
import subprocess
|
|
5
6
|
from typing import Annotated, Any, Optional
|
|
6
7
|
|
|
@@ -250,16 +251,21 @@ def install_model_entry(
|
|
|
250
251
|
target = docker_model_name(entry)
|
|
251
252
|
if _docker_model_cli_available():
|
|
252
253
|
_ensure_runner(compatibility.backend, compatibility.accelerator)
|
|
253
|
-
|
|
254
|
+
pull_result = _pull_model(target)
|
|
254
255
|
run_command = ["model", "run", "--detach"]
|
|
255
256
|
resolved_context = context_size or entry.get("context_size")
|
|
256
257
|
if resolved_context and _docker_model_run_supports_context_size():
|
|
257
258
|
run_command.extend(["--context-size", str(resolved_context)])
|
|
258
259
|
run_command.append(target)
|
|
259
260
|
_docker(run_command, timeout=300)
|
|
260
|
-
return {
|
|
261
|
+
return {
|
|
262
|
+
"entry": entry,
|
|
263
|
+
"docker_model": target,
|
|
264
|
+
"compatibility": payload,
|
|
265
|
+
**pull_result,
|
|
266
|
+
}
|
|
261
267
|
|
|
262
|
-
api_result = dmr_api_pull_model(target, timeout=
|
|
268
|
+
api_result = dmr_api_pull_model(target, timeout=_model_pull_timeout_seconds())
|
|
263
269
|
return {
|
|
264
270
|
"entry": entry,
|
|
265
271
|
"docker_model": target,
|
|
@@ -269,6 +275,37 @@ def install_model_entry(
|
|
|
269
275
|
}
|
|
270
276
|
|
|
271
277
|
|
|
278
|
+
def _model_pull_timeout_seconds() -> float:
|
|
279
|
+
try:
|
|
280
|
+
return max(float(os.getenv("MN_DOCKER_MODEL_PULL_TIMEOUT_SECONDS", "3600")), 1.0)
|
|
281
|
+
except ValueError:
|
|
282
|
+
return 3600.0
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _pull_model(target: str) -> dict[str, Any]:
|
|
286
|
+
if _endpoint_responds():
|
|
287
|
+
api_result = dmr_api_pull_model(target, timeout=_model_pull_timeout_seconds())
|
|
288
|
+
return {"transport": "docker_model_runner_api", "api": api_result}
|
|
289
|
+
_docker_model_pull(target)
|
|
290
|
+
return {"transport": "docker_cli"}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _docker_model_pull(target: str, *, attempts: int = 2) -> None:
|
|
294
|
+
last_error: RuntimeError | None = None
|
|
295
|
+
for attempt in range(1, attempts + 1):
|
|
296
|
+
try:
|
|
297
|
+
_docker(["model", "pull", target], timeout=900, stream=True)
|
|
298
|
+
return
|
|
299
|
+
except RuntimeError as exc:
|
|
300
|
+
if _model_installed(target):
|
|
301
|
+
return
|
|
302
|
+
last_error = exc
|
|
303
|
+
if attempt < attempts:
|
|
304
|
+
console.print("[yellow]Docker model pull failed; retrying once...[/yellow]")
|
|
305
|
+
if last_error is not None:
|
|
306
|
+
raise last_error
|
|
307
|
+
|
|
308
|
+
|
|
272
309
|
def remove_model_ref(model: str, *, force: bool = False) -> None:
|
|
273
310
|
if _docker_model_cli_available():
|
|
274
311
|
command = ["model", "rm"]
|
|
@@ -79,8 +79,19 @@ def gar_requirement_lines(manifest: dict[str, Any] | None) -> list[str]:
|
|
|
79
79
|
]
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def gar_requirements_file_lines(manifest: dict[str, Any] | None) -> list[str]:
|
|
83
|
+
requirements = pinned_skill_dependency_requirements(manifest)
|
|
84
|
+
if not requirements:
|
|
85
|
+
return []
|
|
86
|
+
return [
|
|
87
|
+
f"--index-url {GAR_PIP_INDEX_URL}",
|
|
88
|
+
f"--extra-index-url {PYPI_PIP_INDEX_URL}",
|
|
89
|
+
*requirements,
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
|
|
82
93
|
def gar_requirements_text(manifest: dict[str, Any] | None) -> str:
|
|
83
|
-
lines =
|
|
94
|
+
lines = gar_requirements_file_lines(manifest)
|
|
84
95
|
return "\n".join(lines).strip() + ("\n" if lines else "")
|
|
85
96
|
|
|
86
97
|
|
|
@@ -310,15 +310,21 @@ def ensure_context_engine(
|
|
|
310
310
|
)
|
|
311
311
|
summary = ensure_context_engine_runtime(force=force)
|
|
312
312
|
progress.update(task, description="[green]Context memory is ready.")
|
|
313
|
+
details = [
|
|
314
|
+
("Service", summary["service"]),
|
|
315
|
+
("Model", summary["model"]),
|
|
316
|
+
("Model status", summary.get("model_status", "unknown")),
|
|
317
|
+
]
|
|
318
|
+
if summary.get("membrane_dir"):
|
|
319
|
+
details.append(("Membrane", summary["membrane_dir"]))
|
|
320
|
+
if summary.get("engine_image"):
|
|
321
|
+
details.append(("Engine image", summary["engine_image"]))
|
|
322
|
+
|
|
313
323
|
print_success_confirmation(
|
|
314
324
|
console,
|
|
315
325
|
"Context engine",
|
|
316
326
|
status=summary["status"],
|
|
317
|
-
details=
|
|
318
|
-
("Service", summary["service"]),
|
|
319
|
-
("Model", summary["model"]),
|
|
320
|
-
("Membrane", summary["membrane_dir"]),
|
|
321
|
-
],
|
|
327
|
+
details=details,
|
|
322
328
|
next_steps="mn runtime health",
|
|
323
329
|
)
|
|
324
330
|
except Exception as exc:
|
|
@@ -9,6 +9,7 @@ import shutil
|
|
|
9
9
|
import socket
|
|
10
10
|
import subprocess
|
|
11
11
|
import sys
|
|
12
|
+
import tempfile
|
|
12
13
|
import time
|
|
13
14
|
import urllib.request
|
|
14
15
|
from datetime import datetime, timezone
|
|
@@ -112,6 +113,11 @@ RUNTIME_MODELS_OVERRIDE_FILE = "docker-compose.models.yml"
|
|
|
112
113
|
DEFAULT_LLM_MODEL_RUNNER_MODEL = "ai/gemma4:E2B"
|
|
113
114
|
DEFAULT_CONTEXT_MODEL_RUNNER_MODEL = "hf.co/homerquan/mn-context-engine-model-v-Q4_K_M"
|
|
114
115
|
DEFAULT_MEMBRANE_REPO = "MirrorNeuronLab/Membrane"
|
|
116
|
+
DEFAULT_MEMBRANE_ENGINE_IMAGE_REPOSITORY = (
|
|
117
|
+
"us-central1-docker.pkg.dev/mirrorneuron-public-packages/"
|
|
118
|
+
"mirrorneuron-runtime/membrane-context-engine"
|
|
119
|
+
)
|
|
120
|
+
PUBLIC_GAR_PROJECT_PATH = "/mirrorneuron-public-packages/"
|
|
115
121
|
CONTEXT_ENGINE_SERVICE = "membrane-context-engine"
|
|
116
122
|
CONTEXT_ENGINE_CONTAINER = "mirror-neuron-context-engine"
|
|
117
123
|
CONTEXT_ENGINE_MODEL_CONTAINER = "mirror-neuron-context-engine-model"
|
|
@@ -2000,36 +2006,68 @@ def ensure_context_engine_runtime(*, force: bool = False) -> dict[str, str]:
|
|
|
2000
2006
|
)
|
|
2001
2007
|
|
|
2002
2008
|
env = _runtime_base_env(True)
|
|
2003
|
-
source_dir = _ensure_context_engine_source(env)
|
|
2004
2009
|
profiles = _compose_profiles_with(env.get("COMPOSE_PROFILES"), "context")
|
|
2005
2010
|
model = str(env.get("MN_CONTEXT_MODEL_RUNNER_MODEL") or DEFAULT_CONTEXT_MODEL_RUNNER_MODEL)
|
|
2011
|
+
engine_image = _context_engine_release_image(env)
|
|
2012
|
+
use_engine_image = _context_engine_image_mode_enabled(env, engine_image)
|
|
2006
2013
|
updates = {
|
|
2007
2014
|
"COMPOSE_PROFILES": profiles,
|
|
2008
|
-
"MEMBRANE_DIR": str(source_dir),
|
|
2009
2015
|
"MN_CONTEXT_MODEL_RUNNER_MODEL": model,
|
|
2010
2016
|
}
|
|
2017
|
+
source_dir: Path | None = None
|
|
2018
|
+
if use_engine_image:
|
|
2019
|
+
updates["ENGINE_IMAGE"] = engine_image
|
|
2020
|
+
updates["MN_MEMBRANE_ENGINE_IMAGE"] = engine_image
|
|
2021
|
+
_remove_env_file_keys(RUNTIME_COMPOSE_ENV, {"MEMBRANE_DIR"})
|
|
2022
|
+
env.pop("MEMBRANE_DIR", None)
|
|
2023
|
+
else:
|
|
2024
|
+
source_dir = _ensure_context_engine_source(env)
|
|
2025
|
+
updates["MEMBRANE_DIR"] = str(source_dir)
|
|
2011
2026
|
_write_env_file_values(RUNTIME_COMPOSE_ENV, updates)
|
|
2012
2027
|
env.update(updates)
|
|
2013
2028
|
|
|
2014
2029
|
_remove_non_mirror_neuron_container(CONTEXT_ENGINE_CONTAINER)
|
|
2015
2030
|
_remove_non_mirror_neuron_container(CONTEXT_ENGINE_MODEL_CONTAINER)
|
|
2016
2031
|
_ensure_docker_model_runner()
|
|
2032
|
+
model_already_installed = _docker_model_inspect_ok(model)
|
|
2033
|
+
model_status = "already_installed" if model_already_installed else "compose_pending"
|
|
2017
2034
|
|
|
2018
2035
|
already_running = _docker_container_running(CONTEXT_ENGINE_CONTAINER)
|
|
2019
|
-
if force or not already_running:
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2036
|
+
if force or not already_running or not model_already_installed:
|
|
2037
|
+
compose_env = env
|
|
2038
|
+
anonymous_docker_config: Path | None = None
|
|
2039
|
+
if use_engine_image:
|
|
2040
|
+
compose_env, anonymous_docker_config = _anonymous_public_gar_docker_env(env, engine_image)
|
|
2041
|
+
compose_process_env = _compose_subprocess_env(compose_env)
|
|
2042
|
+
try:
|
|
2043
|
+
if use_engine_image:
|
|
2044
|
+
subprocess.run(
|
|
2045
|
+
runtime_compose_cmd("pull", CONTEXT_ENGINE_SERVICE),
|
|
2046
|
+
check=True,
|
|
2047
|
+
stdout=subprocess.DEVNULL,
|
|
2048
|
+
env=compose_process_env,
|
|
2049
|
+
)
|
|
2050
|
+
up_args = ("up", "-d", "--no-build", CONTEXT_ENGINE_SERVICE)
|
|
2051
|
+
else:
|
|
2052
|
+
subprocess.run(
|
|
2053
|
+
runtime_compose_cmd("build", CONTEXT_ENGINE_SERVICE),
|
|
2054
|
+
check=True,
|
|
2055
|
+
stdout=subprocess.DEVNULL,
|
|
2056
|
+
env=compose_process_env,
|
|
2057
|
+
)
|
|
2058
|
+
up_args = ("up", "-d", CONTEXT_ENGINE_SERVICE)
|
|
2059
|
+
subprocess.run(
|
|
2060
|
+
runtime_compose_cmd(*up_args),
|
|
2061
|
+
check=True,
|
|
2062
|
+
stdout=subprocess.DEVNULL,
|
|
2063
|
+
env=compose_process_env,
|
|
2064
|
+
)
|
|
2065
|
+
finally:
|
|
2066
|
+
if anonymous_docker_config is not None:
|
|
2067
|
+
shutil.rmtree(anonymous_docker_config, ignore_errors=True)
|
|
2032
2068
|
status = "restarted" if already_running and force else "started"
|
|
2069
|
+
if not model_already_installed:
|
|
2070
|
+
model_status = "installed" if _docker_model_inspect_ok(model) else "compose_managed"
|
|
2033
2071
|
else:
|
|
2034
2072
|
status = "already_running"
|
|
2035
2073
|
|
|
@@ -2038,10 +2076,106 @@ def ensure_context_engine_runtime(*, force: bool = False) -> dict[str, str]:
|
|
|
2038
2076
|
"service": CONTEXT_ENGINE_SERVICE,
|
|
2039
2077
|
"container": CONTEXT_ENGINE_CONTAINER,
|
|
2040
2078
|
"model": model,
|
|
2041
|
-
"
|
|
2079
|
+
"model_status": model_status,
|
|
2042
2080
|
"compose_profiles": profiles,
|
|
2081
|
+
**({"engine_image": engine_image} if use_engine_image else {"membrane_dir": str(source_dir)}),
|
|
2043
2082
|
}
|
|
2044
2083
|
|
|
2084
|
+
def _compose_subprocess_env(env: dict[str, str]) -> dict[str, str]:
|
|
2085
|
+
merged = dict(os.environ)
|
|
2086
|
+
merged.update({str(key): str(value) for key, value in env.items()})
|
|
2087
|
+
return merged
|
|
2088
|
+
|
|
2089
|
+
def _docker_model_inspect_ok(model: str) -> bool:
|
|
2090
|
+
if not model:
|
|
2091
|
+
return False
|
|
2092
|
+
return _docker_command_ok(["docker", "model", "inspect", model])
|
|
2093
|
+
|
|
2094
|
+
def _anonymous_public_gar_docker_env(env: dict[str, str], image: str) -> tuple[dict[str, str], Path | None]:
|
|
2095
|
+
if not _is_public_gar_image(image):
|
|
2096
|
+
return env, None
|
|
2097
|
+
|
|
2098
|
+
registry_host = _docker_image_registry_host(image)
|
|
2099
|
+
config_dir = Path(tempfile.mkdtemp(prefix="mn-public-gar-docker-config-"))
|
|
2100
|
+
source_config = _docker_config_dir(env) / "config.json"
|
|
2101
|
+
config: dict[str, Any] = {}
|
|
2102
|
+
if source_config.exists():
|
|
2103
|
+
try:
|
|
2104
|
+
loaded = json.loads(source_config.read_text(encoding="utf-8"))
|
|
2105
|
+
if isinstance(loaded, dict):
|
|
2106
|
+
config = loaded
|
|
2107
|
+
except Exception:
|
|
2108
|
+
logger.debug("Could not read Docker config for public GAR pull; using anonymous config", exc_info=True)
|
|
2109
|
+
|
|
2110
|
+
config = _docker_config_without_public_gar_credentials(config, registry_host)
|
|
2111
|
+
(config_dir / "config.json").write_text(json.dumps(config, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
2112
|
+
_expose_docker_cli_plugins(source_config.parent, config_dir)
|
|
2113
|
+
_expose_docker_contexts(source_config.parent, config_dir)
|
|
2114
|
+
|
|
2115
|
+
next_env = dict(env)
|
|
2116
|
+
next_env["DOCKER_CONFIG"] = str(config_dir)
|
|
2117
|
+
return next_env, config_dir
|
|
2118
|
+
|
|
2119
|
+
def _expose_docker_cli_plugins(source_config_dir: Path, target_config_dir: Path) -> None:
|
|
2120
|
+
source_plugins = source_config_dir / "cli-plugins"
|
|
2121
|
+
if not source_plugins.is_dir():
|
|
2122
|
+
return
|
|
2123
|
+
target_plugins = target_config_dir / "cli-plugins"
|
|
2124
|
+
try:
|
|
2125
|
+
os.symlink(source_plugins, target_plugins, target_is_directory=True)
|
|
2126
|
+
except Exception:
|
|
2127
|
+
try:
|
|
2128
|
+
shutil.copytree(source_plugins, target_plugins, symlinks=True)
|
|
2129
|
+
except Exception:
|
|
2130
|
+
logger.debug("Could not expose Docker CLI plugins for anonymous GAR pull", exc_info=True)
|
|
2131
|
+
|
|
2132
|
+
def _expose_docker_contexts(source_config_dir: Path, target_config_dir: Path) -> None:
|
|
2133
|
+
source_contexts = source_config_dir / "contexts"
|
|
2134
|
+
if not source_contexts.is_dir():
|
|
2135
|
+
return
|
|
2136
|
+
target_contexts = target_config_dir / "contexts"
|
|
2137
|
+
try:
|
|
2138
|
+
os.symlink(source_contexts, target_contexts, target_is_directory=True)
|
|
2139
|
+
except Exception:
|
|
2140
|
+
try:
|
|
2141
|
+
shutil.copytree(source_contexts, target_contexts, symlinks=True)
|
|
2142
|
+
except Exception:
|
|
2143
|
+
logger.debug("Could not expose Docker contexts for anonymous GAR pull", exc_info=True)
|
|
2144
|
+
|
|
2145
|
+
def _is_public_gar_image(image: str) -> bool:
|
|
2146
|
+
text = str(image or "").strip()
|
|
2147
|
+
return PUBLIC_GAR_PROJECT_PATH in text and _docker_image_registry_host(text).endswith(".pkg.dev")
|
|
2148
|
+
|
|
2149
|
+
def _docker_image_registry_host(image: str) -> str:
|
|
2150
|
+
return str(image or "").split("/", 1)[0].strip().lower()
|
|
2151
|
+
|
|
2152
|
+
def _docker_config_dir(env: dict[str, str]) -> Path:
|
|
2153
|
+
configured = str(env.get("DOCKER_CONFIG") or os.environ.get("DOCKER_CONFIG") or "").strip()
|
|
2154
|
+
return Path(configured).expanduser() if configured else Path.home() / ".docker"
|
|
2155
|
+
|
|
2156
|
+
def _docker_config_without_public_gar_credentials(config: dict[str, Any], registry_host: str) -> dict[str, Any]:
|
|
2157
|
+
sanitized = dict(config)
|
|
2158
|
+
sanitized.pop("credsStore", None)
|
|
2159
|
+
for key in ("credHelpers", "auths"):
|
|
2160
|
+
value = sanitized.get(key)
|
|
2161
|
+
if not isinstance(value, dict):
|
|
2162
|
+
continue
|
|
2163
|
+
filtered = {
|
|
2164
|
+
str(registry): details
|
|
2165
|
+
for registry, details in value.items()
|
|
2166
|
+
if not _docker_registry_matches_public_gar(str(registry), registry_host)
|
|
2167
|
+
}
|
|
2168
|
+
if filtered:
|
|
2169
|
+
sanitized[key] = filtered
|
|
2170
|
+
else:
|
|
2171
|
+
sanitized.pop(key, None)
|
|
2172
|
+
return sanitized
|
|
2173
|
+
|
|
2174
|
+
def _docker_registry_matches_public_gar(registry: str, registry_host: str) -> bool:
|
|
2175
|
+
normalized = registry.strip().lower()
|
|
2176
|
+
normalized = normalized.removeprefix("https://").removeprefix("http://").split("/", 1)[0]
|
|
2177
|
+
return normalized == registry_host or normalized.endswith(".pkg.dev")
|
|
2178
|
+
|
|
2045
2179
|
def _compose_profiles_with(value: object, required_profile: str) -> str:
|
|
2046
2180
|
profiles: list[str] = []
|
|
2047
2181
|
seen: set[str] = set()
|
|
@@ -2058,6 +2192,50 @@ def _compose_profiles_with(value: object, required_profile: str) -> str:
|
|
|
2058
2192
|
profiles.append(required_profile)
|
|
2059
2193
|
return ",".join(profiles)
|
|
2060
2194
|
|
|
2195
|
+
def _normalized_release_image_tag(value: object) -> str:
|
|
2196
|
+
tag = str(value or "").strip()
|
|
2197
|
+
if not tag:
|
|
2198
|
+
return ""
|
|
2199
|
+
return tag if tag.startswith("v") else f"v{tag}"
|
|
2200
|
+
|
|
2201
|
+
def _context_engine_release_image(env: dict[str, str]) -> str:
|
|
2202
|
+
explicit = str(
|
|
2203
|
+
os.getenv("MN_MEMBRANE_ENGINE_IMAGE")
|
|
2204
|
+
or env.get("MN_MEMBRANE_ENGINE_IMAGE")
|
|
2205
|
+
or os.getenv("MN_CONTEXT_ENGINE_IMAGE")
|
|
2206
|
+
or env.get("MN_CONTEXT_ENGINE_IMAGE")
|
|
2207
|
+
or os.getenv("ENGINE_IMAGE")
|
|
2208
|
+
or env.get("ENGINE_IMAGE")
|
|
2209
|
+
or ""
|
|
2210
|
+
).strip()
|
|
2211
|
+
if explicit:
|
|
2212
|
+
return explicit
|
|
2213
|
+
|
|
2214
|
+
tag = _normalized_release_image_tag(
|
|
2215
|
+
os.getenv("MN_MEMBRANE_ENGINE_IMAGE_TAG")
|
|
2216
|
+
or env.get("MN_MEMBRANE_ENGINE_IMAGE_TAG")
|
|
2217
|
+
or os.getenv("MN_RUNTIME_MODULE_VERSION")
|
|
2218
|
+
or env.get("MN_RUNTIME_MODULE_VERSION")
|
|
2219
|
+
or os.getenv("MN_PACKAGE_VERSION")
|
|
2220
|
+
or env.get("MN_PACKAGE_VERSION")
|
|
2221
|
+
)
|
|
2222
|
+
if not tag:
|
|
2223
|
+
return ""
|
|
2224
|
+
repository = str(
|
|
2225
|
+
os.getenv("MN_MEMBRANE_ENGINE_IMAGE_REPOSITORY")
|
|
2226
|
+
or env.get("MN_MEMBRANE_ENGINE_IMAGE_REPOSITORY")
|
|
2227
|
+
or DEFAULT_MEMBRANE_ENGINE_IMAGE_REPOSITORY
|
|
2228
|
+
).strip().rstrip("/")
|
|
2229
|
+
return f"{repository}:{tag}" if repository else ""
|
|
2230
|
+
|
|
2231
|
+
def _context_engine_image_mode_enabled(env: dict[str, str], image: str) -> bool:
|
|
2232
|
+
mode = str(os.getenv("MN_MEMBRANE_SOURCE_MODE") or env.get("MN_MEMBRANE_SOURCE_MODE") or "").strip().lower()
|
|
2233
|
+
if mode in {"source", "git", "checkout", "local"}:
|
|
2234
|
+
return False
|
|
2235
|
+
if mode in {"image", "docker", "gar", "release"}:
|
|
2236
|
+
return bool(image)
|
|
2237
|
+
return bool(image and image != "mirror-neuron-memory-engine:latest")
|
|
2238
|
+
|
|
2061
2239
|
def _context_engine_git_url(env: dict[str, str]) -> str:
|
|
2062
2240
|
explicit = str(os.getenv("MN_MEMBRANE_GIT_URL") or env.get("MN_MEMBRANE_GIT_URL") or "").strip()
|
|
2063
2241
|
if explicit:
|
|
@@ -3658,7 +3836,6 @@ def _start_server(
|
|
|
3658
3836
|
if token:
|
|
3659
3837
|
_write_network_token(network_token)
|
|
3660
3838
|
env = _runtime_base_env(compose_runtime)
|
|
3661
|
-
persisted_join_profile_before_network = bool(compose_runtime and not ip and _persisted_join_profile(env))
|
|
3662
3839
|
mode_override = docker_network_mode or os.getenv("MN_DOCKER_NETWORK_MODE", "").strip()
|
|
3663
3840
|
requested_docker_mode = _docker_network_mode(
|
|
3664
3841
|
mode_override or None,
|
|
@@ -15,6 +15,7 @@ runner = CliRunner()
|
|
|
15
15
|
@pytest.fixture(autouse=True)
|
|
16
16
|
def isolate_model_ownership(monkeypatch, tmp_path):
|
|
17
17
|
monkeypatch.setenv("MN_MODEL_OWNERSHIP_PATH", str(tmp_path / "ownership.json"))
|
|
18
|
+
monkeypatch.setattr("mn_cli.libs.model_cmds._endpoint_responds", lambda: False)
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def _completed(command, returncode=0, stdout="", stderr=""):
|
|
@@ -128,6 +129,38 @@ def test_model_install_streams_pull_progress(mocker):
|
|
|
128
129
|
assert pull_kwargs["capture_output"] is False
|
|
129
130
|
|
|
130
131
|
|
|
132
|
+
def test_model_install_retries_transient_pull_failure(mocker):
|
|
133
|
+
calls = []
|
|
134
|
+
pull_attempts = 0
|
|
135
|
+
|
|
136
|
+
def fake_run(command, **kwargs):
|
|
137
|
+
nonlocal pull_attempts
|
|
138
|
+
calls.append(command)
|
|
139
|
+
if command[:4] == ["docker", "model", "status", "--json"]:
|
|
140
|
+
return _completed(command, stdout=json.dumps({"running": True, "backends": {"llama.cpp": "Running"}}))
|
|
141
|
+
if command[:4] == ["docker", "model", "run", "--help"]:
|
|
142
|
+
return _completed(command, stdout="Options:\n")
|
|
143
|
+
if command == ["docker", "model", "pull", "ai/gemma4:E2B"]:
|
|
144
|
+
pull_attempts += 1
|
|
145
|
+
if pull_attempts == 1:
|
|
146
|
+
return _completed(command, returncode=1, stderr="writing blob: blob digest mismatch")
|
|
147
|
+
if command == ["docker", "model", "inspect", "ai/gemma4:E2B"]:
|
|
148
|
+
return _completed(command, returncode=1)
|
|
149
|
+
return _completed(command)
|
|
150
|
+
|
|
151
|
+
mocker.patch("subprocess.run", side_effect=fake_run)
|
|
152
|
+
mocker.patch(
|
|
153
|
+
"mn_sdk.model_runtime.detect_host_hardware",
|
|
154
|
+
return_value=HostHardwareProfile("darwin", "arm64", total_memory_gb=16, unified_memory_gb=16, has_apple_silicon=True),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
result = runner.invoke(app, ["model", "install", "gemma4:e2b"])
|
|
158
|
+
|
|
159
|
+
assert result.exit_code == 0
|
|
160
|
+
assert calls.count(["docker", "model", "pull", "ai/gemma4:E2B"]) == 2
|
|
161
|
+
assert ["docker", "model", "run", "--detach", "ai/gemma4:E2B"] in calls
|
|
162
|
+
|
|
163
|
+
|
|
131
164
|
def test_model_install_persists_manual_ownership_record(mocker):
|
|
132
165
|
def fake_run(command, **kwargs):
|
|
133
166
|
if command[:4] == ["docker", "model", "status", "--json"]:
|
|
@@ -239,6 +272,40 @@ def test_model_install_falls_back_to_dmr_rest_when_cli_plugin_missing(mocker):
|
|
|
239
272
|
assert {"from": "ai/gemma4:E2B"} in payloads
|
|
240
273
|
|
|
241
274
|
|
|
275
|
+
def test_model_install_prefers_dmr_rest_pull_when_runner_api_reachable(mocker, monkeypatch):
|
|
276
|
+
calls = []
|
|
277
|
+
requests = []
|
|
278
|
+
monkeypatch.setattr("mn_cli.libs.model_cmds._endpoint_responds", lambda: True)
|
|
279
|
+
|
|
280
|
+
def fake_run(command, **kwargs):
|
|
281
|
+
calls.append(command)
|
|
282
|
+
if command[:4] == ["docker", "model", "status", "--json"]:
|
|
283
|
+
return _completed(command, stdout=json.dumps({"running": True, "backends": {"llama.cpp": "Running"}}))
|
|
284
|
+
if command[:4] == ["docker", "model", "run", "--help"]:
|
|
285
|
+
return _completed(command, stdout="Options:\n")
|
|
286
|
+
return _completed(command)
|
|
287
|
+
|
|
288
|
+
def fake_urlopen(request, timeout=0):
|
|
289
|
+
requests.append((request.full_url, request.get_method(), request.data, timeout))
|
|
290
|
+
return FakeResponse("{}")
|
|
291
|
+
|
|
292
|
+
mocker.patch("subprocess.run", side_effect=fake_run)
|
|
293
|
+
mocker.patch("urllib.request.urlopen", side_effect=fake_urlopen)
|
|
294
|
+
mocker.patch(
|
|
295
|
+
"mn_sdk.model_runtime.detect_host_hardware",
|
|
296
|
+
return_value=HostHardwareProfile("darwin", "arm64", total_memory_gb=16, unified_memory_gb=16, has_apple_silicon=True),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
result = runner.invoke(app, ["model", "install", "gemma4:e2b"])
|
|
300
|
+
|
|
301
|
+
assert result.exit_code == 0
|
|
302
|
+
assert ["docker", "model", "pull", "ai/gemma4:E2B"] not in calls
|
|
303
|
+
assert ["docker", "model", "run", "--detach", "ai/gemma4:E2B"] in calls
|
|
304
|
+
assert any(url.endswith("/models/create") and method == "POST" for url, method, _data, _timeout in requests)
|
|
305
|
+
payloads = [json.loads(data.decode("utf-8")) for _url, _method, data, _timeout in requests if data]
|
|
306
|
+
assert {"from": "ai/gemma4:E2B"} in payloads
|
|
307
|
+
|
|
308
|
+
|
|
242
309
|
def test_model_list_reads_dmr_rest_tags_when_cli_plugin_missing(mocker):
|
|
243
310
|
def fake_run(command, **kwargs):
|
|
244
311
|
if command[:3] == ["docker", "model", "--help"] or command[:3] == ["docker", "model", "list"]:
|
|
@@ -293,6 +360,8 @@ def test_model_install_failure_does_not_record_manual_ownership(mocker, tmp_path
|
|
|
293
360
|
return _completed(command, stdout=json.dumps({"running": True, "backends": {"llama.cpp": "Running"}}))
|
|
294
361
|
if command[:3] == ["docker", "model", "pull"]:
|
|
295
362
|
return _completed(command, returncode=1, stderr="pull failed")
|
|
363
|
+
if command[:3] == ["docker", "model", "inspect"]:
|
|
364
|
+
return _completed(command, returncode=1)
|
|
296
365
|
return _completed(command)
|
|
297
366
|
|
|
298
367
|
mocker.patch("subprocess.run", side_effect=fake_run)
|
|
@@ -789,6 +789,27 @@ def test_runtime_ensure_context_engine_explains_first_launch(mocker):
|
|
|
789
789
|
assert "First launch may download the context model" in stdout_text
|
|
790
790
|
assert "Context engine" in result.stdout
|
|
791
791
|
assert "hf.co/example/context-model" in result.stdout
|
|
792
|
+
assert "/tmp/Membrane" in result.stdout
|
|
793
|
+
mock_ensure.assert_called_once_with(force=False)
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
def test_runtime_ensure_context_engine_reports_release_image(mocker):
|
|
797
|
+
mock_ensure = mocker.patch(
|
|
798
|
+
"mn_cli.libs.sys_cmds.ensure_context_engine_runtime",
|
|
799
|
+
return_value={
|
|
800
|
+
"status": "started",
|
|
801
|
+
"service": "membrane-context-engine",
|
|
802
|
+
"model": "hf.co/example/context-model",
|
|
803
|
+
"engine_image": "us-central1-docker.pkg.dev/example/runtime/membrane-context-engine:v1.2.14",
|
|
804
|
+
},
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
result = runner.invoke(app, ["runtime", "ensure-context-engine"])
|
|
808
|
+
|
|
809
|
+
assert result.exit_code == 0
|
|
810
|
+
assert "Context engine" in result.stdout
|
|
811
|
+
assert "hf.co/example/context-model" in result.stdout
|
|
812
|
+
assert "membrane-context-engine:v1.2.14" in result.stdout
|
|
792
813
|
mock_ensure.assert_called_once_with(force=False)
|
|
793
814
|
|
|
794
815
|
|
|
@@ -983,6 +1004,9 @@ def test_openshell_skill_dependency_context_injects_pinned_gar_install(tmp_path)
|
|
|
983
1004
|
assert context != sandbox_dir
|
|
984
1005
|
assert "mirrorneuron-websocket-stream-skill==1.2.7" in requirements
|
|
985
1006
|
assert "https://us-central1-python.pkg.dev/mirrorneuron-public-packages/agent-skills/simple/" in requirements
|
|
1007
|
+
assert "--index-url\n" not in requirements
|
|
1008
|
+
assert "--index-url https://us-central1-python.pkg.dev/mirrorneuron-public-packages/agent-skills/simple/" in requirements
|
|
1009
|
+
assert "--extra-index-url https://pypi.org/simple" in requirements
|
|
986
1010
|
assert "COPY __mn_skill_dependencies/requirements.txt" in dockerfile
|
|
987
1011
|
assert "pip install --break-system-packages --no-cache-dir -r /tmp/mn-skill-dependencies/requirements.txt" in dockerfile
|
|
988
1012
|
|
|
@@ -384,6 +384,9 @@ def test_stage_skill_dependency_payloads_injects_pinned_gar_requirements_for_doc
|
|
|
384
384
|
dockerfile = payloads["worker/docker_worker/Dockerfile"].decode()
|
|
385
385
|
assert "mirrorneuron-rag-skill==1.2.7" in requirements
|
|
386
386
|
assert "https://us-central1-python.pkg.dev/mirrorneuron-public-packages/agent-skills/simple/" in requirements
|
|
387
|
+
assert "--index-url\n" not in requirements
|
|
388
|
+
assert "--index-url https://us-central1-python.pkg.dev/mirrorneuron-public-packages/agent-skills/simple/" in requirements
|
|
389
|
+
assert "--extra-index-url https://pypi.org/simple" in requirements
|
|
387
390
|
assert "COPY __mn_skill_dependencies/requirements.txt" in dockerfile
|
|
388
391
|
assert "pip install --break-system-packages --no-cache-dir -r /tmp/mn-skill-dependencies/requirements.txt" in dockerfile
|
|
389
392
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import pytest
|
|
3
|
+
import shutil
|
|
3
4
|
import subprocess
|
|
4
5
|
import sys
|
|
5
6
|
from io import StringIO
|
|
@@ -324,6 +325,9 @@ def test_deploy_compose_passes_host_shared_storage_to_core():
|
|
|
324
325
|
|
|
325
326
|
assert "MN_HOST_SHARED_STORAGE_ROOT:" in compose_text
|
|
326
327
|
assert "${MN_HOST_SHARED_STORAGE_ROOT:-${MN_SHARED_STORAGE_ROOT:-" in compose_text
|
|
328
|
+
assert "membrane-context-engine:" in compose_text
|
|
329
|
+
assert "context-engine-model:" in compose_text
|
|
330
|
+
assert "MN_CONTEXT_MODEL_ENDPOINT" in compose_text
|
|
327
331
|
|
|
328
332
|
def test_runtime_blueprint_env_updates_ignores_legacy_host_mn_dir(tmp_path):
|
|
329
333
|
legacy_home = tmp_path / "legacy-mn-home"
|
|
@@ -574,6 +578,7 @@ def test_ensure_context_engine_runtime_persists_profile_and_starts_compose(mocke
|
|
|
574
578
|
server_cmds.RUNTIME_COMPOSE_FILE.write_text("services: {}\n", encoding="utf-8")
|
|
575
579
|
mocker.patch("mn_cli.server_cmds._ensure_context_engine_source", return_value=membrane_dir)
|
|
576
580
|
mocker.patch("mn_cli.server_cmds._ensure_docker_model_runner")
|
|
581
|
+
inspect_model = mocker.patch("mn_cli.server_cmds._docker_model_inspect_ok", side_effect=[False, True])
|
|
577
582
|
mocker.patch("mn_cli.server_cmds._remove_non_mirror_neuron_container")
|
|
578
583
|
mocker.patch("mn_cli.server_cmds._docker_container_running", return_value=False)
|
|
579
584
|
run = mocker.patch("mn_cli.server_cmds.subprocess.run")
|
|
@@ -585,9 +590,99 @@ def test_ensure_context_engine_runtime_persists_profile_and_starts_compose(mocke
|
|
|
585
590
|
assert env["MEMBRANE_DIR"] == str(membrane_dir)
|
|
586
591
|
assert env["MN_CONTEXT_MODEL_RUNNER_MODEL"] == server_cmds.DEFAULT_CONTEXT_MODEL_RUNNER_MODEL
|
|
587
592
|
assert result["status"] == "started"
|
|
593
|
+
assert result["model_status"] == "installed"
|
|
594
|
+
assert inspect_model.call_args_list[0].args[0] == server_cmds.DEFAULT_CONTEXT_MODEL_RUNNER_MODEL
|
|
588
595
|
assert run.call_args_list[0].args[0] == runtime_compose_cmd("build", "membrane-context-engine")
|
|
589
596
|
assert run.call_args_list[1].args[0] == runtime_compose_cmd("up", "-d", "membrane-context-engine")
|
|
590
597
|
|
|
598
|
+
def test_ensure_context_engine_runtime_uses_release_image_without_source_clone(mocker, monkeypatch):
|
|
599
|
+
monkeypatch.setenv("PATH", "/usr/local/bin:/usr/bin:/bin")
|
|
600
|
+
server_cmds.RUNTIME_COMPOSE_ENV.parent.mkdir(parents=True, exist_ok=True)
|
|
601
|
+
server_cmds.RUNTIME_COMPOSE_ENV.write_text(
|
|
602
|
+
"COMPOSE_PROJECT_NAME=mirror-neuron\n"
|
|
603
|
+
"COMPOSE_PROFILES=openshell\n"
|
|
604
|
+
"MN_RUNTIME_MODULE_VERSION=1.2.7\n"
|
|
605
|
+
"MEMBRANE_DIR=/private/membrane\n",
|
|
606
|
+
encoding="utf-8",
|
|
607
|
+
)
|
|
608
|
+
server_cmds.RUNTIME_COMPOSE_FILE.write_text("services: {}\n", encoding="utf-8")
|
|
609
|
+
ensure_source = mocker.patch("mn_cli.server_cmds._ensure_context_engine_source")
|
|
610
|
+
mocker.patch("mn_cli.server_cmds._ensure_docker_model_runner")
|
|
611
|
+
inspect_model = mocker.patch("mn_cli.server_cmds._docker_model_inspect_ok", return_value=True)
|
|
612
|
+
mocker.patch("mn_cli.server_cmds._remove_non_mirror_neuron_container")
|
|
613
|
+
mocker.patch("mn_cli.server_cmds._docker_container_running", return_value=False)
|
|
614
|
+
run = mocker.patch("mn_cli.server_cmds.subprocess.run")
|
|
615
|
+
|
|
616
|
+
result = server_cmds.ensure_context_engine_runtime()
|
|
617
|
+
|
|
618
|
+
env = server_cmds._read_env_file(server_cmds.RUNTIME_COMPOSE_ENV)
|
|
619
|
+
expected_image = (
|
|
620
|
+
"us-central1-docker.pkg.dev/mirrorneuron-public-packages/"
|
|
621
|
+
"mirrorneuron-runtime/membrane-context-engine:v1.2.7"
|
|
622
|
+
)
|
|
623
|
+
assert env["COMPOSE_PROFILES"] == "openshell,context"
|
|
624
|
+
assert env["ENGINE_IMAGE"] == expected_image
|
|
625
|
+
assert env["MN_MEMBRANE_ENGINE_IMAGE"] == expected_image
|
|
626
|
+
assert "MEMBRANE_DIR" not in env
|
|
627
|
+
assert result["status"] == "started"
|
|
628
|
+
assert result["model_status"] == "already_installed"
|
|
629
|
+
assert result["engine_image"] == expected_image
|
|
630
|
+
ensure_source.assert_not_called()
|
|
631
|
+
inspect_model.assert_called_once_with(server_cmds.DEFAULT_CONTEXT_MODEL_RUNNER_MODEL)
|
|
632
|
+
assert run.call_args_list[0].args[0] == runtime_compose_cmd("pull", "membrane-context-engine")
|
|
633
|
+
assert run.call_args_list[1].args[0] == runtime_compose_cmd("up", "-d", "--no-build", "membrane-context-engine")
|
|
634
|
+
assert run.call_args_list[0].kwargs["env"]["DOCKER_CONFIG"] != str(Path.home() / ".docker")
|
|
635
|
+
assert run.call_args_list[0].kwargs["env"]["PATH"] == "/usr/local/bin:/usr/bin:/bin"
|
|
636
|
+
|
|
637
|
+
def test_public_gar_docker_env_strips_gcloud_helpers(monkeypatch, tmp_path):
|
|
638
|
+
docker_config = tmp_path / "docker-config"
|
|
639
|
+
docker_config.mkdir()
|
|
640
|
+
docker_plugins = docker_config / "cli-plugins"
|
|
641
|
+
docker_plugins.mkdir()
|
|
642
|
+
(docker_plugins / "docker-compose").write_text("#!/bin/sh\n", encoding="utf-8")
|
|
643
|
+
docker_context = docker_config / "contexts" / "meta" / "desktop-linux"
|
|
644
|
+
docker_context.mkdir(parents=True)
|
|
645
|
+
(docker_context / "meta.json").write_text('{"Name":"desktop-linux"}\n', encoding="utf-8")
|
|
646
|
+
(docker_config / "config.json").write_text(
|
|
647
|
+
json.dumps(
|
|
648
|
+
{
|
|
649
|
+
"currentContext": "desktop-linux",
|
|
650
|
+
"auths": {
|
|
651
|
+
"https://us-central1-docker.pkg.dev": {"auth": "private"},
|
|
652
|
+
"ghcr.io": {"auth": "keep"},
|
|
653
|
+
},
|
|
654
|
+
"credHelpers": {
|
|
655
|
+
"us-central1-docker.pkg.dev": "gcloud",
|
|
656
|
+
"ghcr.io": "ghcr",
|
|
657
|
+
},
|
|
658
|
+
"credsStore": "desktop",
|
|
659
|
+
"proxies": {"default": {"httpProxy": "http://proxy.test"}},
|
|
660
|
+
}
|
|
661
|
+
),
|
|
662
|
+
encoding="utf-8",
|
|
663
|
+
)
|
|
664
|
+
env = {"DOCKER_CONFIG": str(docker_config)}
|
|
665
|
+
|
|
666
|
+
next_env, temp_config_dir = server_cmds._anonymous_public_gar_docker_env(
|
|
667
|
+
env,
|
|
668
|
+
"us-central1-docker.pkg.dev/mirrorneuron-public-packages/mirrorneuron-runtime/membrane-context-engine:v1.2.8",
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
try:
|
|
672
|
+
assert temp_config_dir is not None
|
|
673
|
+
assert next_env["DOCKER_CONFIG"] == str(temp_config_dir)
|
|
674
|
+
assert next_env["DOCKER_CONFIG"] != env["DOCKER_CONFIG"]
|
|
675
|
+
sanitized = json.loads((temp_config_dir / "config.json").read_text(encoding="utf-8"))
|
|
676
|
+
assert "credsStore" not in sanitized
|
|
677
|
+
assert sanitized["credHelpers"] == {"ghcr.io": "ghcr"}
|
|
678
|
+
assert sanitized["auths"] == {"ghcr.io": {"auth": "keep"}}
|
|
679
|
+
assert sanitized["proxies"] == {"default": {"httpProxy": "http://proxy.test"}}
|
|
680
|
+
assert sanitized["currentContext"] == "desktop-linux"
|
|
681
|
+
assert (temp_config_dir / "cli-plugins" / "docker-compose").exists()
|
|
682
|
+
assert (temp_config_dir / "contexts" / "meta" / "desktop-linux" / "meta.json").exists()
|
|
683
|
+
finally:
|
|
684
|
+
shutil.rmtree(temp_config_dir, ignore_errors=True)
|
|
685
|
+
|
|
591
686
|
def test_ensure_context_engine_runtime_skips_compose_when_already_running(mocker, tmp_path):
|
|
592
687
|
membrane_dir = tmp_path / "Membrane"
|
|
593
688
|
membrane_dir.mkdir()
|
|
@@ -602,6 +697,7 @@ def test_ensure_context_engine_runtime_skips_compose_when_already_running(mocker
|
|
|
602
697
|
server_cmds.RUNTIME_COMPOSE_FILE.write_text("services: {}\n", encoding="utf-8")
|
|
603
698
|
mocker.patch("mn_cli.server_cmds._ensure_context_engine_source", return_value=membrane_dir)
|
|
604
699
|
mocker.patch("mn_cli.server_cmds._ensure_docker_model_runner")
|
|
700
|
+
inspect_model = mocker.patch("mn_cli.server_cmds._docker_model_inspect_ok", return_value=True)
|
|
605
701
|
mocker.patch("mn_cli.server_cmds._remove_non_mirror_neuron_container")
|
|
606
702
|
mocker.patch("mn_cli.server_cmds._docker_container_running", return_value=True)
|
|
607
703
|
run = mocker.patch("mn_cli.server_cmds.subprocess.run")
|
|
@@ -610,8 +706,36 @@ def test_ensure_context_engine_runtime_skips_compose_when_already_running(mocker
|
|
|
610
706
|
|
|
611
707
|
assert result["status"] == "already_running"
|
|
612
708
|
assert result["model"] == "hf.co/acme/context"
|
|
709
|
+
assert result["model_status"] == "already_installed"
|
|
710
|
+
inspect_model.assert_called_once_with("hf.co/acme/context")
|
|
613
711
|
run.assert_not_called()
|
|
614
712
|
|
|
713
|
+
def test_ensure_context_engine_runtime_reconciles_missing_model_with_compose(mocker, tmp_path):
|
|
714
|
+
membrane_dir = tmp_path / "Membrane"
|
|
715
|
+
membrane_dir.mkdir()
|
|
716
|
+
(membrane_dir / "Dockerfile").write_text("FROM scratch\n", encoding="utf-8")
|
|
717
|
+
server_cmds.RUNTIME_COMPOSE_ENV.parent.mkdir(parents=True, exist_ok=True)
|
|
718
|
+
server_cmds.RUNTIME_COMPOSE_ENV.write_text(
|
|
719
|
+
"COMPOSE_PROJECT_NAME=mirror-neuron\n"
|
|
720
|
+
"COMPOSE_PROFILES=context\n"
|
|
721
|
+
"MN_CONTEXT_MODEL_RUNNER_MODEL=hf.co/acme/context\n",
|
|
722
|
+
encoding="utf-8",
|
|
723
|
+
)
|
|
724
|
+
server_cmds.RUNTIME_COMPOSE_FILE.write_text("services: {}\n", encoding="utf-8")
|
|
725
|
+
mocker.patch("mn_cli.server_cmds._ensure_context_engine_source", return_value=membrane_dir)
|
|
726
|
+
mocker.patch("mn_cli.server_cmds._ensure_docker_model_runner")
|
|
727
|
+
mocker.patch("mn_cli.server_cmds._docker_model_inspect_ok", side_effect=[False, True])
|
|
728
|
+
mocker.patch("mn_cli.server_cmds._remove_non_mirror_neuron_container")
|
|
729
|
+
mocker.patch("mn_cli.server_cmds._docker_container_running", return_value=True)
|
|
730
|
+
run = mocker.patch("mn_cli.server_cmds.subprocess.run")
|
|
731
|
+
|
|
732
|
+
result = server_cmds.ensure_context_engine_runtime()
|
|
733
|
+
|
|
734
|
+
assert result["status"] == "started"
|
|
735
|
+
assert result["model_status"] == "installed"
|
|
736
|
+
assert run.call_args_list[0].args[0] == runtime_compose_cmd("build", "membrane-context-engine")
|
|
737
|
+
assert run.call_args_list[1].args[0] == runtime_compose_cmd("up", "-d", "membrane-context-engine")
|
|
738
|
+
|
|
615
739
|
def test_runtime_compose_cmd_includes_models_override():
|
|
616
740
|
server_cmds.RUNTIME_COMPOSE_ENV.parent.mkdir(parents=True, exist_ok=True)
|
|
617
741
|
server_cmds.RUNTIME_COMPOSE_ENV.write_text("COMPOSE_PROJECT_NAME=mirror-neuron\n", encoding="utf-8")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mirrorneuron_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mirrorneuron_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mirrorneuron_cli-1.2.7 → mirrorneuron_cli-1.2.15}/mn_cli/schemas/workflow_manifest.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|