mirrorneuron-cli 1.2.8__tar.gz → 1.2.18__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 (89) hide show
  1. {mirrorneuron_cli-1.2.8/mirrorneuron_cli.egg-info → mirrorneuron_cli-1.2.18}/PKG-INFO +1 -1
  2. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18/mirrorneuron_cli.egg-info}/PKG-INFO +1 -1
  3. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mirrorneuron_cli.egg-info/SOURCES.txt +2 -0
  4. mirrorneuron_cli-1.2.18/mirrorneuron_cli.egg-info/scm_file_list.json +83 -0
  5. mirrorneuron_cli-1.2.18/mirrorneuron_cli.egg-info/scm_version.json +8 -0
  6. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/job_cmds.py +3 -9
  7. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/model_cmds.py +24 -3
  8. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/run_cmds.py +145 -1
  9. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/skill_dependencies.py +12 -1
  10. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/sys_cmds.py +11 -5
  11. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/server_cmds.py +205 -120
  12. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_job_cmds.py +4 -4
  13. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_model_cmds.py +35 -0
  14. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_run_cmds.py +117 -0
  15. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_run_helpers.py +3 -0
  16. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_server_cmds.py +207 -66
  17. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_shared.py +12 -12
  18. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/.github/workflows/ci.yml +0 -0
  19. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/.github/workflows/release.yml +0 -0
  20. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/.gitignore +0 -0
  21. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/.python-version +0 -0
  22. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/AGENTS.md +0 -0
  23. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/LICENSE +0 -0
  24. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/README.md +0 -0
  25. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/RELEASE.md +0 -0
  26. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mirrorneuron_cli.egg-info/dependency_links.txt +0 -0
  27. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mirrorneuron_cli.egg-info/entry_points.txt +0 -0
  28. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mirrorneuron_cli.egg-info/requires.txt +0 -0
  29. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mirrorneuron_cli.egg-info/top_level.txt +0 -0
  30. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/__init__.py +0 -0
  31. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/banner.py +0 -0
  32. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/config.py +0 -0
  33. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/error_handler.py +0 -0
  34. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/__init__.py +0 -0
  35. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/artifacts.py +0 -0
  36. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/backup_cmds.py +0 -0
  37. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/blueprint_cmds.py +0 -0
  38. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/blueprint_models.py +0 -0
  39. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/blueprint_observability.py +0 -0
  40. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/blueprint_repository.py +0 -0
  41. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/blueprint_resources.py +0 -0
  42. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/bundles.py +0 -0
  43. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/deployment_cmds.py +0 -0
  44. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/event_relay.py +0 -0
  45. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/resource_cmds.py +0 -0
  46. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/run_logs.py +0 -0
  47. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/run_manifest.py +0 -0
  48. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/runtime_health.py +0 -0
  49. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/schedule_cmds.py +0 -0
  50. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/service_cmds.py +0 -0
  51. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/skill_runtime.py +0 -0
  52. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/ui.py +0 -0
  53. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/workflow_progress.py +0 -0
  54. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/libs/workflow_validation.py +0 -0
  55. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/logging_config.py +0 -0
  56. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/main.py +0 -0
  57. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/runtime_mode.py +0 -0
  58. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/runtime_state.py +0 -0
  59. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/schemas/workflow_manifest.schema.json +0 -0
  60. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/sdk_path.py +0 -0
  61. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/shared.py +0 -0
  62. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/terminal.py +0 -0
  63. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/mn_cli/update_cmds.py +0 -0
  64. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/pyproject.toml +0 -0
  65. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/scripts/check-release-artifacts.sh +0 -0
  66. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/scripts/make-release-zip.sh +0 -0
  67. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/scripts/validate-version-tag.sh +0 -0
  68. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/setup.cfg +0 -0
  69. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/conftest.py +0 -0
  70. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_backup_cmds.py +0 -0
  71. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_blueprint_cmds.py +0 -0
  72. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_blueprint_repository.py +0 -0
  73. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_blueprint_resources.py +0 -0
  74. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_deployment_cmds.py +0 -0
  75. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_docker_network_integration.py +0 -0
  76. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_main.py +0 -0
  77. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_repo_hygiene.py +0 -0
  78. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_resource_cmds.py +0 -0
  79. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_runtime_health.py +0 -0
  80. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_runtime_mode.py +0 -0
  81. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_runtime_state.py +0 -0
  82. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_schedule_cmds.py +0 -0
  83. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_service_cmds.py +0 -0
  84. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_sys_cmds.py +0 -0
  85. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_terminal.py +0 -0
  86. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_ui.py +0 -0
  87. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_update_cmds.py +0 -0
  88. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/tests/test_workflow_validation.py +0 -0
  89. {mirrorneuron_cli-1.2.8 → mirrorneuron_cli-1.2.18}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mirrorneuron-cli
