mlspace-plugin 0.3.3__tar.gz → 0.3.4__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 (136) hide show
  1. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/INSTALL.md +1 -1
  2. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/PKG-INFO +1 -1
  3. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/RELEASE.md +5 -5
  4. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/plugin/.claude-plugin/plugin.json +1 -1
  5. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/plugin/.codex-plugin/plugin.json +1 -1
  6. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/plugin/.mcp.json +1 -1
  7. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/plugin/mcp.json +1 -1
  8. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/plugin/plugin.json +1 -1
  9. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/plugin/skills/mlspace-diagnose-data-transfer/SKILL.md +12 -0
  10. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/pyproject.toml +1 -1
  11. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/__init__.py +1 -1
  12. mlspace_plugin-0.3.4/src/mlspace_mcp/data_transfer_hooks.py +68 -0
  13. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/init_cli.py +20 -17
  14. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/registry.py +5 -0
  15. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/setup_ui.py +18 -0
  16. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/tools/data_transfer.py +10 -2
  17. mlspace_plugin-0.3.4/tests/test_acceptance_regressions.py +177 -0
  18. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/.agents/plugins/marketplace.json +0 -0
  19. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/.claude-plugin/marketplace.json +0 -0
  20. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/.env.example +0 -0
  21. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/.gitignore +0 -0
  22. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/AGENTS.md +0 -0
  23. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/CONTRIBUTING.md +0 -0
  24. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/INSTALL_AGENT.md +0 -0
  25. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/LICENSE +0 -0
  26. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/README.md +0 -0
  27. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/plugin/README.md +0 -0
  28. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/plugin/skills/mlspace-call-deployed-model-predict/SKILL.md +0 -0
  29. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/plugin/skills/mlspace-deploy-inference-service/SKILL.md +0 -0
  30. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/plugin/skills/mlspace-diagnose-failed-training-job/SKILL.md +0 -0
  31. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/plugin/skills/mlspace-diagnose-job-launch-rejection/SKILL.md +0 -0
  32. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/plugin/skills/mlspace-diagnose-unhealthy-inference/SKILL.md +0 -0
  33. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/plugin/skills/mlspace-discover-launchable-resources/SKILL.md +0 -0
  34. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/plugin/skills/mlspace-launch-training-job/SKILL.md +0 -0
  35. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/plugin/skills/mlspace-list-running-jobs/SKILL.md +0 -0
  36. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/plugin/skills/mlspace-resume-notebook-and-set-autoshutdown/SKILL.md +0 -0
  37. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/plugin/skills/mlspace-stage-s3-to-nfs/SKILL.md +0 -0
  38. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/plugin/skills/mlspace-transfer-compute-node/SKILL.md +0 -0
  39. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/scripts/build_plugin.py +0 -0
  40. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/scripts/demo_multi_workspace.py +0 -0
  41. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/scripts/gen_body_schemas.py +0 -0
  42. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/scripts/gen_plugin.py +0 -0
  43. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/scripts/mcp_call.py +0 -0
  44. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/scripts/serve_stdio.sh +0 -0
  45. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/scripts/serve_stdio_dryrun.sh +0 -0
  46. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/__main__.py +0 -0
  47. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/_status.py +0 -0
  48. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/auth.py +0 -0
  49. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/bodyspec.py +0 -0
  50. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/client.py +0 -0
  51. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/client_setup.py +0 -0
  52. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/config.py +0 -0
  53. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/errors.py +0 -0
  54. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/formatting.py +0 -0
  55. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/job_hooks.py +0 -0
  56. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/jsonc.py +0 -0
  57. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/native_plugins.py +0 -0
  58. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/overview.py +0 -0
  59. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/resource_refs.py +0 -0
  60. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/server.py +0 -0
  61. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/setup_conflicts.py +0 -0
  62. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/setup_verification.py +0 -0
  63. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/spec/body_schemas.json +0 -0
  64. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/spec/openapi.json +0 -0
  65. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/tls.py +0 -0
  66. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/tools/__init__.py +0 -0
  67. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/tools/allocations.py +0 -0
  68. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/tools/async_inference.py +0 -0
  69. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/tools/build_image.py +0 -0
  70. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/tools/dalle.py +0 -0
  71. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/tools/docker_registry.py +0 -0
  72. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/tools/inference.py +0 -0
  73. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/tools/jobs.py +0 -0
  74. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/tools/notebooks.py +0 -0
  75. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/tools/queues.py +0 -0
  76. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/tools/resources.py +0 -0
  77. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/tools/tensorboards.py +0 -0
  78. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/tools/workspaces.py +0 -0
  79. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/src/mlspace_mcp/workspace_context.py +0 -0
  80. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/conftest.py +0 -0
  81. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/specutil.py +0 -0
  82. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_allocations.py +0 -0
  83. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_artifact_sync.py +0 -0
  84. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_async_inference.py +0 -0
  85. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_auth.py +0 -0
  86. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_autofill.py +0 -0
  87. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_bodyspec.py +0 -0
  88. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_build_image.py +0 -0
  89. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_client.py +0 -0
  90. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_client_setup.py +0 -0
  91. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_configs_shrink.py +0 -0
  92. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_credential_pointer.py +0 -0
  93. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_dalle.py +0 -0
  94. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_data_transfer.py +0 -0
  95. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_docker_registry.py +0 -0
  96. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_drift.py +0 -0
  97. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_dry_run.py +0 -0
  98. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_dry_run_fidelity.py +0 -0
  99. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_env_file.py +0 -0
  100. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_errors.py +0 -0
  101. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_false_success.py +0 -0
  102. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_formatting.py +0 -0
  103. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_init_cli.py +0 -0
  104. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_integration.py +0 -0
  105. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_job_status.py +0 -0
  106. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_jobs.py +0 -0
  107. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_log_normalisation.py +0 -0
  108. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_mcp_error_contract.py +0 -0
  109. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_native_cli_integration.py +0 -0
  110. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_native_plugins.py +0 -0
  111. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_notebooks.py +0 -0
  112. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_onboarding_init.py +0 -0
  113. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_onboarding_tls.py +0 -0
  114. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_opencode_journey.py +0 -0
  115. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_overview.py +0 -0
  116. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_plugin_bundle.py +0 -0
  117. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_plugin_cli.py +0 -0
  118. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_plugin_release.py +0 -0
  119. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_preflight.py +0 -0
  120. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_queue_scope.py +0 -0
  121. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_queues.py +0 -0
  122. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_registry.py +0 -0
  123. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_resource_refs.py +0 -0
  124. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_resources.py +0 -0
  125. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_route_confusion.py +0 -0
  126. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_secret_disclosure.py +0 -0
  127. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_setup_conflicts.py +0 -0
  128. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_setup_native_journey.py +0 -0
  129. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_setup_ui.py +0 -0
  130. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_setup_verification.py +0 -0
  131. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_skills.py +0 -0
  132. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_tensorboards.py +0 -0
  133. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_user_agent.py +0 -0
  134. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_workspace_context.py +0 -0
  135. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_workspaces.py +0 -0
  136. {mlspace_plugin-0.3.3 → mlspace_plugin-0.3.4}/tests/test_wrapper_hardening.py +0 -0
@@ -21,7 +21,7 @@ mlspace-plugin setup
21
21
  ```
22
22
 
23
23
  Если пакет уже установлен: `uv tool install --force .`.
24
- При установке из wheel: `uv tool install /путь/к/mlspace_plugin-0.3.3-py3-none-any.whl`.
24
+ При установке из wheel: `uv tool install /путь/к/mlspace_plugin-0.3.4-py3-none-any.whl`.
25
25
  Не используйте editable-установку из `/tmp` для постоянной работы.
26
26
  Далее команды показаны для PyPI. После установки исходников или wheel через
27
27
  `uv tool install` заменяйте `uvx mlspace-plugin@latest` на `mlspace-plugin`,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mlspace-plugin
3
- Version: 0.3.3
3
+ Version: 0.3.4
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
@@ -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.3.3 на выпускаемую версию):
10
+ 3. Соберите пакет (в командах ниже замените 0.3.4 на выпускаемую версию):
11
11
 
12
12
  ```sh
13
- uv build --out-dir dist/release-0.3.3
14
- uv run python scripts/build_plugin.py --output dist/plugin-release-0.3.3
13
+ uv build --out-dir dist/release-0.3.4
14
+ uv run python scripts/build_plugin.py --output dist/plugin-release-0.3.4
15
15
  ```
16
16
 
17
17
  4. Просмотрите содержимое wheel, sdist и ZIP. В них не должно быть credentials,
@@ -23,11 +23,11 @@ uv run python scripts/build_plugin.py --output dist/plugin-release-0.3.3
23
23
  `UV_PUBLISH_TOKEN` средствами вашего хранилища секретов, не аргументом команды:
24
24
 
25
25
  ```sh
26
- uv publish --trusted-publishing never dist/release-0.3.3/mlspace_plugin-0.3.3-py3-none-any.whl dist/release-0.3.3/mlspace_plugin-0.3.3.tar.gz
26
+ uv publish --trusted-publishing never dist/release-0.3.4/mlspace_plugin-0.3.4-py3-none-any.whl dist/release-0.3.4/mlspace_plugin-0.3.4.tar.gz
27
27
  ```
28
28
 
29
29
  6. Проверьте версию и SHA256 обоих файлов через PyPI JSON API; проверьте установку
30
- `uvx --from mlspace-plugin==0.3.3 mlspace-plugin --version` из нового кеша.
30
+ `uvx --from mlspace-plugin==0.3.4 mlspace-plugin --version` из нового кеша.
31
31
  7. Только после проверки точной версии в PyPI обновите рабочую ветку GitHub
32
32
  marketplace и создайте тег релиза. До этого проверяйте изменения на отдельной
33
33
  release-ветке или локальном Git marketplace: main не должен ссылаться на
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlspace",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
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.3.3",
3
+ "version": "0.3.4",
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
- "mlspace-plugin==0.3.3",
8
+ "mlspace-plugin==0.3.4",
9
9
  "mlspace-plugin",
10
10
  "--transport",
11
11
  "stdio"
@@ -6,7 +6,7 @@
6
6
  "command": "uvx",
7
7
  "args": [
8
8
  "--from",
9
- "mlspace-plugin==0.3.3",
9
+ "mlspace-plugin==0.3.4",
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.3.3",
4
+ "version": "0.3.4",
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"
@@ -23,6 +23,18 @@ carried out any other way.
23
23
 
24
24
  ## Procedure
25
25
 
26
+ Final-answer contract:
27
+ - **Status and reported counters**: observations returned by the API.
28
+ - **Failure cause and phase**: use explicit matching log evidence; otherwise unknown.
29
+ - **Data impact**: unknown until an independent source/destination contents check
30
+ establishes it. Even a known cause such as AccessDenied at PutObject does not
31
+ establish whether other writes in the run already happened. A known cause and
32
+ unknown data impact can both be true. Zero counters are not that contents check.
33
+
34
+ The tool's `_mcp_evidence` describes the limits of those observations, not an API
35
+ defect or a separate user-facing warning. Never translate zero counters into
36
+ "the run failed before transfer started" or "nothing was read/written".
37
+
26
38
  Goal: determine whether a data transfer truly failed / was partial, and why.
27
39
  1. Resolve the transfer: if transfer_id given use it, else data_transfer list_transfers
28
40
  and match transfer_name.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mlspace-plugin"
3
- version = "0.3.3"
3
+ version = "0.3.4"
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,4 +1,4 @@
1
1
  """mlspace_mcp — MCP server for the Cloud.ru MLSpace public API v2."""
2
2
 
3
- __version__ = "0.3.3"
3
+ __version__ = "0.3.4"
4
4
  USER_AGENT = f"mlspace-plugin/{__version__}"
@@ -0,0 +1,68 @@
1
+ """Evidence boundaries alongside Data Transfer observations, without new API calls."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ _DATA_IMPACT_GUIDANCE = (
7
+ 'In the final diagnosis explicitly state: Data impact is unverified. '
8
+ 'Do not claim that the source was read, nothing was written, or destination '
9
+ 'data is unchanged without an independent contents check. A failed PutObject '
10
+ 'describes that request, not other objects or earlier attempts in the run.'
11
+ )
12
+
13
+
14
+ def _unknown_evidence() -> dict[str, str]:
15
+ return {
16
+ 'failure_cause': 'unknown',
17
+ 'failure_phase': 'unknown',
18
+ 'data_impact': 'unknown',
19
+ 'reporting_requirement': _DATA_IMPACT_GUIDANCE,
20
+ 'basis': 'Counters/empty logs alone; independent evidence may establish more.',
21
+ 'interpretation': (
22
+ 'Reported counters (including zero) and absent logs do not prove that '
23
+ 'the run stopped before reading/writing or left destination data unchanged.'
24
+ ),
25
+ }
26
+
27
+
28
+ def history_evidence(data: Any, values: dict[str, Any]) -> Any:
29
+ """Preserve history shape and counters; failed/cancelled entries lack a diagnosis."""
30
+ if isinstance(data, list):
31
+ return [history_evidence(row, values) for row in data]
32
+ if isinstance(data, dict):
33
+ if isinstance(data.get('data'), list):
34
+ return {**data, 'data': history_evidence(data['data'], values)}
35
+ if data.get('status') in ('failed', 'cancelled'):
36
+ return {**data, '_mcp_evidence': _unknown_evidence()}
37
+ return data
38
+
39
+
40
+ def log_evidence(data: Any, values: dict[str, Any]) -> Any:
41
+ """Keep log evidence intact; individual events are not a destination-data audit."""
42
+ empty = data is None or data == [] or data == {} or data == ''
43
+ recognized = isinstance(data, list)
44
+ if isinstance(data, dict) and data:
45
+ collections = {'items', 'data', 'events', 'logs'}
46
+ metadata = {'count', 'total_count', 'offset', 'limit', 'page', 'page_size', 'status'}
47
+ present = collections & data.keys()
48
+ recognized = any(isinstance(data[key], list) for key in present)
49
+ empty = bool(present) and all(data[key] == [] for key in present) and (
50
+ not (data.keys() - collections - metadata)
51
+ ) and all(not data.get(key) for key in ('count', 'total_count'))
52
+ if empty:
53
+ return {'result': data, '_mcp_evidence': _unknown_evidence()}
54
+ if recognized:
55
+ evidence = {
56
+ 'data_impact': 'unknown',
57
+ 'reporting_requirement': _DATA_IMPACT_GUIDANCE,
58
+ 'basis': 'Logs describe individual events, not a complete source/destination audit.',
59
+ 'interpretation': (
60
+ 'Use explicit log messages to identify cause and failure phase. '
61
+ 'Report data impact separately as unknown until source/destination contents '
62
+ 'are independently checked. Even AccessDenied and all-zero counters do not '
63
+ 'establish that this entire run left destination data unchanged.'
64
+ ),
65
+ }
66
+ result = data if isinstance(data, dict) else {'result': data}
67
+ return {**result, '_mcp_evidence': evidence}
68
+ return data
@@ -9,6 +9,7 @@ import re
9
9
  import stat
10
10
  import sys
11
11
  import tempfile
12
+ from contextlib import nullcontext
12
13
  from pathlib import Path
13
14
  from typing import Any
14
15
  from urllib.parse import urlsplit
@@ -35,7 +36,7 @@ from .config import (
35
36
  user_config_file,
36
37
  )
37
38
  from .setup_conflicts import Resolutions, choose_resolutions
38
- from .setup_ui import choose_workspaces, display, masked_prompt
39
+ from .setup_ui import choose_workspaces, display, hidden_input, masked_prompt
39
40
  from .setup_verification import verify_client
40
41
  from .tls import safe_error as _safe_error
41
42
  from .tls import tls_context
@@ -44,14 +45,15 @@ HEADER = f'# MLSpace credentials, written by mlspace-plugin setup.\n{LITERAL_DOT
44
45
 
45
46
 
46
47
  def _ask(label: str, secret: bool = True, hidden: bool = True) -> str:
47
- while True:
48
- value = masked_prompt(f' {label}: ') if hidden else input(f' {label}: ')
49
- if re.match(r'^\s*(?:export\s+)?MLSPACE_\w+\s*=', value):
50
- print('Вставьте только значение ключа: без MLSPACE_...=, export и кавычек вокруг значения.', file=sys.stderr)
51
- elif value.strip():
52
- return value if secret else value.strip()
53
- else:
54
- print('Поле обязательно. Вставьте значение ключа; звёздочки подтверждают ввод и скрывают его содержимое.', file=sys.stderr)
48
+ with hidden_input() if hidden else nullcontext():
49
+ while True:
50
+ value = masked_prompt(f' {label}: ') if hidden else input(f' {label}: ')
51
+ if re.match(r'^\s*(?:export\s+)?MLSPACE_\w+\s*=', value):
52
+ print('Вставьте только значение ключа: без MLSPACE_...=, export и кавычек вокруг значения.', file=sys.stderr)
53
+ elif value.strip():
54
+ return value if secret else value.strip()
55
+ else:
56
+ print('Поле обязательно. Вставьте значение ключа; звёздочки подтверждают ввод и скрывают его содержимое.', file=sys.stderr)
55
57
 
56
58
 
57
59
  def _resolve_base_url(target: Path, override: str | None) -> str:
@@ -315,14 +317,15 @@ def _run_init(path: Path | None, **options: Any) -> int:
315
317
  else:
316
318
  source = {}
317
319
  keys = {}
318
- for name, label in [('CLIENT_ID', 'Cloud.ru Key ID'), ('CLIENT_SECRET', 'Cloud.ru Key Secret')]:
319
- value = source.get('MLSPACE_' + name)
320
- if not value or not value.strip():
321
- if non_interactive or options['credentials_file'] or options['from_env']:
322
- raise ValueError('Для этого адреса API отсутствуют ключи. Введите их через setup в терминале '
323
- 'либо явно укажите --from-env или --credentials-file.')
324
- value = _ask(label, secret=name == 'CLIENT_SECRET')
325
- keys[name] = value
320
+ with hidden_input():
321
+ for name, label in [('CLIENT_ID', 'Cloud.ru Key ID'), ('CLIENT_SECRET', 'Cloud.ru Key Secret')]:
322
+ value = source.get('MLSPACE_' + name)
323
+ if not value or not value.strip():
324
+ if non_interactive or options['credentials_file'] or options['from_env']:
325
+ raise ValueError('Для этого адреса API отсутствуют ключи. Введите их через setup в терминале '
326
+ 'либо явно укажите --from-env или --credentials-file.')
327
+ value = _ask(label, secret=name == 'CLIENT_SECRET')
328
+ keys[name] = value
326
329
  ca = str(options['ca_file'] or saved.get('MLSPACE_CA_FILE') or os.environ.get('SSL_CERT_FILE') or '')
327
330
  ca = str(Path(ca).expanduser().absolute()) if ca else ''
328
331
  if not listing:
@@ -25,6 +25,7 @@ from pydantic import Field
25
25
 
26
26
  from . import bodyspec, formatting
27
27
  from .config import Settings
28
+ from .data_transfer_hooks import history_evidence, log_evidence
28
29
  from .errors import MLSpaceError
29
30
  from .job_hooks import (
30
31
  guard_job_delete,
@@ -322,6 +323,10 @@ PREFLIGHT[("POST", "/public/v2/jobs")] = preflight_job_run
322
323
  PREFLIGHT[("GET", "/public/v2/jobs")] = preflight_jobs_list
323
324
  GUARDS[("DELETE", "/public/v2/jobs/{job_name}")] = guard_job_delete
324
325
  POSTPROCESS[("DELETE", "/public/v2/jobs/{job_name}")] = postprocess_job_delete
326
+ POSTPROCESS[("GET", "/public/v2/data_transfer/v3/history")] = history_evidence
327
+ POSTPROCESS[("GET", "/public/v2/data_transfer/v3/history/status/stream")] = history_evidence
328
+ POSTPROCESS[("GET", "/public/v2/data_transfer/v2/events/list")] = log_evidence
329
+ POSTPROCESS[("GET", "/public/v2/data_transfer/v3/connectors/{connector_type}/{id_}/try/logs")] = log_evidence
325
330
 
326
331
 
327
332
  def _postprocess_delete_connectors(data: Any, values: dict[str, Any]) -> Any:
@@ -2,10 +2,13 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import sys
5
+ from collections.abc import Iterator
6
+ from contextlib import closing, contextmanager
5
7
  from dataclasses import dataclass, field
6
8
  from typing import Any
7
9
 
8
10
  from prompt_toolkit import Application, PromptSession
11
+ from prompt_toolkit.input import create_input
9
12
  from prompt_toolkit.key_binding import KeyBindings
10
13
  from prompt_toolkit.layout import HSplit, Layout, Window
11
14
  from prompt_toolkit.layout.controls import FormattedTextControl
@@ -24,6 +27,21 @@ def masked_prompt(label: str, **kwargs: Any) -> str:
24
27
  **kwargs).prompt(label))
25
28
 
26
29
 
30
+ @contextmanager
31
+ def hidden_input() -> Iterator[None]:
32
+ """Keep type-ahead hidden between credential prompts, restoring the TTY on exit.
33
+
34
+ PromptSession restores its caller's terminal mode after each prompt. Keep an
35
+ outer raw mode across validation/retries and both credentials so it restores
36
+ to a non-echoing mode, not the shell's echoing mode. Nested scopes are safe.
37
+ """
38
+ if not sys.stdin.isatty():
39
+ yield
40
+ return
41
+ with closing(create_input()) as terminal, terminal.raw_mode():
42
+ yield
43
+
44
+
27
45
  @dataclass
28
46
  class WorkspaceSelection:
29
47
  rows: list[dict[str, str]]
@@ -184,6 +184,9 @@ ACTIONS: dict[str, Op] = {
184
184
  required=("offset", "limit"),
185
185
  kind="list",
186
186
  help="Get transfer event logs (needs offset+limit and either history_id or transfer_id). "
187
+ "In a diagnosis, report cause and data impact separately. Explicitly state "
188
+ "that data impact is unverified unless source/destination contents were checked; "
189
+ "AccessDenied or zero counters cannot prove that nothing was written. "
187
190
  "If empty, cross-check run status and connector logs; do not infer success "
188
191
  "or a specific cause from absent events. Rerunning changes state; never "
189
192
  "recommend a rerun as a read-only diagnostic check.",
@@ -271,8 +274,13 @@ DOMAIN = DomainTool(
271
274
  name="mlspace_data_transfer",
272
275
  title="MLSpace Data Transfer",
273
276
  summary=(
274
- "Manage MLSpace data-transfer connectors, transfers, and migration history "
275
- "(create/update/try connectors, run/cancel/rerun transfers, inspect logs). "
277
+ "Inspect connectors, transfers, run history and logs; manage transfers and user connectors. "
278
+ "Capability boundary: creating connectors is supported ONLY for postgresql, mssql, mysql, "
279
+ "clickhouse, oracledb, s3amazon, s3custom, s3google. Existing system connectors "
280
+ "(NFS, S3 MLSpace/Evolution/DataHub, HDFS) can be inspected and used by transfers, "
281
+ "but this tool cannot create them. Do not describe source discovery as creation support. "
282
+ "For a service overview, briefly describe connectors, transfer configurations, and "
283
+ "run history/logs. Diagnostic caveats belong in an actual investigation. "
276
284
  "For failed-run diagnosis, use the mlspace-diagnose-data-transfer skill when available."
277
285
  ),
278
286
  actions=ACTIONS,
@@ -0,0 +1,177 @@
1
+ """User-visible regressions found by the OpenCode acceptance journey."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import select
7
+ import subprocess
8
+ import sys
9
+ import time
10
+
11
+ import httpx
12
+ import pytest
13
+ import respx
14
+ from conftest import AUTH_URL, BASE_URL, make_settings, runtime_ctx
15
+
16
+ from mlspace_mcp.registry import _build_handler, available_actions
17
+ from mlspace_mcp.tools.data_transfer import DOMAIN
18
+
19
+
20
+ @pytest.mark.parametrize('invalid', [b'\r', b'MLSPACE_CLIENT_SECRET=wrong\r'])
21
+ @pytest.mark.parametrize('cancel', [False, True])
22
+ def test_early_secret_input_is_masked_during_retry_and_echo_is_restored(invalid, cancel):
23
+ master, slave = os.openpty()
24
+ code = '''
25
+ import termios
26
+ from mlspace_mcp.init_cli import _ask
27
+ before = termios.tcgetattr(0)
28
+ try:
29
+ assert _ask('Secret') == 'SYNTHETIC-EARLY-PASTE'
30
+ print('ACCEPTED')
31
+ except KeyboardInterrupt:
32
+ print('CANCELLED')
33
+ finally:
34
+ after = termios.tcgetattr(0)
35
+ # macOS sets PENDIN itself when canonical input is restored.
36
+ before[3] &= ~getattr(termios, 'PENDIN', 0)
37
+ after[3] &= ~getattr(termios, 'PENDIN', 0)
38
+ assert after == before
39
+ print('TTY_RESTORED')
40
+ '''
41
+ process = subprocess.Popen([sys.executable, '-c', code], stdin=slave, stdout=slave,
42
+ stderr=slave, env=dict(os.environ, TERM='xterm',
43
+ PROMPT_TOOLKIT_NO_CPR='1'))
44
+ os.close(slave)
45
+ transcript = b''
46
+ stage = 0
47
+ try:
48
+ deadline = time.monotonic() + 12
49
+ while time.monotonic() < deadline:
50
+ if select.select([master], [], [], 0.05)[0]:
51
+ try:
52
+ chunk = os.read(master, 65536)
53
+ except OSError:
54
+ break
55
+ transcript += chunk
56
+ if stage == 0 and b'Secret:' in transcript:
57
+ os.write(master, invalid)
58
+ stage = 1
59
+ if stage == 1 and any(x.encode() in transcript for x in
60
+ ('Поле обязательно', 'Вставьте только значение')):
61
+ # Type ahead immediately, before the next PromptSession is ready.
62
+ os.write(master, b'\x03' if cancel else
63
+ b'\x1b[200~SYNTHETIC-EARLY-PASTE\x1b[201~\r')
64
+ stage = 2
65
+ if process.poll() is not None:
66
+ break
67
+ assert process.wait(timeout=2) == 0, transcript.decode(errors="replace")
68
+ assert b'TTY_RESTORED' in transcript
69
+ assert (b'CANCELLED' if cancel else b'ACCEPTED') in transcript
70
+ assert b'SYNTHETIC-EARLY-PASTE' not in transcript
71
+ finally:
72
+ if process.poll() is None:
73
+ process.kill()
74
+ process.wait()
75
+ os.close(master)
76
+
77
+
78
+ @pytest.mark.parametrize('action,body,kwargs', [
79
+ ('list_history', {'data': [{'history_id': 'h', 'status': 'failed', 'size_bytes': 0,
80
+ 'rows_count': 0, 'progress': 0}], 'total_count': 1}, {}),
81
+ ('list_history', {'data': [{'history_id': 'h', 'status': 'failed', 'size_bytes': 512}],
82
+ 'total_count': 1}, {}),
83
+ ('get_history_status', [{'history_id': 'h', 'status': 'cancelled'}], {'ids': ['h']}),
84
+ ])
85
+ async def test_failed_run_returns_explicit_evidence_limits_without_changing_observations(action, body, kwargs):
86
+ handler = _build_handler(DOMAIN, available_actions(DOMAIN, True), make_settings())
87
+ with respx.mock as mock:
88
+ mock.post(AUTH_URL).respond(200, json={'token': {'access_token': 't'}})
89
+ mock.get(BASE_URL + DOMAIN.actions[action].path).respond(200, json=body)
90
+ async with httpx.AsyncClient() as http:
91
+ result = json.loads(await handler(action=action, ctx=runtime_ctx(http), **kwargs))
92
+ original_row = body['data'][0] if isinstance(body, dict) else body[0]
93
+ row = result['data'][0] if isinstance(result, dict) else result[0]
94
+ assert {k: row[k] for k in original_row} == original_row
95
+ assert row['_mcp_evidence']['data_impact'] == 'unknown'
96
+ assert row['_mcp_evidence']['failure_phase'] == 'unknown'
97
+ assert 'Counters/empty logs alone' in row['_mcp_evidence']['basis']
98
+ if isinstance(body, dict):
99
+ assert result['total_count'] == body['total_count']
100
+
101
+
102
+ @pytest.mark.parametrize('action,kwargs', [
103
+ ('get_event_logs', {'history_id': 'h', 'offset': 0, 'limit': 20}),
104
+ ('get_connector_logs', {'connector_type': 's3custom', 'id_': 'c'}),
105
+ ])
106
+ async def test_empty_diagnostic_logs_do_not_imply_no_data_changes(action, kwargs):
107
+ handler = _build_handler(DOMAIN, available_actions(DOMAIN, True), make_settings())
108
+ with respx.mock as mock:
109
+ mock.post(AUTH_URL).respond(200, json={'token': {'access_token': 't'}})
110
+ mock.get(url__regex=BASE_URL + '/public/v2/data_transfer/.*').respond(200, json=[])
111
+ async with httpx.AsyncClient() as http:
112
+ result = json.loads(await handler(action=action, ctx=runtime_ctx(http), **kwargs))
113
+ assert result['result'] == []
114
+ assert result['_mcp_evidence']['data_impact'] == 'unknown'
115
+ assert result['_mcp_evidence']['failure_phase'] == 'unknown'
116
+
117
+
118
+ @pytest.mark.parametrize('payload', [
119
+ {'data': [], 'total_count': 0},
120
+ {'data': [{'history_id': 'ok', 'status': 'success', 'size_bytes': 42}], 'total_count': 1},
121
+ {'unexpected': [{'status': 'failed'}]},
122
+ 'unexpected upstream text',
123
+ ])
124
+ def test_history_evidence_leaves_unrelated_shapes_and_success_unchanged(payload):
125
+ from copy import deepcopy
126
+
127
+ from mlspace_mcp.data_transfer_hooks import history_evidence
128
+ before = deepcopy(payload)
129
+ assert history_evidence(payload, {}) == before
130
+ assert payload == before
131
+
132
+
133
+ @pytest.mark.parametrize('payload', [
134
+ {'items': [{'message': 'Access denied'}], 'count': 1},
135
+ {'items': [], 'error_message': 'Access denied'},
136
+ {'items': [], 'total_count': 4},
137
+ 'Access denied',
138
+ ['Access denied'],
139
+ {'status': 'failed', 'logs': ['Access denied']},
140
+ ])
141
+ def test_log_evidence_preserves_real_errors_and_nonempty_or_unknown_shapes(payload):
142
+ from mlspace_mcp.data_transfer_hooks import log_evidence
143
+ result = log_evidence(payload, {})
144
+ if isinstance(payload, dict):
145
+ assert {key: result[key] for key in payload} == payload
146
+ assert result["_mcp_evidence"]["data_impact"] == "unknown"
147
+ assert "failure_cause" not in result["_mcp_evidence"]
148
+ elif isinstance(payload, list):
149
+ assert result["result"] == payload
150
+ assert result["_mcp_evidence"]["data_impact"] == "unknown"
151
+ else:
152
+ assert result == payload
153
+
154
+
155
+ @pytest.mark.parametrize('payload', [None, '', {}, {'items': [], 'count': 0},
156
+ {'status': 'failed', 'logs': []}])
157
+ def test_log_evidence_handles_other_empty_shapes(payload):
158
+ from mlspace_mcp.data_transfer_hooks import log_evidence
159
+ result = log_evidence(payload, {})
160
+ assert result['result'] == payload
161
+ assert result['_mcp_evidence']['data_impact'] == 'unknown'
162
+
163
+
164
+ async def test_known_error_log_preserves_cause_but_does_not_certify_data_impact():
165
+ body = {'items': [{'last_log': 'Destination returned AccessDenied during PutObject.'}], 'count': 1}
166
+ handler = _build_handler(DOMAIN, available_actions(DOMAIN, True), make_settings())
167
+ with respx.mock as mock:
168
+ mock.post(AUTH_URL).respond(200, json={'token': {'access_token': 't'}})
169
+ mock.get(BASE_URL + DOMAIN.actions['get_event_logs'].path).respond(200, json=body)
170
+ async with httpx.AsyncClient() as http:
171
+ result = json.loads(await handler(action='get_event_logs', history_id='h', offset=0,
172
+ limit=20, ctx=runtime_ctx(http)))
173
+ assert result['items'] == body['items']
174
+ assert result['count'] == 1
175
+ assert result['_mcp_evidence']['data_impact'] == 'unknown'
176
+ assert 'failure_cause' not in result['_mcp_evidence']
177
+ assert 'failure_phase' not in result['_mcp_evidence']
File without changes
File without changes
File without changes