mlspace-plugin 0.2.1__tar.gz → 0.2.2__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 (121) hide show
  1. mlspace_plugin-0.2.2/.gitignore +16 -0
  2. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/INSTALL.md +15 -9
  3. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/INSTALL_AGENT.md +6 -4
  4. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/PKG-INFO +5 -39
  5. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/README.md +3 -37
  6. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/RELEASE.md +5 -5
  7. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/plugin/.claude-plugin/plugin.json +1 -1
  8. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/plugin/.codex-plugin/plugin.json +1 -1
  9. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/plugin/.mcp.json +1 -1
  10. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/plugin/README.md +8 -8
  11. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/plugin/mcp.json +1 -1
  12. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/plugin/plugin.json +1 -1
  13. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/pyproject.toml +1 -1
  14. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/scripts/serve_stdio.sh +0 -0
  15. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/scripts/serve_stdio_dryrun.sh +0 -0
  16. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/__init__.py +1 -1
  17. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/client_setup.py +34 -0
  18. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/init_cli.py +27 -3
  19. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_client_setup.py +64 -1
  20. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_onboarding_init.py +64 -1
  21. mlspace_plugin-0.2.1/.gitignore +0 -53
  22. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/.env.example +0 -0
  23. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/AGENTS.md +0 -0
  24. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/CONTRIBUTING.md +0 -0
  25. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/LICENSE +0 -0
  26. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/plugin/skills/mlspace-call-deployed-model-predict/SKILL.md +0 -0
  27. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/plugin/skills/mlspace-deploy-inference-service/SKILL.md +0 -0
  28. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/plugin/skills/mlspace-diagnose-data-transfer/SKILL.md +0 -0
  29. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/plugin/skills/mlspace-diagnose-failed-training-job/SKILL.md +0 -0
  30. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/plugin/skills/mlspace-diagnose-job-launch-rejection/SKILL.md +0 -0
  31. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/plugin/skills/mlspace-diagnose-unhealthy-inference/SKILL.md +0 -0
  32. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/plugin/skills/mlspace-discover-launchable-resources/SKILL.md +0 -0
  33. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/plugin/skills/mlspace-launch-training-job/SKILL.md +0 -0
  34. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/plugin/skills/mlspace-list-running-jobs/SKILL.md +0 -0
  35. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/plugin/skills/mlspace-resume-notebook-and-set-autoshutdown/SKILL.md +0 -0
  36. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/plugin/skills/mlspace-stage-s3-to-nfs/SKILL.md +0 -0
  37. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/plugin/skills/mlspace-transfer-compute-node/SKILL.md +0 -0
  38. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/scripts/build_plugin.py +0 -0
  39. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/scripts/demo_multi_workspace.py +0 -0
  40. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/scripts/gen_body_schemas.py +0 -0
  41. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/scripts/gen_plugin.py +0 -0
  42. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/scripts/mcp_call.py +0 -0
  43. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/__main__.py +0 -0
  44. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/_status.py +0 -0
  45. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/auth.py +0 -0
  46. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/bodyspec.py +0 -0
  47. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/client.py +0 -0
  48. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/config.py +0 -0
  49. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/errors.py +0 -0
  50. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/formatting.py +0 -0
  51. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/job_hooks.py +0 -0
  52. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/jsonc.py +0 -0
  53. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/overview.py +0 -0
  54. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/registry.py +0 -0
  55. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/resource_refs.py +0 -0
  56. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/server.py +0 -0
  57. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/setup_ui.py +0 -0
  58. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/spec/body_schemas.json +0 -0
  59. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/spec/openapi.json +0 -0
  60. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/tls.py +0 -0
  61. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/tools/__init__.py +0 -0
  62. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/tools/allocations.py +0 -0
  63. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/tools/async_inference.py +0 -0
  64. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/tools/build_image.py +0 -0
  65. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/tools/dalle.py +0 -0
  66. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/tools/data_transfer.py +0 -0
  67. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/tools/docker_registry.py +0 -0
  68. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/tools/inference.py +0 -0
  69. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/tools/jobs.py +0 -0
  70. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/tools/notebooks.py +0 -0
  71. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/tools/queues.py +0 -0
  72. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/tools/resources.py +0 -0
  73. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/tools/tensorboards.py +0 -0
  74. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/tools/workspaces.py +0 -0
  75. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/src/mlspace_mcp/workspace_context.py +0 -0
  76. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/conftest.py +0 -0
  77. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/specutil.py +0 -0
  78. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_allocations.py +0 -0
  79. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_artifact_sync.py +0 -0
  80. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_async_inference.py +0 -0
  81. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_auth.py +0 -0
  82. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_autofill.py +0 -0
  83. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_bodyspec.py +0 -0
  84. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_build_image.py +0 -0
  85. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_client.py +0 -0
  86. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_configs_shrink.py +0 -0
  87. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_dalle.py +0 -0
  88. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_data_transfer.py +0 -0
  89. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_docker_registry.py +0 -0
  90. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_drift.py +0 -0
  91. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_dry_run.py +0 -0
  92. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_dry_run_fidelity.py +0 -0
  93. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_env_file.py +0 -0
  94. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_errors.py +0 -0
  95. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_false_success.py +0 -0
  96. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_formatting.py +0 -0
  97. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_init_cli.py +0 -0
  98. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_integration.py +0 -0
  99. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_job_status.py +0 -0
  100. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_jobs.py +0 -0
  101. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_log_normalisation.py +0 -0
  102. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_mcp_error_contract.py +0 -0
  103. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_notebooks.py +0 -0
  104. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_onboarding_tls.py +0 -0
  105. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_overview.py +0 -0
  106. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_plugin_bundle.py +0 -0
  107. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_plugin_cli.py +0 -0
  108. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_plugin_release.py +0 -0
  109. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_preflight.py +0 -0
  110. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_queue_scope.py +0 -0
  111. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_queues.py +0 -0
  112. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_registry.py +0 -0
  113. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_resource_refs.py +0 -0
  114. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_resources.py +0 -0
  115. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_route_confusion.py +0 -0
  116. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_setup_ui.py +0 -0
  117. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_skills.py +0 -0
  118. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_tensorboards.py +0 -0
  119. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_workspace_context.py +0 -0
  120. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_workspaces.py +0 -0
  121. {mlspace_plugin-0.2.1 → mlspace_plugin-0.2.2}/tests/test_wrapper_hardening.py +0 -0
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ build/
6
+ dist/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ htmlcov/
12
+ .env
13
+ .env.*
14
+ !.env.example
15
+ *.secret
16
+ .DS_Store
@@ -20,8 +20,11 @@ mlspace-plugin setup
20
20
  ```
21
21
 
22
22
  Если пакет уже установлен: `uv tool install --force .`.
23
- При установке из wheel: `uv tool install /путь/к/mlspace_plugin-0.2.1-py3-none-any.whl`.
23
+ При установке из wheel: `uv tool install /путь/к/mlspace_plugin-0.2.2-py3-none-any.whl`.
24
24
  Не используйте editable-установку из `/tmp` для постоянной работы.
25
+ Далее команды показаны для PyPI. После установки исходников или wheel через
26
+ `uv tool install` заменяйте `uvx mlspace-plugin@latest` на `mlspace-plugin`,
27
+ чтобы использовать именно установленную сборку.
25
28
 
26
29
  Мастер:
27
30
 
@@ -41,7 +44,7 @@ mlspace-plugin setup
41
44
  ## Stage и корпоративные сертификаты
42
45
 
43
46
  ```bash
44
- mlspace-plugin setup --base-url https://mlspace-stage.example.com
47
+ uvx mlspace-plugin@latest setup --base-url https://mlspace-stage.example.com
45
48
  ```
46
49
 
47
50
  Настройка и работа сервера используют системные доверенные сертификаты: Keychain
@@ -49,7 +52,7 @@ mlspace-plugin setup --base-url https://mlspace-stage.example.com
49
52
  попросит путь к PEM, полученному у администратора. Можно указать заранее:
50
53
 
51
54
  ```bash
52
- mlspace-plugin setup --ca-file /путь/к/corporate-ca.pem
55
+ uvx mlspace-plugin@latest setup --ca-file /путь/к/corporate-ca.pem
53
56
  ```
54
57
 
55
58
  Мастер сохранит копию CA рядом с настройками. Переменные текущего терминала и
@@ -63,10 +66,10 @@ mlspace-plugin setup --ca-file /путь/к/corporate-ca.pem
63
66
  ## Повторная настройка
64
67
 
65
68
  ```bash
66
- mlspace-plugin setup --force
67
- mlspace-plugin setup --force --replace-credentials
68
- mlspace-plugin setup --client opencode
69
- mlspace-plugin setup --client-path opencode=/полный/путь/opencode
69
+ uvx mlspace-plugin@latest setup --force
70
+ uvx mlspace-plugin@latest setup --force --replace-credentials
71
+ uvx mlspace-plugin@latest setup --force --client opencode
72
+ uvx mlspace-plugin@latest setup --force --client-path opencode=/полный/путь/opencode
70
73
  ```
71
74
 
72
75
  `--force` повторяет настройку с сохранёнными ключами на прежнем endpoint.
@@ -88,7 +91,7 @@ mlspace-plugin setup --client-path opencode=/полный/путь/opencode
88
91
 
89
92
  ## Linux по SSH / установка агентом
90
93
 
91
- Запустите `mlspace-plugin setup` в своей SSH-сессии. Браузер и графическая среда не нужны.
94
+ Запустите `uvx mlspace-plugin@latest setup` в своей SSH-сессии. Браузер и графическая среда не нужны.
92
95
  Если установкой занимается агент, он готовит пакет, а ключи вы вводите сами в своём
93
96
  терминале. После завершения сообщите агенту «готово», без ключей и содержимого `.env`.
94
97
 
@@ -101,7 +104,10 @@ mlspace-plugin setup --client-path opencode=/полный/путь/opencode
101
104
 
102
105
  Для установки через PyPI повторите `uvx mlspace-plugin@latest setup --force`.
103
106
  Мастер сохранит ключи, предложит выбор workspace и обновит скиллы без локальных
104
- изменений. Изменённые вручную скиллы вызовут конфликт с указанием файла.
107
+ изменений. MCP обновляется во всех клиентах, ранее подключённых мастером,
108
+ чтобы версия сервера совпадала с общими скиллами. `--client` выбирает новые
109
+ подключения, а существующие управляемые подключения обновляются вместе.
110
+ Изменённые вручную регистрации и скиллы вызовут конфликт с указанием файла.
105
111
  Регистрация MCP, созданная мастером и не изменённая вручную, обновляется
106
112
  автоматически. Посторонние и изменённые регистрации требуют разбора конфликта.
107
113
  Для постоянной установки: `uv tool upgrade mlspace-plugin`, затем
@@ -9,10 +9,12 @@
9
9
  При установке из исходников установите пакет в постоянное окружение: `uv tool install .` из checkout или `uv tool install /path/package.whl`.
10
10
  Для уже установленного пакета добавьте `--force` только в рамках запрошенного обновления.
11
11
  Не оставляйте постоянный запуск на editable-исходниках в `/tmp`.
12
+ Далее команды показаны для PyPI. Если установили исходники/wheel через `uv tool install`,
13
+ заменяйте префикс `uvx mlspace-plugin@latest` на `mlspace-plugin`, чтобы использовать именно эту сборку.
12
14
  2. Не просите Key ID/Secret в чате, не читайте `.env`, не печатайте окружение и
13
15
  не вставляйте ключи в аргументы shell/CLI. Не запускайте интерактивный ввод
14
16
  ключей в терминале, который видите только вы.
15
- 3. Дайте человеку одну команду `mlspace-plugin setup` для его собственного терминала
17
+ 3. Дайте человеку одну команду `uvx mlspace-plugin@latest setup` для его собственного терминала
16
18
  (по SSH — на целевой машине). Для stage добавьте согласованный `--base-url`.
17
19
  Объясните: два ключа под звёздочками → поиск и выбор workspace → подключение.
18
20
  Дождитесь сообщения о завершении. Человек не должен присылать секреты или файл.
@@ -21,7 +23,7 @@
21
23
  клиента можно заранее дать `--client claude-code`, `--client codex` или `--client opencode`.
22
24
  Сбой TLS исправляется системным CA или PEM от администратора через `--ca-file`.
23
25
  Никогда не отключайте TLS и не подставляйте сертификат с недоверенного сервера.
24
- 5. Проверьте `mlspace-plugin --list-tools`. Эта команда проверяет только состав
26
+ 5. Проверьте `uvx mlspace-plugin@latest --list-tools`. Эта команда проверяет только состав
25
27
  инструментов, а не авторизацию и не загрузку выбранным клиентом.
26
28
  Попросите открыть новую сессию клиента (при необходимости перезапустить).
27
29
  После появления MCP выполните read-only запрос списка выбранных workspace.
@@ -36,7 +38,7 @@
36
38
  файл, передавайте **путь**, не читайте содержимое. Для поиска нужных workspace:
37
39
 
38
40
  ```bash
39
- mlspace-plugin setup --non-interactive --credentials-file /path/credentials.env \
41
+ uvx mlspace-plugin@latest setup --non-interactive --credentials-file /path/credentials.env \
40
42
  --base-url https://api.ai.cloud.ru --list-workspaces --search project --limit 20
41
43
  ```
42
44
 
@@ -45,7 +47,7 @@ mlspace-plugin setup --non-interactive --credentials-file /path/credentials.env
45
47
  Установка с точными ID, без интерактивных вопросов:
46
48
 
47
49
  ```bash
48
- mlspace-plugin setup --non-interactive --credentials-file /path/credentials.env \
50
+ uvx mlspace-plugin@latest setup --non-interactive --credentials-file /path/credentials.env \
49
51
  --client opencode --workspace WORKSPACE_UUID
50
52
  ```
51
53
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: mlspace-plugin
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Cloud.ru MLSpace plugin: MCP server, agent skills and setup
5
5
  Project-URL: Repository, https://github.com/noesskeetit/mlspace-plugin
6
6
  Project-URL: Issues, https://github.com/noesskeetit/mlspace-plugin/issues
@@ -114,7 +114,7 @@ TLS использует системные доверенные CA. Для ко
114
114
  мастер сохраняет копию для последующих запусков. Ошибка TLS/сети останавливает
115
115
  настройку без перезаписи прежнего файла и без запроса ручного x-api-key.
116
116
 
117
- Повторная настройка — `mlspace-plugin setup --force`: сохранённые ключи на прежнем
117
+ Повторная настройка — `uvx mlspace-plugin@latest setup --force`: сохранённые ключи на прежнем
118
118
  endpoint переиспользуются. Для замены ключей добавьте `--replace-credentials`.
119
119
  Для stage укажите `--base-url https://mlspace-stage.example.com`.
120
120
  Только credentials без подключения клиентов — `--config-only`.
@@ -274,11 +274,11 @@ write-запросов подаются параметром `body: dict`; об
274
274
  ## Плагин: Codex, Claude Code и совместимые агенты
275
275
 
276
276
  [Поставка MLSpace](plugin/README.md) объединяет MCP-сервер и 12 скиллов.
277
- Внутренний релиз `mlspace-plugin-<версия>.zip` содержит точную версию сервера,
277
+ Архив релиза `mlspace-plugin-<версия>.zip` содержит точную версию сервера,
278
278
  общие скиллы и манифесты для загрузчиков Codex и Claude Code. Его установка
279
279
  не зависит от публикации `mlspace-plugin` в публичном PyPI.
280
280
 
281
- После распаковки и однократного `mlspace-plugin setup` добавьте каталог релиза как
281
+ После распаковки и однократного `setup --config-only` через wheel из архива добавьте каталог релиза как
282
282
  marketplace клиента. [Команды установки и сборки](plugin/README.md).
283
283
  Ключи остаются в пользовательском конфиге вне плагина.
284
284
 
@@ -318,40 +318,6 @@ MCP-подключение; поддержку конкретного загру
318
318
 
319
319
  ---
320
320
 