3
- Version: 1.2.8
3
+ Version: 1.2.18
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.8
3
+ Version: 1.2.18
4
4
  Summary: MirrorNeuron CLI
5
5
  License-Expression: MIT
6
6
  Classifier: Programming Language :: Python :: 3
@@ -13,6 +13,8 @@ mirrorneuron_cli.egg-info/SOURCES.txt
13
13
  mirrorneuron_cli.egg-info/dependency_links.txt
14
14
  mirrorneuron_cli.egg-info/entry_points.txt
15
15
  mirrorneuron_cli.egg-info/requires.txt
16
+ mirrorneuron_cli.egg-info/scm_file_list.json
17
+ mirrorneuron_cli.egg-info/scm_version.json
16
18
  mirrorneuron_cli.egg-info/top_level.txt
17
19
  mn_cli/__init__.py
18
20
  mn_cli/banner.py
@@ -0,0 +1,83 @@
1
+ {
2
+ "files": [
3
+ "README.md",
4
+ "uv.lock",
5
+ "RELEASE.md",
6
+ ".python-version",
7
+ "LICENSE",
8
+ "pyproject.toml",
9
+ "AGENTS.md",
10
+ ".gitignore",
11
+ "scripts/check-release-artifacts.sh",
12
+ "scripts/make-release-zip.sh",
13
+ "scripts/validate-version-tag.sh",
14
+ "mn_cli/runtime_state.py",
15
+ "mn_cli/__init__.py",
16
+ "mn_cli/error_handler.py",
17
+ "mn_cli/config.py",
18
+ "mn_cli/sdk_path.py",
19
+ "mn_cli/logging_config.py",
20
+ "mn_cli/banner.py",
21
+ "mn_cli/main.py",
22
+ "mn_cli/runtime_mode.py",
23
+ "mn_cli/server_cmds.py",
24
+ "mn_cli/terminal.py",
25
+ "mn_cli/shared.py",
26
+ "mn_cli/update_cmds.py",
27
+ "mn_cli/libs/backup_cmds.py",
28
+ "mn_cli/libs/model_cmds.py",
29
+ "mn_cli/libs/run_manifest.py",
30
+ "mn_cli/libs/__init__.py",
31
+ "mn_cli/libs/artifacts.py",
32
+ "mn_cli/libs/blueprint_cmds.py",
33
+ "mn_cli/libs/workflow_progress.py",
34
+ "mn_cli/libs/blueprint_models.py",
35
+ "mn_cli/libs/blueprint_observability.py",
36
+ "mn_cli/libs/deployment_cmds.py",
37
+ "mn_cli/libs/run_cmds.py",
38
+ "mn_cli/libs/skill_runtime.py",
39
+ "mn_cli/libs/event_relay.py",
40
+ "mn_cli/libs/resource_cmds.py",
41
+ "mn_cli/libs/skill_dependencies.py",
42
+ "mn_cli/libs/service_cmds.py",
43
+ "mn_cli/libs/run_logs.py",
44
+ "mn_cli/libs/runtime_health.py",
45
+ "mn_cli/libs/bundles.py",
46
+ "mn_cli/libs/blueprint_resources.py",
47
+ "mn_cli/libs/blueprint_repository.py",
48
+ "mn_cli/libs/schedule_cmds.py",
49
+ "mn_cli/libs/sys_cmds.py",
50
+ "mn_cli/libs/ui.py",
51
+ "mn_cli/libs/workflow_validation.py",
52
+ "mn_cli/libs/job_cmds.py",
53
+ "mn_cli/schemas/workflow_manifest.schema.json",
54
+ "tests/test_shared.py",
55
+ "tests/test_model_cmds.py",
56
+ "tests/test_deployment_cmds.py",
57
+ "tests/test_docker_network_integration.py",
58
+ "tests/test_service_cmds.py",
59
+ "tests/test_blueprint_resources.py",
60
+ "tests/test_backup_cmds.py",
61
+ "tests/test_server_cmds.py",
62
+ "tests/test_run_cmds.py",
63
+ "tests/test_blueprint_repository.py",
64
+ "tests/test_blueprint_cmds.py",
65
+ "tests/test_workflow_validation.py",
66
+ "tests/test_runtime_mode.py",
67
+ "tests/test_update_cmds.py",
68
+ "tests/test_schedule_cmds.py",
69
+ "tests/test_run_helpers.py",
70
+ "tests/test_sys_cmds.py",
71
+ "tests/conftest.py",
72
+ "tests/test_terminal.py",
73
+ "tests/test_job_cmds.py",
74
+ "tests/test_ui.py",
75
+ "tests/test_resource_cmds.py",
76
+ "tests/test_main.py",
77
+ "tests/test_runtime_health.py",
78
+ "tests/test_runtime_state.py",
79
+ "tests/test_repo_hygiene.py",
80
+ ".github/workflows/release.yml",
81
+ ".github/workflows/ci.yml"
82
+ ]
83
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.2.18",
3
+ "distance": 0,
4
+ "node": "g2f11bbb43d316e874c05b7e3fb0e28480742005c",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-06-22"
8
+ }
@@ -98,11 +98,6 @@ def list_jobs(running_only: bool = typer.Option(False, "--running-only", help="O
98
98
  def clear():
99
99
  """Remove all job records except running ones"""
100
100
  try:
101
- admin_token = str(getattr(client, "admin_token", "") or config.grpc_admin_token or "").strip()
102
- if not admin_token:
103
- console.print("[red]Error: No local gRPC admin token was found for job clear.[/red]")
104
- console.print("Start or recreate the runtime so the shared grpc_admin.token file is generated.")
105
- return
106
101
  cleared_count = client.clear_jobs()
107
102
  logger.info("Cleared %d non-running jobs", cleared_count)
108
103
  print_success_confirmation(
@@ -115,11 +110,10 @@ def clear():
115
110
  if e.code() == grpc.StatusCode.PERMISSION_DENIED and "MN_GRPC_ADMIN_TOKEN" in str(e.details()):
116
111
  console.print("[red]Error: ClearJobs admin authorization failed.[/red]")
117
112
  console.print(
118
- "The local admin token exists, but the running core rejected it. "
119
- "This usually means the CLI/API is pointed at a runtime with different cluster tokens, "
120
- "or the running core has not been recreated with the shared token-file mount."
113
+ "The running core rejected the fixed gRPC admin token. "
114
+ "Recreate the runtime with the current fixed-token build."
121
115
  )
122
- console.print("Recreate the runtime once, then retry: mn runtime stop; mn runtime start")
116
+ console.print("Retry after: mn runtime stop; mn runtime start")
123
117
  return
124
118
  handle_cli_error(e, console, 'clear')
125
119
  except Exception as e:
@@ -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
- _docker_model_pull(target)
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 {"entry": entry, "docker_model": target, "compatibility": payload, "transport": "docker_cli"}
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=900)
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,21 @@ 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
+
272
293
  def _docker_model_pull(target: str, *, attempts: int = 2) -> None:
273
294
  last_error: RuntimeError | None = None
274
295
  for attempt in range(1, attempts + 1):
@@ -61,14 +61,23 @@ from mn_cli.libs.workflow_validation import (
61
61
  from mn_cli.libs.blueprint_observability import (
62
62
  make_blueprint_run_id as _make_blueprint_run_id,
63
63
  )
64
+ from mn_cli.libs.blueprint_models import BlueprintModelOps, blueprint_model_dependency_summary
65
+ from mn_cli.libs.model_cmds import install_model_entry, model_installed
64
66
  from mn_cli.libs.blueprint_resources import cleanup_blueprint_host_hooks
65
67
  from mn_cli.server_cmds import ensure_context_engine_runtime
66
68
  from mn_cli.shared import console, client, logger
67
69
  from mn_cli.terminal import use_progress
68
70
  from mn_cli.error_handler import handle_cli_error
69
71
  from mn_sdk import (
72
+ cluster_provided_model,
73
+ docker_model_name,
74
+ load_model_catalog,
75
+ load_model_ownership,
70
76
  make_validation_report,
71
77
  prepare_job_submission,
78
+ record_model_owner,
79
+ required_blueprint_models,
80
+ resolve_model_entry,
72
81
  run_hardware_requirements_validation,
73
82
  run_input_validation,
74
83
  run_model_validation,
@@ -234,6 +243,119 @@ def _manifest_config(manifest: dict[str, Any]) -> dict[str, Any]:
234
243
  return {}
235
244
 
236
245
 
246
+ def _prepare_runtime_models_for_run_or_exit(
247
+ bundle_dir: Path,
248
+ manifest: dict[str, Any],
249
+ *,
250
+ env_overrides: Optional[dict[str, str]] = None,
251
+ config_overrides: Optional[dict[str, Any]] = None,
252
+ force: bool = False,
253
+ ) -> dict[str, Any]:
254
+ config = load_blueprint_config(bundle_dir, config_overrides=config_overrides) or {}
255
+ validation_manifest = _manifest_for_model_validation(manifest, config)
256
+ summary = blueprint_model_dependency_summary(
257
+ blueprint_id=_runtime_model_blueprint_id(bundle_dir, manifest, config),
258
+ blueprint_revision=_runtime_model_blueprint_revision(manifest, config),
259
+ bundle_root=bundle_dir,
260
+ manifest=validation_manifest,
261
+ config=config,
262
+ install_source=str(bundle_dir),
263
+ force=force,
264
+ ops=BlueprintModelOps(
265
+ load_model_catalog=load_model_catalog,
266
+ required_blueprint_models=required_blueprint_models,
267
+ load_model_ownership=load_model_ownership,
268
+ resolve_model_entry=resolve_model_entry,
269
+ docker_model_name=docker_model_name,
270
+ cluster_provided_model=cluster_provided_model,
271
+ record_model_owner=record_model_owner,
272
+ model_installed=model_installed,
273
+ install_model_entry=install_model_entry,
274
+ notify_model_install_start=_print_runtime_model_install_start,
275
+ ),
276
+ )
277
+ _print_runtime_model_install_summary(summary)
278
+ if summary["errors"]:
279
+ raise typer.Exit(1)
280
+ return summary
281
+
282
+
283
+ def _runtime_model_blueprint_id(
284
+ bundle_dir: Path,
285
+ manifest: dict[str, Any],
286
+ config: dict[str, Any],
287
+ ) -> str:
288
+ metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
289
+ identity = config.get("identity") if isinstance(config.get("identity"), dict) else {}
290
+ for value in (
291
+ metadata.get("blueprint_id"),
292
+ metadata.get("blueprintId"),
293
+ identity.get("blueprint_id"),
294
+ identity.get("blueprintId"),
295
+ manifest.get("id"),
296
+ manifest.get("graph_id"),
297
+ manifest.get("job_name"),
298
+ ):
299
+ text = str(value or "").strip()
300
+ if text:
301
+ return text
302
+ return bundle_dir.name
303
+
304
+
305
+ def _runtime_model_blueprint_revision(
306
+ manifest: dict[str, Any],
307
+ config: dict[str, Any],
308
+ ) -> str | None:
309
+ metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
310
+ identity = config.get("identity") if isinstance(config.get("identity"), dict) else {}
311
+ for value in (
312
+ metadata.get("blueprint_revision"),
313
+ metadata.get("blueprintRevision"),
314
+ identity.get("blueprint_revision"),
315
+ identity.get("blueprintRevision"),
316
+ manifest.get("revision"),
317
+ manifest.get("version"),
318
+ ):
319
+ text = str(value or "").strip()
320
+ if text:
321
+ return text
322
+ return None
323
+
324
+
325
+ def _print_runtime_model_install_start(model: dict[str, Any]) -> None:
326
+ label = str(model.get("id") or model.get("model") or "runtime model")
327
+ docker_model = str(model.get("model") or "")
328
+ backend = str(model.get("backend") or "auto")
329
+ detail = f"{label} ({docker_model})" if docker_model and docker_model != label else label
330
+ console.print(
331
+ f"[yellow]Runtime model {detail} is not installed. "
332
+ f"Installing with backend {backend}; this may take a few minutes the first time.[/yellow]"
333
+ )
334
+
335
+
336
+ def _print_runtime_model_install_summary(summary: dict[str, Any]) -> None:
337
+ models = summary.get("models") or []
338
+ if not models:
339
+ return
340
+
341
+ prepared = [
342
+ item
343
+ for item in models
344
+ if str(item.get("status") or "")
345
+ in {"installed", "already_installed", "service_required", "cluster_provided"}
346
+ ]
347
+ if prepared:
348
+ labels = ", ".join(
349
+ str(item.get("id") or item.get("model") or "runtime model")
350
+ for item in prepared[:4]
351
+ )
352
+ if len(prepared) > 4:
353
+ labels = f"{labels}, +{len(prepared) - 4} more"
354
+ console.print(f"[green]Runtime models ready:[/green] {labels}")
355
+ for error in summary.get("errors") or []:
356
+ console.print(f"[red]Runtime model install failed: {error}[/red]")
357
+
358
+
237
359
  def _ensure_context_engine_for_run_if_needed(
238
360
  bundle_dir: Path,
239
361
  manifest: dict[str, Any],
@@ -1609,6 +1731,17 @@ def run_bundle(
1609
1731
  env_overrides=env_overrides,
1610
1732
  config_overrides=config_overrides,
1611
1733
  )
1734
+ _print_launch_progress(
1735
+ "Prepare runtime models",
1736
+ "installing any missing Docker Model Runner models required by this blueprint.",
1737
+ )
1738
+ _prepare_runtime_models_for_run_or_exit(
1739
+ bundle_dir,
1740
+ manifest_dict,
1741
+ env_overrides=env_overrides,
1742
+ config_overrides=config_overrides,
1743
+ force=force,
1744
+ )
1612
1745
  _validate_manifest_models_or_exit(
1613
1746
  bundle_dir,
1614
1747
  manifest_dict,
@@ -1623,7 +1756,18 @@ def run_bundle(
1623
1756
  )
1624
1757
  else:
1625
1758
  console.print(
1626
- "[yellow]Validation skipped because --force was provided; service checks, model checks, input checks, and non-hard runtime requirements will be bypassed for this run.[/yellow]"
1759
+ "[yellow]Validation skipped because --force was provided; service checks, model checks, input checks, and non-hard runtime requirements will be bypassed, but required runtime models will still be prepared.[/yellow]"
1760
+ )
1761
+ _print_launch_progress(
1762
+ "Prepare runtime models",
1763
+ "installing any missing Docker Model Runner models required by this blueprint.",
1764
+ )
1765
+ _prepare_runtime_models_for_run_or_exit(
1766
+ bundle_dir,
1767
+ manifest_dict,
1768
+ env_overrides=env_overrides,
1769
+ config_overrides=config_overrides,
1770
+ force=force,
1627
1771
  )
1628
1772
  _print_launch_progress(
1629
1773
  "Package workflow",
@@ -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 = gar_requirement_lines(manifest)
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: