mirrorneuron-cli 1.2.15__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.15/mirrorneuron_cli.egg-info → mirrorneuron_cli-1.2.18}/PKG-INFO +1 -1
  2. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18/mirrorneuron_cli.egg-info}/PKG-INFO +1 -1
  3. {mirrorneuron_cli-1.2.15 → 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.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/job_cmds.py +3 -9
  7. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/run_cmds.py +145 -1
  8. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/server_cmds.py +82 -107
  9. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_job_cmds.py +4 -4
  10. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_run_cmds.py +93 -0
  11. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_server_cmds.py +123 -73
  12. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_shared.py +12 -12
  13. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/.github/workflows/ci.yml +0 -0
  14. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/.github/workflows/release.yml +0 -0
  15. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/.gitignore +0 -0
  16. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/.python-version +0 -0
  17. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/AGENTS.md +0 -0
  18. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/LICENSE +0 -0
  19. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/README.md +0 -0
  20. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/RELEASE.md +0 -0
  21. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mirrorneuron_cli.egg-info/dependency_links.txt +0 -0
  22. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mirrorneuron_cli.egg-info/entry_points.txt +0 -0
  23. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mirrorneuron_cli.egg-info/requires.txt +0 -0
  24. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mirrorneuron_cli.egg-info/top_level.txt +0 -0
  25. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/__init__.py +0 -0
  26. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/banner.py +0 -0
  27. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/config.py +0 -0
  28. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/error_handler.py +0 -0
  29. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/__init__.py +0 -0
  30. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/artifacts.py +0 -0
  31. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/backup_cmds.py +0 -0
  32. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/blueprint_cmds.py +0 -0
  33. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/blueprint_models.py +0 -0
  34. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/blueprint_observability.py +0 -0
  35. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/blueprint_repository.py +0 -0
  36. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/blueprint_resources.py +0 -0
  37. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/bundles.py +0 -0
  38. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/deployment_cmds.py +0 -0
  39. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/event_relay.py +0 -0
  40. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/model_cmds.py +0 -0
  41. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/resource_cmds.py +0 -0
  42. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/run_logs.py +0 -0
  43. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/run_manifest.py +0 -0
  44. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/runtime_health.py +0 -0
  45. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/schedule_cmds.py +0 -0
  46. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/service_cmds.py +0 -0
  47. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/skill_dependencies.py +0 -0
  48. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/skill_runtime.py +0 -0
  49. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/sys_cmds.py +0 -0
  50. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/ui.py +0 -0
  51. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/workflow_progress.py +0 -0
  52. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/libs/workflow_validation.py +0 -0
  53. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/logging_config.py +0 -0
  54. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/main.py +0 -0
  55. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/runtime_mode.py +0 -0
  56. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/runtime_state.py +0 -0
  57. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/schemas/workflow_manifest.schema.json +0 -0
  58. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/sdk_path.py +0 -0
  59. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/shared.py +0 -0
  60. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/terminal.py +0 -0
  61. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/mn_cli/update_cmds.py +0 -0
  62. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/pyproject.toml +0 -0
  63. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/scripts/check-release-artifacts.sh +0 -0
  64. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/scripts/make-release-zip.sh +0 -0
  65. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/scripts/validate-version-tag.sh +0 -0
  66. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/setup.cfg +0 -0
  67. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/conftest.py +0 -0
  68. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_backup_cmds.py +0 -0
  69. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_blueprint_cmds.py +0 -0
  70. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_blueprint_repository.py +0 -0
  71. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_blueprint_resources.py +0 -0
  72. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_deployment_cmds.py +0 -0
  73. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_docker_network_integration.py +0 -0
  74. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_main.py +0 -0
  75. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_model_cmds.py +0 -0
  76. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_repo_hygiene.py +0 -0
  77. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_resource_cmds.py +0 -0
  78. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_run_helpers.py +0 -0
  79. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_runtime_health.py +0 -0
  80. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_runtime_mode.py +0 -0
  81. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_runtime_state.py +0 -0
  82. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_schedule_cmds.py +0 -0
  83. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_service_cmds.py +0 -0
  84. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_sys_cmds.py +0 -0
  85. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_terminal.py +0 -0
  86. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_ui.py +0 -0
  87. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_update_cmds.py +0 -0
  88. {mirrorneuron_cli-1.2.15 → mirrorneuron_cli-1.2.18}/tests/test_workflow_validation.py +0 -0
  89. {mirrorneuron_cli-1.2.15 → 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.15
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.15
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:
@@ -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",
@@ -19,7 +19,7 @@ from urllib.parse import urlparse
19
19
  import typer
20
20
  from rich.console import Console
21
21
  from rich.table import Table
22
- from mn_sdk.blueprint_source import DEFAULT_BLUEPRINT_REPO
22
+ from mn_sdk.blueprint_source import DEFAULT_BLUEPRINT_REPO, normalize_blueprint_repo_value
23
23
  from mn_cli.config import CliConfig
24
24
  from mn_cli.libs.ui import print_confirmed, print_success_confirmation
25
25
  from mn_cli.logging_config import configure_logging
@@ -36,6 +36,8 @@ logger = configure_logging("mn-cli", CliConfig.from_env().log_path)
36
36
  GRPC_ADMIN_TOKEN_ENV = "MN_GRPC_ADMIN_TOKEN"
37
37
  GRPC_AUTH_TOKEN_FILE_ENV = "MN_GRPC_AUTH_TOKEN_FILE"
38
38
  GRPC_ADMIN_TOKEN_FILE_ENV = "MN_GRPC_ADMIN_TOKEN_FILE"
39
+ FIXED_GRPC_AUTH_TOKEN = "mirror_neuron_password"
40
+ FIXED_GRPC_ADMIN_TOKEN = "mirror_neuron_password_admin"
39
41
 
40
42
  def _erl_aflags(dist_port: str | int) -> str:
41
43
  return (
@@ -186,8 +188,6 @@ DEFAULT_BLUEPRINT_WEB_UI_PORT_ALLOCATION_MODE = "prepublished"
186
188
  DEFAULT_CONTAINER_RUNS_ROOT = "/root/.mn/runs"
187
189
  DEFAULT_CONTAINER_BLOB_STORE_ROOT = "/root/.mn/blobs"
188
190
  DEFAULT_RUNTIME_SHARED_STORAGE_ROOT = "/root/.mn/shared"
189
- DEFAULT_CONTAINER_GRPC_AUTH_TOKEN_FILE = "/root/.mn/grpc_auth.token"
190
- DEFAULT_CONTAINER_GRPC_ADMIN_TOKEN_FILE = "/root/.mn/grpc_admin.token"
191
191
  DEFAULT_REDIS_IMAGE = "redis:8"
192
192
  LEGACY_GRPC_PORT = "50051"
193
193
  LEGACY_API_PORT = "4001"
@@ -732,46 +732,10 @@ def _resolve_mn_cookie() -> str:
732
732
  return generated_cookie
733
733
 
734
734
  def _resolve_grpc_auth_token() -> str:
735
- env_token = os.getenv("MN_GRPC_AUTH_TOKEN", "").strip()
736
- if env_token:
737
- return env_token
738
-
739
- token_file = DIR / "grpc_auth.token"
740
- try:
741
- existing_token = token_file.read_text().strip()
742
- if existing_token:
743
- return existing_token
744
- except FileNotFoundError:
745
- pass
746
-
747
- DIR.mkdir(parents=True, exist_ok=True)
748
- generated_token = secrets.token_hex(32)
749
- _write_grpc_token_file(token_file, generated_token, "gRPC auth")
750
- return generated_token
735
+ return FIXED_GRPC_AUTH_TOKEN
751
736
 
752
737
  def _resolve_grpc_admin_token() -> str:
753
- env_token = os.getenv(GRPC_ADMIN_TOKEN_ENV, "").strip()
754
- if env_token:
755
- return env_token
756
-
757
- token_file = DIR / "grpc_admin.token"
758
- try:
759
- existing_token = token_file.read_text().strip()
760
- if existing_token:
761
- return existing_token
762
- except FileNotFoundError:
763
- pass
764
-
765
- DIR.mkdir(parents=True, exist_ok=True)
766
- generated_token = secrets.token_hex(32)
767
- _write_grpc_token_file(token_file, generated_token, "gRPC admin")
768
- return generated_token
769
-
770
- def _write_grpc_token_file(token_file: Path, token: str, label: str) -> None:
771
- token = str(token or "").strip()
772
- if not token:
773
- return
774
- write_private_text(token_file, f"{token}\n")
738
+ return FIXED_GRPC_ADMIN_TOKEN
775
739
 
776
740
  def _resolve_api_token() -> str:
777
741
  env_token = os.getenv("MN_API_TOKEN", "").strip()
@@ -802,57 +766,34 @@ def _ensure_runtime_api_token(env: dict[str, str], *, persist_compose: bool = Fa
802
766
 
803
767
  def _ensure_runtime_grpc_tokens(env: dict[str, str], *, persist_compose: bool = False) -> dict[str, str]:
804
768
  resolved = dict(env)
805
- if not str(resolved.get("MN_GRPC_AUTH_TOKEN") or "").strip():
806
- resolved["MN_GRPC_AUTH_TOKEN"] = _resolve_grpc_auth_token()
807
- if not str(resolved.get(GRPC_ADMIN_TOKEN_ENV) or "").strip():
808
- resolved[GRPC_ADMIN_TOKEN_ENV] = _resolve_grpc_admin_token()
809
- if not str(resolved.get(GRPC_AUTH_TOKEN_FILE_ENV) or "").strip():
810
- resolved[GRPC_AUTH_TOKEN_FILE_ENV] = DEFAULT_CONTAINER_GRPC_AUTH_TOKEN_FILE
811
- if not str(resolved.get(GRPC_ADMIN_TOKEN_FILE_ENV) or "").strip():
812
- resolved[GRPC_ADMIN_TOKEN_FILE_ENV] = DEFAULT_CONTAINER_GRPC_ADMIN_TOKEN_FILE
813
-
814
- _write_grpc_token_file(DIR / "grpc_auth.token", resolved["MN_GRPC_AUTH_TOKEN"], "gRPC auth")
815
- _write_grpc_token_file(
816
- DIR / "grpc_admin.token",
817
- resolved[GRPC_ADMIN_TOKEN_ENV],
818
- "gRPC admin",
819
- )
769
+ fixed_tokens = {
770
+ "MN_GRPC_AUTH_TOKEN": FIXED_GRPC_AUTH_TOKEN,
771
+ GRPC_ADMIN_TOKEN_ENV: FIXED_GRPC_ADMIN_TOKEN,
772
+ }
773
+ stale_token_keys = {
774
+ "MN_MIRROR_NEURON_GRPC_ADMIN_TOKEN",
775
+ GRPC_AUTH_TOKEN_FILE_ENV,
776
+ GRPC_ADMIN_TOKEN_FILE_ENV,
777
+ }
778
+ for key in stale_token_keys:
779
+ resolved.pop(key, None)
780
+ resolved.update(fixed_tokens)
820
781
  if persist_compose:
821
- _write_env_file_values(
822
- RUNTIME_COMPOSE_ENV,
823
- {
824
- "MN_GRPC_AUTH_TOKEN": resolved["MN_GRPC_AUTH_TOKEN"],
825
- GRPC_ADMIN_TOKEN_ENV: resolved[GRPC_ADMIN_TOKEN_ENV],
826
- GRPC_AUTH_TOKEN_FILE_ENV: resolved[GRPC_AUTH_TOKEN_FILE_ENV],
827
- GRPC_ADMIN_TOKEN_FILE_ENV: resolved[GRPC_ADMIN_TOKEN_FILE_ENV],
828
- },
829
- )
782
+ _write_env_file_values(RUNTIME_COMPOSE_ENV, fixed_tokens)
783
+ _remove_env_file_keys(RUNTIME_COMPOSE_ENV, stale_token_keys)
830
784
  return resolved
831
785
 
832
786
  def _grpc_tokens_from_handshake(handshake: Optional[dict]) -> dict[str, str]:
833
- if not handshake:
834
- return {}
835
- tokens: dict[str, str] = {}
836
- auth_token = str(handshake.get("grpc_auth_token") or "").strip()
837
- admin_token = str(handshake.get("grpc_admin_token") or "").strip()
838
- if auth_token:
839
- tokens["MN_GRPC_AUTH_TOKEN"] = auth_token
840
- if admin_token:
841
- tokens[GRPC_ADMIN_TOKEN_ENV] = admin_token
842
- return tokens
787
+ return {
788
+ "MN_GRPC_AUTH_TOKEN": FIXED_GRPC_AUTH_TOKEN,
789
+ GRPC_ADMIN_TOKEN_ENV: FIXED_GRPC_ADMIN_TOKEN,
790
+ }
843
791
 
844
792
  def _runtime_grpc_tokens_from_running_container() -> dict[str, str]:
845
- tokens: dict[str, str] = {}
846
- for container_name in (LOCAL_CORE_CONTAINER, NETWORK_CORE_CONTAINER):
847
- auth_token = _docker_container_env_value(container_name, "MN_GRPC_AUTH_TOKEN")
848
- admin_token = _docker_container_env_value(container_name, GRPC_ADMIN_TOKEN_ENV)
849
- if auth_token:
850
- tokens["MN_GRPC_AUTH_TOKEN"] = auth_token
851
- if admin_token:
852
- tokens[GRPC_ADMIN_TOKEN_ENV] = admin_token
853
- if tokens:
854
- break
855
- return tokens
793
+ return {
794
+ "MN_GRPC_AUTH_TOKEN": FIXED_GRPC_AUTH_TOKEN,
795
+ GRPC_ADMIN_TOKEN_ENV: FIXED_GRPC_ADMIN_TOKEN,
796
+ }
856
797
 
857
798
  def _resolve_network_token(force_new: bool = False) -> str:
858
799
  if force_new:
@@ -1245,8 +1186,8 @@ def _network_core_env(
1245
1186
  "MN_RUNTIME_SHARED_STORAGE_ROOT": DEFAULT_RUNTIME_SHARED_STORAGE_ROOT,
1246
1187
  "MN_DIST_PORT": str(dist_port),
1247
1188
  "MN_COOKIE": _derive_network_secret(token, "cookie"),
1248
- "MN_GRPC_AUTH_TOKEN": _resolve_grpc_auth_token(),
1249
- GRPC_ADMIN_TOKEN_ENV: _resolve_grpc_admin_token(),
1189
+ "MN_GRPC_AUTH_TOKEN": FIXED_GRPC_AUTH_TOKEN,
1190
+ GRPC_ADMIN_TOKEN_ENV: FIXED_GRPC_ADMIN_TOKEN,
1250
1191
  "ERL_EPMD_ADDRESS": "0.0.0.0",
1251
1192
  "ERL_EPMD_PORT": str(epmd_port),
1252
1193
  "ERL_AFLAGS": _erl_aflags(dist_port),
@@ -1652,9 +1593,6 @@ def _start_network_seed(
1652
1593
  redis_public_host=redis_public_host,
1653
1594
  redis_public_port=redis_public_port,
1654
1595
  )
1655
- env["MN_GRPC_AUTH_TOKEN"] = _resolve_grpc_auth_token()
1656
- env[GRPC_ADMIN_TOKEN_ENV] = _resolve_grpc_admin_token()
1657
-
1658
1596
  env = _ensure_runtime_grpc_tokens(env, persist_compose=runtime_compose_available())
1659
1597
 
1660
1598
  console.print("=> Starting MirrorNeuron core-only exposed node...")
@@ -2029,11 +1967,10 @@ def ensure_context_engine_runtime(*, force: bool = False) -> dict[str, str]:
2029
1967
  _remove_non_mirror_neuron_container(CONTEXT_ENGINE_CONTAINER)
2030
1968
  _remove_non_mirror_neuron_container(CONTEXT_ENGINE_MODEL_CONTAINER)
2031
1969
  _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"
1970
+ model_status = _install_context_engine_model(model)
2034
1971
 
2035
1972
  already_running = _docker_container_running(CONTEXT_ENGINE_CONTAINER)
2036
- if force or not already_running or not model_already_installed:
1973
+ if force or not already_running:
2037
1974
  compose_env = env
2038
1975
  anonymous_docker_config: Path | None = None
2039
1976
  if use_engine_image:
@@ -2066,8 +2003,6 @@ def ensure_context_engine_runtime(*, force: bool = False) -> dict[str, str]:
2066
2003
  if anonymous_docker_config is not None:
2067
2004
  shutil.rmtree(anonymous_docker_config, ignore_errors=True)
2068
2005
  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"
2071
2006
  else:
2072
2007
  status = "already_running"
2073
2008
 
@@ -2091,6 +2026,37 @@ def _docker_model_inspect_ok(model: str) -> bool:
2091
2026
  return False
2092
2027
  return _docker_command_ok(["docker", "model", "inspect", model])
2093
2028
 
2029
+ def _install_context_engine_model(model: str) -> str:
2030
+ model = str(model or "").strip()
2031
+ if not model:
2032
+ return "skipped"
2033
+ if _docker_model_inspect_ok(model):
2034
+ return "already_installed"
2035
+ pull_result = subprocess.run(
2036
+ ["docker", "model", "pull", model],
2037
+ capture_output=True,
2038
+ text=True,
2039
+ timeout=900,
2040
+ )
2041
+ if pull_result.returncode != 0 and not _docker_model_inspect_ok(model):
2042
+ output = _subprocess_error_output(pull_result)
2043
+ raise RuntimeError(f"Failed to install context engine model {model}: {output}")
2044
+ run_result = subprocess.run(
2045
+ ["docker", "model", "run", "--detach", model],
2046
+ capture_output=True,
2047
+ text=True,
2048
+ timeout=300,
2049
+ )
2050
+ if run_result.returncode != 0:
2051
+ output = _subprocess_error_output(run_result)
2052
+ if "already" not in output.lower():
2053
+ raise RuntimeError(f"Failed to start context engine model {model}: {output}")
2054
+ return "installed"
2055
+
2056
+ def _subprocess_error_output(result: subprocess.CompletedProcess[str]) -> str:
2057
+ output = f"{result.stderr or ''}\n{result.stdout or ''}".strip()
2058
+ return output or f"exit code {result.returncode}"
2059
+
2094
2060
  def _anonymous_public_gar_docker_env(env: dict[str, str], image: str) -> tuple[dict[str, str], Path | None]:
2095
2061
  if not _is_public_gar_image(image):
2096
2062
  return env, None
@@ -2454,10 +2420,13 @@ def _cluster_endpoint_host(env: dict[str, str], host: str) -> str:
2454
2420
  return _native_endpoint_host(normalized)
2455
2421
 
2456
2422
  def _runtime_blueprint_env_updates(env: dict[str, str]) -> dict[str, str]:
2423
+ runtime_env = str(env.get("MN_ENV") or os.getenv("MN_ENV") or "dev").strip().lower()
2457
2424
  blueprint_source = str(env.get("MN_BLUEPRINT_SOURCE") or os.getenv("MN_BLUEPRINT_SOURCE") or "github").strip().lower()
2458
2425
  if blueprint_source not in {"github", "local"}:
2459
2426
  blueprint_source = "github"
2460
- blueprint_repo = str(env.get("MN_BLUEPRINT_REPO") or os.getenv("MN_BLUEPRINT_REPO") or DEFAULT_BLUEPRINT_REPO).strip()
2427
+ blueprint_repo = normalize_blueprint_repo_value(
2428
+ str(env.get("MN_BLUEPRINT_REPO") or os.getenv("MN_BLUEPRINT_REPO") or DEFAULT_BLUEPRINT_REPO).strip()
2429
+ )
2461
2430
  blueprint_local = str(env.get("MN_BLUEPRINT_LOCAL") or os.getenv("MN_BLUEPRINT_LOCAL") or "").strip()
2462
2431
  host_home_dir = str(
2463
2432
  env.get("MN_HOST_HOME_DIR")
@@ -2500,6 +2469,7 @@ def _runtime_blueprint_env_updates(env: dict[str, str]) -> dict[str, str]:
2500
2469
  or DEFAULT_RUNTIME_SHARED_STORAGE_ROOT
2501
2470
  ).strip()
2502
2471
  updates: dict[str, str] = {
2472
+ "MN_ENV": runtime_env,
2503
2473
  "MN_BLUEPRINT_SOURCE": blueprint_source,
2504
2474
  "MN_BLUEPRINT_REPO": blueprint_repo,
2505
2475
  "MN_BLUEPRINT_LOCAL": blueprint_local,
@@ -2636,6 +2606,11 @@ def _runtime_api_config_mismatches(env: dict[str, str], health: Optional[dict[st
2636
2606
  if not health:
2637
2607
  return []
2638
2608
  mismatches: list[tuple[str, str, str]] = []
2609
+ expected_env = str(env.get("MN_ENV") or "dev").strip().lower()
2610
+ active_env = str(health.get("env") or health.get("mn_env") or "").strip().lower()
2611
+ if active_env and expected_env and active_env != expected_env:
2612
+ mismatches.append(("MN_ENV", active_env, expected_env))
2613
+
2639
2614
  expected_blueprint = _expected_blueprint_location(env)
2640
2615
  active_blueprint = str(
2641
2616
  health.get("active_blueprint_location")
@@ -3632,10 +3607,8 @@ def _build_core_docker_run_command(
3632
3607
 
3633
3608
  cmd.extend(["-e", f"MN_NODE_NAME={env['MN_NODE_NAME']}"])
3634
3609
  cmd.extend(["-e", f"MN_COOKIE={env['MN_COOKIE']}"])
3635
- cmd.extend(["-e", f"MN_GRPC_AUTH_TOKEN={env['MN_GRPC_AUTH_TOKEN']}"])
3636
- cmd.extend(["-e", f"{GRPC_AUTH_TOKEN_FILE_ENV}={env[GRPC_AUTH_TOKEN_FILE_ENV]}"])
3637
- cmd.extend(["-e", f"{GRPC_ADMIN_TOKEN_ENV}={env[GRPC_ADMIN_TOKEN_ENV]}"])
3638
- cmd.extend(["-e", f"{GRPC_ADMIN_TOKEN_FILE_ENV}={env[GRPC_ADMIN_TOKEN_FILE_ENV]}"])
3610
+ cmd.extend(["-e", f"MN_GRPC_AUTH_TOKEN={env.get('MN_GRPC_AUTH_TOKEN', FIXED_GRPC_AUTH_TOKEN)}"])
3611
+ cmd.extend(["-e", f"{GRPC_ADMIN_TOKEN_ENV}={env.get(GRPC_ADMIN_TOKEN_ENV, FIXED_GRPC_ADMIN_TOKEN)}"])
3639
3612
  cmd.extend(["-e", f"MN_NETWORK_JOIN_TOKEN={env['MN_NETWORK_JOIN_TOKEN']}"])
3640
3613
  cmd.extend(["-e", f"MN_NETWORK_ADVERTISE_HOST={env['MN_NETWORK_ADVERTISE_HOST']}"])
3641
3614
  if env.get("MN_MODEL_SERVICE_NODE_NAME"):
@@ -3779,14 +3752,16 @@ def _start_server(
3779
3752
  env = _ensure_runtime_api_token(env, persist_compose=compose_runtime)
3780
3753
  if compose_runtime:
3781
3754
  env = _ensure_compose_native_port_settings(env)
3782
- if not _docker_container_running("mirror-neuron-core"):
3755
+ core_running = _docker_container_running("mirror-neuron-core")
3756
+ if not core_running:
3783
3757
  console.print("=> MirrorNeuron Core is not running; starting Docker runtime (Compose)...")
3784
- try:
3785
- subprocess.run(runtime_compose_cmd("up", "-d"), check=True, stdout=subprocess.DEVNULL, env=env)
3758
+ try:
3759
+ subprocess.run(runtime_compose_cmd("up", "-d"), check=True, stdout=subprocess.DEVNULL, env=env)
3760
+ if not core_running:
3786
3761
  console.print(" [green][Started][/green] Docker runtime (Compose project: mirror-neuron)")
3787
- except (FileNotFoundError, subprocess.CalledProcessError):
3788
- console.print("[red]Failed to start MirrorNeuron Docker runtime.[/red]")
3789
- raise typer.Exit(1)
3762
+ except (FileNotFoundError, subprocess.CalledProcessError):
3763
+ console.print("[red]Failed to start MirrorNeuron Docker runtime.[/red]")
3764
+ raise typer.Exit(1)
3790
3765
  env.setdefault("MN_API_HOST", _api_host())
3791
3766
  env.setdefault("MN_API_PORT", DEFAULT_API_PORT)
3792
3767
  env.setdefault("MN_WEB_UI_HOST", _web_ui_host())
@@ -25,7 +25,7 @@ def _capture_console(monkeypatch):
25
25
  return output
26
26
 
27
27
 
28
- def test_clear_preflights_missing_admin_token(monkeypatch):
28
+ def test_clear_runs_without_local_admin_token_preflight(monkeypatch):
29
29
  output = _capture_console(monkeypatch)
30
30
  client = SimpleNamespace(admin_token="", clear_jobs=lambda: 1)
31
31
  monkeypatch.setattr(job_cmds, "client", client)
@@ -34,8 +34,8 @@ def test_clear_preflights_missing_admin_token(monkeypatch):
34
34
  job_cmds.clear()
35
35
 
36
36
  rendered = output.getvalue()
37
- assert "No local gRPC admin token was found" in rendered
38
- assert "shared grpc_admin.token file" in rendered
37
+ assert "Job clear successful" in rendered
38
+ assert "Jobs cleared: 1 non-running" in rendered
39
39
 
40
40
 
41
41
  def test_clear_reports_admin_token_mismatch(monkeypatch):
@@ -54,4 +54,4 @@ def test_clear_reports_admin_token_mismatch(monkeypatch):
54
54
 
55
55
  rendered = output.getvalue()
56
56
  assert "ClearJobs admin authorization failed" in rendered
57
- assert "different cluster tokens" in rendered
57
+ assert "fixed gRPC admin token" in rendered