321
- ## Защиты от дефектов API
322
-
323
- Обёртка гасит несколько особенностей MLSpace API, на которых агент иначе спотыкается.
324
- API-ловушки ниже проверялись на живом API: основная часть — 03.08.2026,
325
- региональность `/home/jovyan` — 12.08.2026. Допустимость binary-команд подтверждена
326
- [официальной CLI-документацией](https://cloud.ru/docs/aicloud/mlspace-cli/topics/references__job-submit)
327
- и локальной схемой; проверка доступности команды в образе остаётся отдельным шагом.
328
-
329
- - **Тип entrypoint и регион проверяются до отправки.** Явный `region` обязателен для
330
- каждого типа задачи. Для `type=binary` поле `script` — непустая команда или путь к
331
- исполняемому файлу (`ls -lah`, `python /home/jovyan/launch.py -f script.py`); MCP
332
- передаёт строку без изменений и не заявляет, что команда существует в выбранном
333
- образе. Для остальных типов сохраняется проверка абсолютного пути под
334
- `/home/jovyan`. Это NFS конкретного региона: одинаковый путь в notebook `SR006` и
335
- job `A100-MT` может указывать на разные данные. Public API не умеет сделать `stat`
336
- на целевом NFS, поэтому наличие файла подтверждается same-region notebook'ом или
337
- transfer'ом. Локальная проверка формы пути или dry-run не проверяет наличие файла.
338
- - **Порядок лога восстанавливается.** `logs?tail=N` возвращает строки в обратном
339
- порядке — трейсбэк переворачивается, и причина оказывается ниже следствия. MCP
340
- сортирует обратно и помечает это в ответе, чтобы модель знала, что порядку можно
341
- доверять. Без `tail` порядок правильный и не трогается.
342
- - **Состояния вместо прозы.** Строки `"No such job"` (логи протухли по retention) и
343
- `Job … in queue. Try later` (задача в очереди) приходят внутри `HTTP 200`. MCP
344
- распознаёт их и выдаёт понятную типизированную пометку, чтобы «логи устарели» не
345
- читалось как «задачи не существует».
346
- - **Ретрай не уходит в бесконечность.** Часть ручек отдаёт `500` детерминированно
347
- (например, assignments на 6 из 7 аллокаций). Совет при `5xx` — повторить один раз,
348
- дальше считать отказ постоянным.
349
- - **Каталог образов сжимается.** `configs` отдаёт ~200 КБ (≈51k токенов), дублируя
350
- образы под каждым `instance_type`; MCP дедуплицирует их без потери данных и умеет
351
- отфильтровать один регион.
352
-
353
- ---
354
-
355
321
  ## Разработка
356
322
 
357
323
  ```bash
@@ -63,7 +63,7 @@ TLS использует системные доверенные CA. Для ко
63
63
  мастер сохраняет копию для последующих запусков. Ошибка TLS/сети останавливает
64
64
  настройку без перезаписи прежнего файла и без запроса ручного x-api-key.
65
65
 
66
- Повторная настройка — `mlspace-plugin setup --force`: сохранённые ключи на прежнем
66
+ Повторная настройка — `uvx mlspace-plugin@latest setup --force`: сохранённые ключи на прежнем
67
67
  endpoint переиспользуются. Для замены ключей добавьте `--replace-credentials`.
68
68
  Для stage укажите `--base-url https://mlspace-stage.example.com`.
69
69
  Только credentials без подключения клиентов — `--config-only`.
@@ -223,11 +223,11 @@ write-запросов подаются параметром `body: dict`; об
223
223
  ## Плагин: Codex, Claude Code и совместимые агенты
224
224
 
225
225
  [Поставка MLSpace](plugin/README.md) объединяет MCP-сервер и 12 скиллов.
226
- Внутренний релиз `mlspace-plugin-<версия>.zip` содержит точную версию сервера,
226
+ Архив релиза `mlspace-plugin-<версия>.zip` содержит точную версию сервера,
227
227
  общие скиллы и манифесты для загрузчиков Codex и Claude Code. Его установка
228
228
  не зависит от публикации `mlspace-plugin` в публичном PyPI.
229
229
 
230
- После распаковки и однократного `mlspace-plugin setup` добавьте каталог релиза как
230
+ После распаковки и однократного `setup --config-only` через wheel из архива добавьте каталог релиза как
231
231
  marketplace клиента. [Команды установки и сборки](plugin/README.md).
232
232
  Ключи остаются в пользовательском конфиге вне плагина.
233
233
 
@@ -267,40 +267,6 @@ MCP-подключение; поддержку конкретного загру
267
267
 
268
268
  ---
269
269
 
270
- ## Защиты от дефектов API
271
-
272
- Обёртка гасит несколько особенностей MLSpace API, на которых агент иначе спотыкается.
273
- API-ловушки ниже проверялись на живом API: основная часть — 03.08.2026,
274
- региональность `/home/jovyan` — 12.08.2026. Допустимость binary-команд подтверждена
275
- [официальной CLI-документацией](https://cloud.ru/docs/aicloud/mlspace-cli/topics/references__job-submit)
276
- и локальной схемой; проверка доступности команды в образе остаётся отдельным шагом.
277
-
278
- - **Тип entrypoint и регион проверяются до отправки.** Явный `region` обязателен для
279
- каждого типа задачи. Для `type=binary` поле `script` — непустая команда или путь к
280
- исполняемому файлу (`ls -lah`, `python /home/jovyan/launch.py -f script.py`); MCP
281
- передаёт строку без изменений и не заявляет, что команда существует в выбранном
282
- образе. Для остальных типов сохраняется проверка абсолютного пути под
283
- `/home/jovyan`. Это NFS конкретного региона: одинаковый путь в notebook `SR006` и
284
- job `A100-MT` может указывать на разные данные. Public API не умеет сделать `stat`
285
- на целевом NFS, поэтому наличие файла подтверждается same-region notebook'ом или
286
- transfer'ом. Локальная проверка формы пути или dry-run не проверяет наличие файла.
287
- - **Порядок лога восстанавливается.** `logs?tail=N` возвращает строки в обратном
288
- порядке — трейсбэк переворачивается, и причина оказывается ниже следствия. MCP
289
- сортирует обратно и помечает это в ответе, чтобы модель знала, что порядку можно
290
- доверять. Без `tail` порядок правильный и не трогается.
291
- - **Состояния вместо прозы.** Строки `"No such job"` (логи протухли по retention) и
292
- `Job … in queue. Try later` (задача в очереди) приходят внутри `HTTP 200`. MCP
293
- распознаёт их и выдаёт понятную типизированную пометку, чтобы «логи устарели» не
294
- читалось как «задачи не существует».
295
- - **Ретрай не уходит в бесконечность.** Часть ручек отдаёт `500` детерминированно
296
- (например, assignments на 6 из 7 аллокаций). Совет при `5xx` — повторить один раз,
297
- дальше считать отказ постоянным.
298
- - **Каталог образов сжимается.** `configs` отдаёт ~200 КБ (≈51k токенов), дублируя
299
- образы под каждым `instance_type`; MCP дедуплицирует их без потери данных и умеет
300
- отфильтровать один регион.
301
-
302
- ---
303
-
304
270
  ## Разработка
305
271
 
306
272
  ```bash
@@ -7,11 +7,11 @@
7
7
  `uv lock` и `uv run python scripts/gen_plugin.py`. Обновите примеры имён wheel/ZIP
8
8
  в инструкциях, если версия присутствует в имени файла.
9
9
  2. Выполните проверки из `CONTRIBUTING.md` на Linux и macOS.
10
- 3. Соберите пакет (в командах ниже замените 0.2.1 на выпускаемую версию):
10
+ 3. Соберите пакет (в командах ниже замените 0.2.2 на выпускаемую версию):
11
11
 
12
12
  ```sh
13
- uv build --out-dir dist/release-0.2.1
14
- uv run python scripts/build_plugin.py --wheel dist/release-0.2.1/mlspace_plugin-0.2.1-py3-none-any.whl --output dist/plugin-release-0.2.1
13
+ uv build --out-dir dist/release-0.2.2
14
+ uv run python scripts/build_plugin.py --wheel dist/release-0.2.2/mlspace_plugin-0.2.2-py3-none-any.whl --output dist/plugin-release-0.2.2
15
15
  ```
16
16
 
17
17
  4. Просмотрите содержимое wheel, sdist и ZIP. В них не должно быть credentials,
@@ -23,11 +23,11 @@ uv run python scripts/build_plugin.py --wheel dist/release-0.2.1/mlspace_plugin-
23
23
  `UV_PUBLISH_TOKEN` средствами вашего хранилища секретов, не аргументом команды:
24
24
 
25
25
  ```sh
26
- uv publish --trusted-publishing never dist/release-0.2.1/mlspace_plugin-0.2.1-py3-none-any.whl dist/release-0.2.1/mlspace_plugin-0.2.1.tar.gz
26
+ uv publish --trusted-publishing never dist/release-0.2.2/mlspace_plugin-0.2.2-py3-none-any.whl dist/release-0.2.2/mlspace_plugin-0.2.2.tar.gz
27
27
  ```
28
28
 
29
29
  6. Проверьте версию и SHA256 обоих файлов через PyPI JSON API; проверьте установку
30
- `uvx --from mlspace-plugin==0.2.1 mlspace-plugin --version` из нового кеша.
30
+ `uvx --from mlspace-plugin==0.2.2 mlspace-plugin --version` из нового кеша.
31
31
  ZIP и SHA256SUMS приложите к тому же релизу публичного Git-репозитория.
32
32
 
33
33
  Пакет не проверяет обновления в фоне. Для PyPI пользователь повторяет
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlspace",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Cloud.ru MLSpace: training jobs, inference, notebooks, data transfer, registry and compute catalog — an MCP server plus skills.",
5
5
  "author": {
6
6
  "name": "Cloud.ru"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlspace",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Cloud.ru MLSpace: training jobs, inference, notebooks, data transfer, registry and compute catalog — an MCP server plus skills.",
5
5
  "author": {
6
6
  "name": "Cloud.ru"
@@ -5,7 +5,7 @@
5
5
  "command": "uvx",
6
6
  "args": [
7
7
  "--from",
8
- "${CLAUDE_PLUGIN_ROOT}/server/mlspace_plugin-0.2.1-py3-none-any.whl",
8
+ "${CLAUDE_PLUGIN_ROOT}/server/mlspace_plugin-0.2.2-py3-none-any.whl",
9
9
  "mlspace-plugin",
10
10
  "--transport",
11
11
  "stdio"
@@ -7,9 +7,9 @@
7
7
 
8
8
  ## Установка из архива релиза
9
9
 
10
- Скачайте `mlspace-plugin-0.2.1.zip` из опубликованного релиза проекта.
10
+ Скачайте `mlspace-plugin-0.2.2.zip` из опубликованного релиза проекта.
11
11
  Распакуйте его в постоянный каталог, например `~/plugins`, и перейдите в
12
- `mlspace-plugin-0.2.1`. Сохраняйте каталог: локальный marketplace нужен для обновлений.
12
+ `mlspace-plugin-0.2.2`. Сохраняйте каталог: локальный marketplace нужен для обновлений.
13
13
 
14
14
  Требования: `uv` (`uvx`) в PATH, Python 3.10+ и установленный клиент агента.
15
15
  При первом запуске `uv` устанавливает зависимости из настроенного Python index;
@@ -19,7 +19,7 @@
19
19
  Настройте ключи сервисной учётной записи и выберите доступные workspace:
20
20
 
21
21
  ```sh
22
- uvx --from ./plugins/mlspace/server/mlspace_plugin-0.2.1-py3-none-any.whl mlspace-plugin setup --config-only
22
+ uvx --from ./plugins/mlspace/server/mlspace_plugin-0.2.2-py3-none-any.whl mlspace-plugin setup --config-only
23
23
  ```
24
24
 
25
25
  Ключи вводятся в терминале. `setup` сохраняет их вне плагина в пользовательском
@@ -62,7 +62,7 @@ claude plugin install mlspace@mlspace
62
62
  Для MCP-only клиента задайте команду `uvx` и аргументы:
63
63
 
64
64
  ```json
65
- ["--from", "/absolute/path/plugins/mlspace/server/mlspace_plugin-0.2.1-py3-none-any.whl", "mlspace-plugin", "--transport", "stdio"]
65
+ ["--from", "/absolute/path/plugins/mlspace/server/mlspace_plugin-0.2.2-py3-none-any.whl", "mlspace-plugin", "--transport", "stdio"]
66
66
  ```
67
67
 
68
68
  `SKILL.md` можно подключать отдельно в клиентах с поддержкой Agent Skills.
@@ -71,7 +71,7 @@ claude plugin install mlspace@mlspace
71
71
  ## Состав архива
72
72
 
73
73
  ```text
74
- mlspace-plugin-0.2.1/
74
+ mlspace-plugin-0.2.2/
75
75
  ├── .agents/plugins/marketplace.json
76
76
  ├── .claude-plugin/marketplace.json
77
77
  ├── README.md
@@ -82,7 +82,7 @@ mlspace-plugin-0.2.1/
82
82
  ├── .codex-plugin/plugin.json
83
83
  ├── .claude-plugin/plugin.json
84
84
  ├── .mcp.json
85
- ├── server/mlspace_plugin-0.2.1-py3-none-any.whl
85
+ ├── server/mlspace_plugin-0.2.2-py3-none-any.whl
86
86
  └── skills/<12 скиллов>/SKILL.md
87
87
  ```
88
88
 
@@ -103,8 +103,8 @@ Portable MCP допускает отдельный `MLSPACE_ENV_FILE=${PLUGIN_DA
103
103
 
104
104
  ```sh
105
105
  python scripts/gen_plugin.py --check
106
- uv build --out-dir dist/release-0.2.1
107
- python scripts/build_plugin.py --wheel dist/release-0.2.1/mlspace_plugin-0.2.1-py3-none-any.whl
106
+ uv build --out-dir dist/release-0.2.2
107
+ python scripts/build_plugin.py --wheel dist/release-0.2.2/mlspace_plugin-0.2.2-py3-none-any.whl
108
108
  ```
109
109
 
110
110
  Готовая поставка и ZIP появятся в `dist/plugins/`.
@@ -6,7 +6,7 @@
6
6
  "command": "uvx",
7
7
  "args": [
8
8
  "--from",
9
- "${PLUGIN_ROOT}/server/mlspace_plugin-0.2.1-py3-none-any.whl",
9
+ "${PLUGIN_ROOT}/server/mlspace_plugin-0.2.2-py3-none-any.whl",
10
10
  "mlspace-plugin",
11
11
  "--transport",
12
12
  "stdio"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "mlspace",
4
- "version": "0.2.1",
4
+ "version": "0.2.2",
5
5
  "description": "Cloud.ru MLSpace: training jobs, inference, notebooks, data transfer, registry and compute catalog — an MCP server plus skills.",
6
6
  "author": {
7
7
  "name": "Cloud.ru"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mlspace-plugin"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "Cloud.ru MLSpace plugin: MCP server, agent skills and setup"
5
5
  keywords = ["mlspace", "mcp", "agent-skills", "cloudru"]
6
6
  classifiers = [
@@ -1,3 +1,3 @@
1
1
  """mlspace_mcp — MCP server for the Cloud.ru MLSpace public API v2."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.2.2"
@@ -106,10 +106,41 @@ def _read_config(path: Path) -> dict[str, Any]:
106
106
  return load_jsonc(path.read_text())
107
107
 
108
108
 
109
+ def managed_clients(overrides: dict[str, str] | None = None) -> dict[str, str]:
110
+ """Find surviving managed registrations so one product updates together."""
111
+ state_path = user_config_file().parent / 'install-state.json'
112
+ state = json.loads(state_path.read_text()) if state_path.exists() else {}
113
+ detected = detect_clients()
114
+ result = {}
115
+ for client in state.get('clients', {}):
116
+ if client not in CLIENTS:
117
+ continue
118
+ config = _config(client)
119
+ previous_path = Path(state.get('configs', {}).get(client, str(config)))
120
+ key = {'claude-code': 'mcpServers', 'codex': 'mcp_servers', 'opencode': 'mcp'}[client]
121
+ if 'mlspace' not in _read_config(previous_path).get(key, {}):
122
+ continue # The user removed this connection; never recreate it.
123
+ if previous_path.absolute() != config.absolute():
124
+ variable = {'claude-code': 'CLAUDE_CONFIG_DIR', 'codex': 'CODEX_HOME',
125
+ 'opencode': 'OPENCODE_CONFIG'}[client]
126
+ raise ValueError(f'Previously connected {client} uses {previous_path}. '
127
+ f'Restore its {variable} setting before updating all MLSpace clients.')
128
+ candidates = [(overrides or {}).get(client), state.get('executables', {}).get(client),
129
+ detected.get(client)]
130
+ executable = next((path for path in candidates if path and Path(path).is_file()
131
+ and os.access(path, os.X_OK)), None)
132
+ if not executable and client != 'opencode':
133
+ raise ValueError(f'Previously connected {client} executable is unavailable. Restore it or use '
134
+ f'--client-path {client}=/absolute/path before updating MLSpace.')
135
+ result[client] = executable or CLIENTS[client] # OpenCode config is written directly.
136
+ return result
137
+
138
+
109
139
  def prepare_clients(clients: dict[str, str], env_file: Path, command: list[str]) -> dict[str, Any]:
110
140
  """Plan all writes and reject conflicts before setup saves credentials."""
111
141
  state_path = user_config_file().parent / 'install-state.json'
112
142
  state = json.loads(state_path.read_text()) if state_path.exists() else {}
143
+ clients = managed_clients(clients) | clients
113
144
  sources = _skill_sources()
114
145
  writes: dict[Path, str] = {}
115
146
  registrations = []
@@ -201,6 +232,7 @@ def install_clients(clients: dict[str, str], env_file: Path, command: list[str])
201
232
  subprocess.run(remove, env=clean_env, check=True,
202
233
  capture_output=True, timeout=30)
203
234
  if comparable != entry:
235
+ config.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
204
236
  arguments = ([executable, 'mcp', 'add', 'mlspace', '--scope', 'user', '--transport', 'stdio',
205
237
  '--env', 'MLSPACE_ENV_FILE=' + str(env_file.absolute())]
206
238
  if client == 'claude-code' else
@@ -216,6 +248,8 @@ def install_clients(clients: dict[str, str], env_file: Path, command: list[str])
216
248
  if comparable_saved != entry:
217
249
  raise ValueError(f'Client registration could not be confirmed for {client}; retry setup.')
218
250
  state.setdefault('clients', {})[client] = _hash(json.dumps(saved, sort_keys=True))
251
+ state.setdefault('configs', {})[client] = str(config.absolute())
252
+ state.setdefault('executables', {})[client] = executable
219
253
  atomic_write(state_path, json.dumps(state, indent=2))
220
254
  installed.append(client)
221
255
  except (subprocess.SubprocessError, OSError) as exc:
@@ -20,6 +20,7 @@ from .client_setup import (
20
20
  atomic_write,
21
21
  detect_clients,
22
22
  install_clients,
23
+ managed_clients,
23
24
  prepare_clients,
24
25
  server_command,
25
26
  )
@@ -166,16 +167,26 @@ def _verify(values: dict[str, str], *, base_url: str) -> str | None:
166
167
 
167
168
  def _clients(requested: list[str], paths: list[str], non_interactive: bool) -> dict[str, str]:
168
169
  detected = detect_clients()
170
+ explicit = {}
169
171
  for item in paths:
170
172
  name, separator, path = item.partition('=')
171
173
  executable = Path(path).expanduser().absolute()
172
174
  if not separator or name not in CLIENTS or not executable.is_file() or not os.access(executable, os.X_OK):
173
175
  raise ValueError('Use --client-path CLIENT=/absolute/path/to/executable.')
174
176
  detected[name] = str(executable)
177
+ explicit[name] = str(executable)
178
+ managed = managed_clients(explicit)
179
+ detected = managed | detected
175
180
  if requested:
176
181
  if any(name not in detected for name in requested):
177
182
  raise ValueError('Requested client not found. Install it or use --client-path CLIENT=/path.')
178
183
  return {name: detected[name] for name in requested}
184
+ if managed:
185
+ print('Updating all previously connected MLSpace clients: ' + ', '.join(managed), file=sys.stderr)
186
+ return managed | explicit
187
+ if not explicit and (user_config_file().parent / 'install-state.json').exists():
188
+ raise ValueError('No previous MLSpace connections remain. Select a new connection explicitly '
189
+ 'with --client or --client-path.')
179
190
  if len(detected) == 1:
180
191
  print('Client detected: ' + next(iter(detected)), file=sys.stderr)
181
192
  return detected
@@ -216,7 +227,7 @@ def run_init(path: Path | None = None, *, force: bool = False, verify: bool = Tr
216
227
  print('\nSetup cancelled; previous credentials preserved unless saving already completed.', file=sys.stderr)
217
228
  return 130
218
229
  except EOFError:
219
- print('\nNo interactive input. Run `mlspace-plugin setup` yourself in a terminal (SSH works). '
230
+ print('\nNo interactive input. Run `uvx mlspace-plugin@latest setup` yourself in a terminal (SSH works). '
220
231
  'Agents: use INSTALL_AGENT.md; never ask for keys in chat.', file=sys.stderr)
221
232
  return 1
222
233
  except (ValueError, OSError, httpx.HTTPError) as exc:
@@ -234,6 +245,10 @@ def _run_init(path: Path | None, **options: Any) -> int:
234
245
  raise EOFError
235
246
  if options['from_env'] and options['credentials_file']:
236
247
  raise ValueError('Choose either --from-env or --credentials-file.')
248
+ if target.is_symlink():
249
+ raise ValueError('Credentials target must not be a symlink.')
250
+ if target.exists() and not target.is_file():
251
+ raise ValueError('Credentials target must be a regular file.')
237
252
  if target.exists() and not options['force'] and not listing:
238
253
  raise ValueError('Configuration already exists. Use --force to reconfigure; saved keys will be reused.')
239
254
  if not options['verify'] and (not options['config_only'] or not options['workspace_ids']):
@@ -252,7 +267,9 @@ def _run_init(path: Path | None, **options: Any) -> int:
252
267
  options['clients'], options['client_paths'], non_interactive)
253
268
  command = server_command() if selected_clients else []
254
269
  if selected_clients:
255
- prepare_clients(selected_clients, target, command)
270
+ plan = prepare_clients(selected_clients, target, command)
271
+ print('MCP and skills will be kept together for: ' +
272
+ ', '.join(row[0] for row in plan['registrations']), file=sys.stderr)
256
273
  if not listing:
257
274
  print('[1/4] Access keys. Paste values only; asterisks hide both fields. '
258
275
  'Keys stay in your user config, not in chat.', file=sys.stderr)
@@ -340,7 +357,14 @@ def _run_init(path: Path | None, **options: Any) -> int:
340
357
  if selected_clients:
341
358
  print('[4/4] Connecting MCP and installing skills…', file=sys.stderr)
342
359
  installed = install_clients(selected_clients, target, command)
343
- _write(target, values)
360
+ try:
361
+ _write(target, values)
362
+ except (OSError, ValueError) as exc:
363
+ if installed:
364
+ raise ValueError(f'MCP and skills installed: {", ".join(installed)}, but credentials could not be saved '
365
+ f'to {target}. Previous credentials were not replaced. Check this path and its '
366
+ 'permissions, then rerun setup --force before using the MCP.') from exc
367
+ raise
344
368
  print(f'Credentials saved: {target} (0600).', file=sys.stderr)
345
369
  if not options['verify']:
346
370
  print('UNVERIFIED configuration saved by explicit request. No client was connected.', file=sys.stderr)
@@ -103,7 +103,6 @@ def test_installed_native_cli_registration_in_isolated_home(tmp_path, monkeypatc
103
103
  monkeypatch.setenv('XDG_CONFIG_HOME', str(tmp_path / '.config'))
104
104
  monkeypatch.setenv('CLAUDE_CONFIG_DIR', str(tmp_path / '.claude'))
105
105
  monkeypatch.setenv('CODEX_HOME', str(tmp_path / '.codex'))
106
- (tmp_path / '.codex').mkdir()
107
106
  args = ({client: executable}, tmp_path / 'fake.env', ['/bin/echo', '--transport', 'stdio'])
108
107
  install_clients(*args)
109
108
  data = _read_config(_config(client))
@@ -231,3 +230,67 @@ def test_local_wheel_in_uvx_cache_is_not_substituted_with_pypi(tmp_path, monkeyp
231
230
  monkeypatch.setattr('shutil.which', lambda name: '/opt/bin/uvx' if name == 'uvx' else None)
232
231
  with pytest.raises(ValueError, match='local wheel'):
233
232
  client_setup.server_command()
233
+
234
+
235
+ def test_shared_skill_update_includes_previously_connected_clients(tmp_path, monkeypatch):
236
+ from mlspace_mcp import client_setup
237
+ monkeypatch.setenv('HOME', str(tmp_path))
238
+ monkeypatch.setenv('XDG_CONFIG_HOME', str(tmp_path / '.config'))
239
+ monkeypatch.setenv('CODEX_HOME', str(tmp_path / '.codex'))
240
+ monkeypatch.setattr(client_setup, '_skill_sources', lambda: {'mlspace-example': 'version one'})
241
+ client_setup.install_clients({'opencode': '/bin/true'}, tmp_path / '.env', ['runtime-v1'])
242
+ monkeypatch.setattr(client_setup, '_skill_sources', lambda: {'mlspace-example': 'version two'})
243
+ plan = client_setup.prepare_clients({'codex': '/bin/true'}, tmp_path / '.env', ['runtime-v2'])
244
+ registrations = {row[0]: row[3] for row in plan['registrations']}
245
+ assert registrations['opencode']['command'] == ['runtime-v2']
246
+ assert registrations['codex']['command'] == 'runtime-v2'
247
+ assert plan['writes'][tmp_path / '.agents/skills/mlspace-example/SKILL.md'] == 'version two'
248
+
249
+
250
+ def test_update_does_not_recreate_removed_client_registration(tmp_path, monkeypatch):
251
+ from mlspace_mcp import client_setup
252
+ monkeypatch.setenv('HOME', str(tmp_path))
253
+ monkeypatch.setenv('XDG_CONFIG_HOME', str(tmp_path / '.config'))
254
+ monkeypatch.setenv('CODEX_HOME', str(tmp_path / '.codex'))
255
+ client_setup.install_clients({'opencode': '/bin/true'}, tmp_path / '.env', ['runtime-v1'])
256
+ (tmp_path / '.config/opencode/opencode.json').write_text('{}')
257
+ plan = client_setup.prepare_clients({'codex': '/bin/true'}, tmp_path / '.env', ['runtime-v2'])
258
+ assert [row[0] for row in plan['registrations']] == ['codex']
259
+
260
+
261
+ def test_modified_previously_connected_client_blocks_shared_update(tmp_path, monkeypatch):
262
+ from mlspace_mcp import client_setup
263
+ monkeypatch.setenv('HOME', str(tmp_path))
264
+ monkeypatch.setenv('XDG_CONFIG_HOME', str(tmp_path / '.config'))
265
+ monkeypatch.setenv('CODEX_HOME', str(tmp_path / '.codex'))
266
+ client_setup.install_clients({'opencode': '/bin/true'}, tmp_path / '.env', ['runtime-v1'])
267
+ config = tmp_path / '.config/opencode/opencode.json'
268
+ config.write_text(config.read_text().replace('runtime-v1', 'user-edited'))
269
+ with pytest.raises(ValueError, match='was not overwritten'):
270
+ client_setup.prepare_clients({'codex': '/bin/true'}, tmp_path / '.env', ['runtime-v2'])
271
+
272
+
273
+ def test_custom_client_config_cannot_be_silently_forgotten(tmp_path, monkeypatch):
274
+ from mlspace_mcp import client_setup
275
+ monkeypatch.setenv('HOME', str(tmp_path))
276
+ monkeypatch.setenv('XDG_CONFIG_HOME', str(tmp_path / '.config'))
277
+ monkeypatch.setenv('OPENCODE_CONFIG', str(tmp_path / 'custom.json'))
278
+ client_setup.install_clients({'opencode': '/bin/true'}, tmp_path / '.env', ['runtime-v1'])
279
+ monkeypatch.delenv('OPENCODE_CONFIG')
280
+ with pytest.raises(ValueError, match='OPENCODE_CONFIG'):
281
+ client_setup.prepare_clients({'opencode': '/bin/true'}, tmp_path / '.env', ['runtime-v2'])
282
+ assert not (tmp_path / '.config/opencode/opencode.json').exists()
283
+
284
+
285
+ def test_missing_managed_native_cli_blocks_before_new_registration(tmp_path, monkeypatch):
286
+ from mlspace_mcp import client_setup
287
+ monkeypatch.setenv('HOME', str(tmp_path))
288
+ monkeypatch.setenv('XDG_CONFIG_HOME', str(tmp_path / '.config'))
289
+ monkeypatch.setenv('CODEX_HOME', str(tmp_path / '.codex'))
290
+ monkeypatch.setattr(client_setup, 'detect_clients', lambda: {})
291
+ client_setup.atomic_write(tmp_path / '.codex/config.toml', '[mcp_servers.mlspace]\ncommand="old"\n')
292
+ client_setup.atomic_write(tmp_path / '.config/mlspace-plugin/install-state.json',
293
+ json.dumps({'clients': {'codex': 'old'}}))
294
+ with pytest.raises(ValueError, match='codex executable is unavailable'):
295
+ client_setup.install_clients({'opencode': '/bin/true'}, tmp_path / '.env', ['runtime-v2'])
296
+ assert not (tmp_path / '.config/opencode/opencode.json').exists()
@@ -163,7 +163,7 @@ def test_client_install_failure_preserves_previous_credentials(tmp_path, monkeyp
163
163
  target.write_text(original)
164
164
  monkeypatch.setattr('mlspace_mcp.init_cli._clients', lambda *a: {'opencode': '/bin/true'})
165
165
  monkeypatch.setattr('mlspace_mcp.init_cli.server_command', lambda: ['/bin/echo'])
166
- monkeypatch.setattr('mlspace_mcp.init_cli.prepare_clients', lambda *a: {})
166
+ monkeypatch.setattr('mlspace_mcp.init_cli.prepare_clients', lambda *a: {'registrations': [('opencode',)]})
167
167
  def fail(*a, **kw):
168
168
  raise ValueError('Client installation failed')
169
169
  monkeypatch.setattr('mlspace_mcp.init_cli.install_clients', fail)
@@ -208,3 +208,66 @@ def test_human_init_connects_detected_client_and_skills_after_two_masked_prompts
208
208
  output = capsys.readouterr()
209
209
  assert 'human-secret' not in output.err + output.out
210
210
  assert 'human-id' not in output.err + output.out
211
+
212
+
213
+ @pytest.mark.parametrize('kind', ['symlink', 'directory'])
214
+ @respx.mock
215
+ def test_invalid_credentials_target_fails_before_client_setup(tmp_path, monkeypatch, capsys, kind):
216
+ target = tmp_path / 'invalid.env'
217
+ if kind == 'symlink':
218
+ target.symlink_to(tmp_path / 'missing.env')
219
+ else:
220
+ target.mkdir()
221
+ calls = []
222
+ monkeypatch.setattr('mlspace_mcp.init_cli._clients', lambda *a: calls.append('clients') or {})
223
+ assert run_init(target, force=True, non_interactive=True, from_env=True) == 1
224
+ assert calls == []
225
+ assert 'Credentials target' in capsys.readouterr().err
226
+
227
+
228
+ @respx.mock
229
+ def test_final_save_failure_reports_connected_clients(tmp_path, monkeypatch, capsys):
230
+ routes()
231
+ target = tmp_path / '.env'
232
+ original = 'MLSPACE_READONLY=true\n'
233
+ target.write_text(original)
234
+ monkeypatch.setattr('mlspace_mcp.init_cli._clients', lambda *a: {'opencode': '/bin/true'})
235
+ monkeypatch.setattr('mlspace_mcp.init_cli.server_command', lambda: ['/bin/echo'])
236
+ def fail_save(*a):
237
+ raise PermissionError('sensitive detail should not be printed')
238
+ monkeypatch.setattr('mlspace_mcp.init_cli._write', fail_save)
239
+ assert run_init(target, force=True, base_url=BASE, non_interactive=True,
240
+ from_env=True, workspace_ids=['ws2']) == 1
241
+ assert target.read_text() == original
242
+ assert (tmp_path / '.config/opencode/opencode.json').exists()
243
+ error = capsys.readouterr().err
244
+ assert 'MCP and skills installed: opencode' in error
245
+ assert 'credentials could not be saved' in error
246
+ assert str(target) in error and '--force' in error
247
+ assert 'sensitive detail' not in error
248
+
249
+
250
+ @respx.mock
251
+ def test_existing_opencode_can_update_without_cli_or_client_selection(tmp_path, monkeypatch):
252
+ from mlspace_mcp import client_setup
253
+ routes()
254
+ target = tmp_path / '.env'
255
+ client_setup.install_clients({'opencode': '/missing/opencode'}, target, ['runtime-v1'])
256
+ monkeypatch.setattr(client_setup, 'detect_clients', lambda: {})
257
+ monkeypatch.setattr('mlspace_mcp.init_cli.detect_clients', lambda: {})
258
+ monkeypatch.setattr('mlspace_mcp.init_cli.server_command', lambda: ['runtime-v2'])
259
+ assert run_init(target, force=True, base_url=BASE, non_interactive=True,
260
+ from_env=True, workspace_ids=['ws2']) == 0
261
+ config = json.loads((tmp_path / '.config/opencode/opencode.json').read_text())
262
+ assert config['mcp']['mlspace']['command'] == ['runtime-v2']
263
+
264
+
265
+ def test_deleted_connections_are_not_automatically_recreated(tmp_path, monkeypatch):
266
+ from mlspace_mcp.client_setup import install_clients
267
+ from mlspace_mcp.init_cli import _clients
268
+ install_clients({'opencode': '/bin/true'}, tmp_path / '.env', ['runtime-v1'])
269
+ (tmp_path / '.config/opencode/opencode.json').write_text('{}')
270
+ monkeypatch.setattr('mlspace_mcp.init_cli.detect_clients', lambda: {'opencode': '/bin/true'})
271
+ with pytest.raises(ValueError, match='Select a new connection explicitly'):
272
+ _clients([], [], True)
273
+ assert _clients(['opencode'], [], True) == {'opencode': '/bin/true'}
@@ -1,53 +0,0 @@
1
- # Python
2
- __pycache__/
3
- *.py[cod]
4
- *.egg-info/
5
- .eggs/
6
- build/
7
- dist/
8
- .venv/
9
- venv/
10
-
11
- # Tooling
12
- .pytest_cache/
13
- .mypy_cache/
14
- .ruff_cache/
15
- .coverage
16
- htmlcov/
17
-
18
- # Env / secrets
19
- .env
20
- .env.*
21
- !.env.example
22
- *.secret
23
-
24
- # OS
25
- .DS_Store
26
-
27
- # Local audit harnesses & artifacts (not part of the shipped product)
28
- scripts/write_audit.py
29
- scripts/api_audit.py
30
- scripts/coverage_probe.py
31
- docs/_*.json
32
- docs/_*.png
33
- _planning/api_evidence.txt
34
- _planning/api_critique_and_prompts.json
35
-
36
- # Agent worktrees (local scratch, never part of the product)
37
- .claude/
38
- .superpowers/
39
-
40
- # Internal audit & planning artefacts (not part of the published server)
41
- audit/
42
- after_audit/
43
- _planning/
44
- docs/proposals/
45
- docs/superpowers/
46
- docs/DESIGN.md
47
- docs/MLSPACE_API_*.md
48
- docs/LLM_FRIENDLINESS_AUDIT.md
49
- docs/E2E_TEST_REPORT.md
50
- docs/EXEC_SUMMARY.md
51
- docs/article_*.md
52
- docs/slides*.md
53
- docs/_*.json
File without changes
File without changes