vaultlayer 0.1.76__tar.gz → 0.1.78__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 (562) hide show
  1. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/PKG-INFO +1 -1
  2. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/pyproject.toml +1 -1
  3. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/startup_scripts.py +70 -4
  4. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/tester/agent.py +1 -1
  5. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/email.py +13 -6
  6. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/job_routes.py +105 -1
  7. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/_preflight.py +180 -8
  8. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/_remote.py +17 -11
  9. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/connect.py +4 -4
  10. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/data.py +3 -3
  11. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/estimate.py +1 -1
  12. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/examples.py +8 -8
  13. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/gpus.py +1 -1
  14. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/inject.py +25 -5
  15. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/main.py +18 -17
  16. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/notify.py +1 -1
  17. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/ps.py +1 -1
  18. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/run.py +8 -2
  19. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/signup.py +1 -1
  20. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/sync.py +6 -6
  21. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/vaultlayer_data.json +3 -3
  22. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/vaultlayer/_resume_state.py +158 -30
  23. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/vaultlayer/_resume_torch.py +52 -14
  24. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/vaultlayer.egg-info/PKG-INFO +1 -1
  25. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/vaultlayer.egg-info/SOURCES.txt +1 -0
  26. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_api_coverage.py +19 -2
  27. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_audit_gaps.py +14 -3
  28. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_docs_command_parity.py +1 -1
  29. vaultlayer-0.1.78/tests/test_emergency_save_guards.py +244 -0
  30. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_inject_coverage.py +38 -0
  31. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_preflight_safety.py +273 -0
  32. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_remote_command_contract.py +51 -0
  33. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_resume_hook.py +75 -0
  34. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_startup_scripts.py +81 -2
  35. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/README.md +0 -0
  36. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/setup.cfg +0 -0
  37. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/__init__.py +0 -0
  38. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/__init__.py +0 -0
  39. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/agent.py +0 -0
  40. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/billing.py +0 -0
  41. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/gang.py +0 -0
  42. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/migration.py +0 -0
  43. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/provider_startup.py +0 -0
  44. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/providers/__init__.py +0 -0
  45. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/providers/aws.py +0 -0
  46. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/providers/aws_capacity.py +0 -0
  47. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/providers/azure.py +0 -0
  48. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/providers/base.py +0 -0
  49. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/providers/coreweave.py +0 -0
  50. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/providers/crusoe.py +0 -0
  51. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/providers/gcp.py +0 -0
  52. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/providers/gcp_capacity.py +0 -0
  53. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/providers/hyperstack.py +0 -0
  54. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/providers/lambda_labs.py +0 -0
  55. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/providers/nebius.py +0 -0
  56. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/providers/region_planner.py +0 -0
  57. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/providers/runpod.py +0 -0
  58. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/providers/runpod_cluster.py +0 -0
  59. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/providers/vast_ai.py +0 -0
  60. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/providers/voltage_park.py +0 -0
  61. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/ssh.py +0 -0
  62. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/broker/warm_pool.py +0 -0
  63. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/finops/__init__.py +0 -0
  64. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/finops/agent.py +0 -0
  65. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/namespace/__init__.py +0 -0
  66. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/namespace/agent.py +0 -0
  67. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/namespace/ingestion.py +0 -0
  68. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/namespace/inject.py +0 -0
  69. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/namespace/mount.py +0 -0
  70. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/namespace/prefetch.py +0 -0
  71. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/orchestration/__init__.py +0 -0
  72. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/orchestration/agent.py +0 -0
  73. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/orchestration/decisions.py +0 -0
  74. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/orchestration/egress.py +0 -0
  75. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/orchestration/prompts.py +0 -0
  76. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/pricing/__init__.py +0 -0
  77. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/pricing/agent.py +0 -0
  78. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/pricing/pollers.py +0 -0
  79. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/tester/__init__.py +0 -0
  80. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/vault/__init__.py +0 -0
  81. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/vault/agent.py +0 -0
  82. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/vault/delta.py +0 -0
  83. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/vault/multipart.py +0 -0
  84. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/vault/r2.py +0 -0
  85. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/watchdog/__init__.py +0 -0
  86. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/watchdog/agent.py +0 -0
  87. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/agents/watchdog/signals.py +0 -0
  88. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/__init__.py +0 -0
  89. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/_errors.py +0 -0
  90. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/admin_dashboard.py +0 -0
  91. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/admin_routes.py +0 -0
  92. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/auth.py +0 -0
  93. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/bill_reconcile.py +0 -0
  94. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/credentials.py +0 -0
  95. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/credits.py +0 -0
  96. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/db.py +0 -0
  97. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/feedback_routes.py +0 -0
  98. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/flags.py +0 -0
  99. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/fraud.py +0 -0
  100. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/gmail_welcome.py +0 -0
  101. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/internal_welcome_routes.py +0 -0
  102. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/invite_routes.py +0 -0
  103. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/job_worker.py +0 -0
  104. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/main.py +0 -0
  105. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/models.py +0 -0
  106. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/org_routes.py +0 -0
  107. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/pricing_routes.py +0 -0
  108. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/smoke_routes.py +0 -0
  109. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/spend_routes.py +0 -0
  110. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/sso.py +0 -0
  111. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/stripe_routes.py +0 -0
  112. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/test_admin_routes.py +0 -0
  113. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/unified_queue.py +0 -0
  114. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/unified_queue_persist.py +0 -0
  115. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/api/user_admin_routes.py +0 -0
  116. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/__init__.py +0 -0
  117. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/_auth_recovery.py +0 -0
  118. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/_dataset.py +0 -0
  119. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/_image_preflight.py +0 -0
  120. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/_pricing.py +0 -0
  121. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/_subprocess.py +0 -0
  122. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/admin_cmd.py +0 -0
  123. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/audit_export.py +0 -0
  124. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/byoc_config_template.py +0 -0
  125. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/checkpoint_template.py +0 -0
  126. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/checkpoint_template_deepspeed.py +0 -0
  127. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/checkpoint_template_jax.py +0 -0
  128. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/crash_reporter.py +0 -0
  129. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/credits_cmd.py +0 -0
  130. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/delete.py +0 -0
  131. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/download.py +0 -0
  132. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/env_check_gen.py +0 -0
  133. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/feedback.py +0 -0
  134. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/login_cmd.py +0 -0
  135. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/migrate_keys.py +0 -0
  136. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/remote_providers.py +0 -0
  137. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/restart.py +0 -0
  138. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/cli/smoke_history.py +0 -0
  139. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/__init__.py +0 -0
  140. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/api_utils.py +0 -0
  141. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/aws_box.py +0 -0
  142. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/bad_hosts.py +0 -0
  143. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/base_image_manifest.py +0 -0
  144. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/bill_auditor.py +0 -0
  145. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/byoc_aws_preflight.py +0 -0
  146. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/byoc_registry.py +0 -0
  147. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/byoc_storage.py +0 -0
  148. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/compute_tiers.py +0 -0
  149. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/config.py +0 -0
  150. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/cred_at_rest.py +0 -0
  151. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/credential_vault.py +0 -0
  152. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/credits.py +0 -0
  153. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/dashboard.py +0 -0
  154. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/data_loader.py +0 -0
  155. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/email.py +0 -0
  156. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/env_spec.py +0 -0
  157. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/env_utils.py +0 -0
  158. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/estimate_calibration.py +0 -0
  159. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/failure_codes.py +0 -0
  160. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/gpu_resolver.py +0 -0
  161. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/gpu_sizing.py +0 -0
  162. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/host_reputation.py +0 -0
  163. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/idempotency.py +0 -0
  164. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/invoice.py +0 -0
  165. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/job_logger.py +0 -0
  166. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/job_run_logger.py +0 -0
  167. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/job_state_store.py +0 -0
  168. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/live_reconcile.py +0 -0
  169. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/log_redaction.py +0 -0
  170. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/logging_utils.py +0 -0
  171. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/low_churn_filter.py +0 -0
  172. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/manifest.py +0 -0
  173. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/models.py +0 -0
  174. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/models_additions.py +0 -0
  175. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/offer_validation.py +0 -0
  176. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/price_guard.py +0 -0
  177. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/provider_allowlist.py +0 -0
  178. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/provider_billing.py +0 -0
  179. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/provider_capacity.py +0 -0
  180. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/provider_health.py +0 -0
  181. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/provider_http_retry.py +0 -0
  182. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/provision_concurrency.py +0 -0
  183. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/queue.py +0 -0
  184. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/quote.py +0 -0
  185. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/r2_credentials.py +0 -0
  186. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/r2_utils.py +0 -0
  187. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/recovery_tiers.py +0 -0
  188. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/retry.py +0 -0
  189. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/run_telemetry.py +0 -0
  190. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/schema_health.py +0 -0
  191. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/script_upload.py +0 -0
  192. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/security.py +0 -0
  193. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/server_audit.py +0 -0
  194. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/sla.py +0 -0
  195. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/spend.py +0 -0
  196. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/storage_billing.py +0 -0
  197. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/sts.py +0 -0
  198. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/suggestions.py +0 -0
  199. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/telemetry.py +0 -0
  200. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/shared/usage_tracker.py +0 -0
  201. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/vaultlayer/__init__.py +0 -0
  202. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/vaultlayer/_resume_hook.py +0 -0
  203. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/vaultlayer.egg-info/dependency_links.txt +0 -0
  204. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/vaultlayer.egg-info/entry_points.txt +0 -0
  205. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/vaultlayer.egg-info/requires.txt +0 -0
  206. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/vaultlayer.egg-info/top_level.txt +0 -0
  207. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/workers/__init__.py +0 -0
  208. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/workers/claude_bug_fixer.py +0 -0
  209. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/src/workers/feedback_worker.py +0 -0
  210. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_admin_cron_cleanup_constant_time.py +0 -0
  211. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_admin_dashboard.py +0 -0
  212. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_admin_invite_cli.py +0 -0
  213. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_admin_mint_user_token.py +0 -0
  214. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_admin_routes.py +0 -0
  215. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_admin_runpod_auth.py +0 -0
  216. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_admin_secret_constant_time.py +0 -0
  217. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_admin_self_auth_cli.py +0 -0
  218. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_admin_user_stats.py +0 -0
  219. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_agents_coverage.py +0 -0
  220. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_agents_misc_coverage.py +0 -0
  221. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_all_migrations_apply.py +0 -0
  222. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_attribution_patch.py +0 -0
  223. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_audit_billing_security.py +0 -0
  224. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_audit_export.py +0 -0
  225. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_audit_p0_leak_fixes.py +0 -0
  226. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_auth_coverage.py +0 -0
  227. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_auth_credits.py +0 -0
  228. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_auth_onboarding.py +0 -0
  229. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_auth_recovery.py +0 -0
  230. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_auth_rollout_gate.py +0 -0
  231. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_auth_user_id_unification.py +0 -0
  232. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_auth_verify_bruteforce.py +0 -0
  233. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_aws_backoff_split.py +0 -0
  234. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_aws_byoc_gang.py +0 -0
  235. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_aws_default_image.py +0 -0
  236. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_aws_dlami_resolver.py +0 -0
  237. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_aws_error_formatting.py +0 -0
  238. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_aws_instance_map_consistency.py +0 -0
  239. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_aws_managed_cred_routing.py +0 -0
  240. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_aws_network_ebs.py +0 -0
  241. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_aws_od_allowlist_gate.py +0 -0
  242. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_aws_orphan_gang_aware.py +0 -0
  243. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_aws_ranker_fallback.py +0 -0
  244. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_aws_residency_lock.py +0 -0
  245. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_aws_spot_interruption_detect.py +0 -0
  246. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_aws_termination_retry.py +0 -0
  247. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_aws_userdata_shim.py +0 -0
  248. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_azure_disk_and_adopt.py +0 -0
  249. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_azure_managed_cred_routing.py +0 -0
  250. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_balance_floor.py +0 -0
  251. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_balance_low_cli_render.py +0 -0
  252. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_balance_low_warning.py +0 -0
  253. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_base_image_cosign_verify.py +0 -0
  254. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_base_image_digest_pin.py +0 -0
  255. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_big_model_sizing.py +0 -0
  256. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_bill_reconcile.py +0 -0
  257. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_billing_estimate_and_rate.py +0 -0
  258. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_billing_freeze_cleared_on_failure.py +0 -0
  259. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_billing_ledger_from_job_runs.py +0 -0
  260. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_billing_ledger_grants.py +0 -0
  261. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_boot_phase_wiring.py +0 -0
  262. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_broker_agent.py +0 -0
  263. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_budget_cap.py +0 -0
  264. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_allowlist_bypass.py +0 -0
  265. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_aws_preflight.py +0 -0
  266. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_billing.py +0 -0
  267. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_broker_isolation.py +0 -0
  268. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_checkpoint_store.py +0 -0
  269. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_config_template.py +0 -0
  270. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_customer_only_logs.py +0 -0
  271. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_customer_orphan_scan.py +0 -0
  272. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_data_gate.py +0 -0
  273. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_dataset_id_block.py +0 -0
  274. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_dormant_path_hygiene.py +0 -0
  275. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_env_file_permissions.py +0 -0
  276. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_gate.py +0 -0
  277. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_nons3_checkpoint_reader.py +0 -0
  278. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_phase2.py +0 -0
  279. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_provider_env_scrub.py +0 -0
  280. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_provider_registry.py +0 -0
  281. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_resume_preserve.py +0 -0
  282. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_run_routing.py +0 -0
  283. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_same_provider_pin.py +0 -0
  284. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_scoped_s3_creds.py +0 -0
  285. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_server_queue.py +0 -0
  286. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_storage_gate.py +0 -0
  287. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_stuck_boot_strand.py +0 -0
  288. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_teardown_creds.py +0 -0
  289. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_terminal_secret_purge.py +0 -0
  290. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_termination_alerts.py +0 -0
  291. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_tier4_recovery.py +0 -0
  292. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_byoc_wallet_backstops.py +0 -0
  293. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_cancel_bridge.py +0 -0
  294. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_canonical_rls_hardening.py +0 -0
  295. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_checkout_landing_pages.py +0 -0
  296. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_checkpoint_dcp_guard.py +0 -0
  297. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_checkpoint_failclosed_nhop.py +0 -0
  298. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_checkpoint_helper.py +0 -0
  299. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_checkpoint_readiness_fence.py +0 -0
  300. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_checkpoint_template_dcp_parity.py +0 -0
  301. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_checkpoint_template_fsdp_guard.py +0 -0
  302. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_cli_arg_ergonomics.py +0 -0
  303. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_cli_coverage.py +0 -0
  304. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_cli_feature_bypass.py +0 -0
  305. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_cli_pinned_provider_quote.py +0 -0
  306. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_cli_run_summary.py +0 -0
  307. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_cli_spend.py +0 -0
  308. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_cli_version.py +0 -0
  309. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_cloud_provider_catalog.py +0 -0
  310. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_cold_start_race.py +0 -0
  311. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_compile_crash_classification.py +0 -0
  312. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_completion_failed_events.py +0 -0
  313. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_config_env_writers.py +0 -0
  314. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_connect_ux.py +0 -0
  315. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_constraint_satisfaction.py +0 -0
  316. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_constraint_satisfaction_integration.py +0 -0
  317. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_coreweave_fence_postdelete.py +0 -0
  318. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_coverage_gaps.py +0 -0
  319. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_cred_at_rest.py +0 -0
  320. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_credentials_api.py +0 -0
  321. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_credits_coverage.py +0 -0
  322. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_credits_ownership.py +0 -0
  323. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_credits_ratio_constant.py +0 -0
  324. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_critical_gaps_batch1.py +0 -0
  325. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_ctrlc_confirm.py +0 -0
  326. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_cuda_mismatch_selfheal.py +0 -0
  327. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_dashboard_cmd.py +0 -0
  328. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_dashboard_identity_bridge.py +0 -0
  329. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_dashboard_pin_gate.py +0 -0
  330. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_data_loader_ttl.py +0 -0
  331. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_dataset_namespacing.py +0 -0
  332. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_dataset_purge.py +0 -0
  333. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_dataset_warmup_guard.py +0 -0
  334. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_datasets.py +0 -0
  335. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_db_admin_authz.py +0 -0
  336. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_db_helpers_propagate.py +0 -0
  337. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_dcp_checkpoint.py +0 -0
  338. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_debug_capture.py +0 -0
  339. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_delete_job_routing.py +0 -0
  340. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_diagnose_command.py +0 -0
  341. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_download_byoc.py +0 -0
  342. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_download_placeholder.py +0 -0
  343. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_e2e_aws_down.py +0 -0
  344. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_e2e_smoke.py +0 -0
  345. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_ec4_weight_storm.py +0 -0
  346. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_egress_engine.py +0 -0
  347. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_elastic_gang_restart.py +0 -0
  348. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_env_check.py +0 -0
  349. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_env_check_imports_from.py +0 -0
  350. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_env_check_timeout.py +0 -0
  351. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_env_config_token_mismatch_warning.py +0 -0
  352. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_env_spec.py +0 -0
  353. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_estimate.py +0 -0
  354. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_estimate_calibration.py +0 -0
  355. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_estimate_range.py +0 -0
  356. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_estimate_runtime_calibration.py +0 -0
  357. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_exit_code_routing.py +0 -0
  358. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_expand_consent_durability.py +0 -0
  359. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_failover_at_persistence.py +0 -0
  360. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_failover_credit_gate.py +0 -0
  361. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_failure_attribution.py +0 -0
  362. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_failure_taxonomy.py +0 -0
  363. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_feedback_snapshot_export.py +0 -0
  364. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_feedback_system.py +0 -0
  365. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_fence_confirmation.py +0 -0
  366. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_find_checkpoint.py +0 -0
  367. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_finops_agent.py +0 -0
  368. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_first_run_wizard.py +0 -0
  369. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_flat_markup_byoc.py +0 -0
  370. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_flow_abc_coverage.py +0 -0
  371. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_flow_abc_smoke.py +0 -0
  372. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_gainshare.py +0 -0
  373. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_gap27_shard_sha256.py +0 -0
  374. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_gaps_batch2.py +0 -0
  375. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_gcp_cred_routing.py +0 -0
  376. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_gcp_image_and_serial.py +0 -0
  377. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_gcp_local_ssd.py +0 -0
  378. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_gcp_zone_fallback.py +0 -0
  379. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_get_live_price_real.py +0 -0
  380. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_gmail_welcome.py +0 -0
  381. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_gpu_catalog_resolves.py +0 -0
  382. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_gpu_contract.py +0 -0
  383. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_gpu_metrics.py +0 -0
  384. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_gpu_metrics_error_surfacing.py +0 -0
  385. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_gpu_resolver.py +0 -0
  386. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_gpu_sizing.py +0 -0
  387. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_gpus_command.py +0 -0
  388. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_gtm_gaps.py +0 -0
  389. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_handle_provision_result.py +0 -0
  390. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_heartbeat_field.py +0 -0
  391. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_hf_cache_retention.py +0 -0
  392. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_hf_mirror.py +0 -0
  393. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_hf_model_cache.py +0 -0
  394. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_home_isolation_guard.py +0 -0
  395. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_host_quality_and_blacklist.py +0 -0
  396. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_host_reputation.py +0 -0
  397. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_idem3_vault_orphan_gc.py +0 -0
  398. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_idempotency.py +0 -0
  399. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_included_providers.py +0 -0
  400. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_init_login_signup.py +0 -0
  401. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_instance_lost_completion.py +0 -0
  402. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_integration.py +0 -0
  403. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_internal_checkout_bridge.py +0 -0
  404. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_internal_welcome_bridge.py +0 -0
  405. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_invite_tokens.py +0 -0
  406. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_invite_update.py +0 -0
  407. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_is_test_account.py +0 -0
  408. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_job_contract.py +0 -0
  409. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_job_logs_cursor_pagination.py +0 -0
  410. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_keep_alive.py +0 -0
  411. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_lambda_terminate_timeout.py +0 -0
  412. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_legacy_credit_endpoints_hardening.py +0 -0
  413. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_list_jobs_columns.py +0 -0
  414. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_list_jobs_run_type_filter.py +0 -0
  415. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_liveness_cloud_orphan_framework.py +0 -0
  416. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_log_dedup_final_post.py +0 -0
  417. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_log_flush_retry.py +0 -0
  418. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_log_redaction.py +0 -0
  419. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_logs_keepalive.py +0 -0
  420. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_logs_ux.py +0 -0
  421. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_managed_balance_retry.py +0 -0
  422. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_markup_resolution.py +0 -0
  423. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_matrix_health.py +0 -0
  424. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_migration_cap_nhop.py +0 -0
  425. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_migration_double_start_guard.py +0 -0
  426. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_migration_leg_reset.py +0 -0
  427. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_migration_nxn.py +0 -0
  428. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_migration_ssh_failure_fence.py +0 -0
  429. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_migration_stale_rate_leak.py +0 -0
  430. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_min_cuda_routing.py +0 -0
  431. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_money_display_ledger.py +0 -0
  432. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_multi_node_guard.py +0 -0
  433. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_multinode_aws_hardening.py +0 -0
  434. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_multinode_distributed_mvp.py +0 -0
  435. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_multinode_gate.py +0 -0
  436. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_namespace_agent.py +0 -0
  437. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_nebius_managed_creds.py +0 -0
  438. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_new_account_guards.py +0 -0
  439. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_new_features.py +0 -0
  440. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_nhop_multihop_coverage.py +0 -0
  441. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_no_phantom_columns.py +0 -0
  442. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_node_auth_owner_resolution.py +0 -0
  443. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_ops_ux_cleanup.py +0 -0
  444. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_orchestration_agent.py +0 -0
  445. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_org_accounts.py +0 -0
  446. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_org_rbac_sso.py +0 -0
  447. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_orphan_reservation_release.py +0 -0
  448. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_orphan_scanner.py +0 -0
  449. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_ownership_db_fallback.py +0 -0
  450. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_p2_billing_correctness.py +0 -0
  451. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_p2_credit_safety.py +0 -0
  452. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_password_reset.py +0 -0
  453. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_password_signup.py +0 -0
  454. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_phase_stuck_budgets.py +0 -0
  455. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_pin_auth.py +0 -0
  456. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_placement_metadata.py +0 -0
  457. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_precompile_check.py +0 -0
  458. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_preemption_attribution.py +0 -0
  459. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_preemption_machine_exclusion.py +0 -0
  460. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_prefers_different_provider_routing.py +0 -0
  461. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_prefetch_r2_prefix.py +0 -0
  462. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_preflight_data.py +0 -0
  463. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_pricing_agent.py +0 -0
  464. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_proactive_spot_flush.py +0 -0
  465. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_prod_isolation.py +0 -0
  466. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_provider_billing.py +0 -0
  467. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_provider_error_sanitization.py +0 -0
  468. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_provider_health.py +0 -0
  469. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_provider_matrix.py +0 -0
  470. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_provider_name_leaks.py +0 -0
  471. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_provider_smoke.py +0 -0
  472. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_provider_smoke_prod.py +0 -0
  473. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_provider_startup.py +0 -0
  474. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_provider_testing_state.py +0 -0
  475. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_provision_error_message.py +0 -0
  476. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_provision_lock_renewal.py +0 -0
  477. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_ps_duration.py +0 -0
  478. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_qa_round_cli_ux.py +0 -0
  479. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_queue_coverage.py +0 -0
  480. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_quota_preflight_initial_provision.py +0 -0
  481. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_r2_credentials.py +0 -0
  482. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_rbac.py +0 -0
  483. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_reconcile_backfill.py +0 -0
  484. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_recovery_job_intent.py +0 -0
  485. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_recovery_resilience.py +0 -0
  486. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_referral_reward.py +0 -0
  487. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_region_constraint_enforcement.py +0 -0
  488. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_registry_managed_path.py +0 -0
  489. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_remaining_audit_gaps.py +0 -0
  490. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_remote_managed_poll.py +0 -0
  491. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_remote_ux_quick_wins.py +0 -0
  492. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_requirements_template.py +0 -0
  493. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_reservation_floor_1h.py +0 -0
  494. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_reservation_settle_balance.py +0 -0
  495. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_reset_pin_cli.py +0 -0
  496. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_restart_balance_errors.py +0 -0
  497. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_restart_cancelled_flow.py +0 -0
  498. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_restart_cli.py +0 -0
  499. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_restart_recovery.py +0 -0
  500. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_resume_alias.py +0 -0
  501. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_resume_chain_credscope.py +0 -0
  502. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_resume_checkpoint_threading.py +0 -0
  503. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_resume_failfast_nhop.py +0 -0
  504. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_resume_leg_reservation_and_budget.py +0 -0
  505. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_rexeed_ux.py +0 -0
  506. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_rotate_token.py +0 -0
  507. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_run_command_leaked_flag.py +0 -0
  508. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_run_data_flag.py +0 -0
  509. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_run_health_rpc.py +0 -0
  510. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_run_managed_default.py +0 -0
  511. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_run_telemetry.py +0 -0
  512. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_scenarios.py +0 -0
  513. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_schema_health.py +0 -0
  514. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_script_upload.py +0 -0
  515. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_security.py +0 -0
  516. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_security_hardening.py +0 -0
  517. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_server_audit.py +0 -0
  518. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_server_lifecycle.py +0 -0
  519. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_settle_idempotent.py +0 -0
  520. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_settle_nhop_atomicity.py +0 -0
  521. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_setup_script.py +0 -0
  522. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_shared_coverage.py +0 -0
  523. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_shared_utils.py +0 -0
  524. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_signup_account_rpc.py +0 -0
  525. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_signup_decay.py +0 -0
  526. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_sla_breach.py +0 -0
  527. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_sla_latency.py +0 -0
  528. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_spend_attribution_propagation.py +0 -0
  529. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_spend_cap_atomic.py +0 -0
  530. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_spend_endpoint_rls.py +0 -0
  531. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_spend_id_fallback.py +0 -0
  532. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_spend_surenoobster_e2e.py +0 -0
  533. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_ssh_resume_checkpoint_resolution.py +0 -0
  534. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_start_job_reservation_flows.py +0 -0
  535. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_status_secret_leak.py +0 -0
  536. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_stop_terminate_verify.py +0 -0
  537. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_storage_billing_coverage.py +0 -0
  538. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_storage_cred_dryrun.py +0 -0
  539. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_sts_concurrent_cache.py +0 -0
  540. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_stuck_leg_billing.py +0 -0
  541. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_stuck_watchdog.py +0 -0
  542. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_submit_reservation.py +0 -0
  543. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_submit_reservation_gate.py +0 -0
  544. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_submit_run_new_account_guards.py +0 -0
  545. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_sync_cloud_mirror.py +0 -0
  546. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_sync_s3_mirror.py +0 -0
  547. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_technical_cracks.py +0 -0
  548. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_telemetry.py +0 -0
  549. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_telemetry_reconcile.py +0 -0
  550. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_termination_guarantee_hardening.py +0 -0
  551. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_test_admin_routes.py +0 -0
  552. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_test_bypass_gates.py +0 -0
  553. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_unified_queue.py +0 -0
  554. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_unified_queue_hardening.py +0 -0
  555. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_unified_queue_persist.py +0 -0
  556. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_v0_gaps.py +0 -0
  557. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_vast_error_redaction.py +0 -0
  558. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_vast_onstart_limit.py +0 -0
  559. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_vault_agent.py +0 -0
  560. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_verification_code_rpc.py +0 -0
  561. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_watchdog_agent.py +0 -0
  562. {vaultlayer-0.1.76 → vaultlayer-0.1.78}/tests/test_watchdog_runpod_termination.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vaultlayer
3
- Version: 0.1.76
3
+ Version: 0.1.78
4
4
  Summary: AI compute arbitrage CLI — move GPU training jobs between clouds automatically
5
5
  Author: VaultLayer
6
6
  License: MIT
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "vaultlayer"
7
7
 
8
- version = "0.1.76"
8
+ version = "0.1.78"
9
9
 
10
10
  description = "AI compute arbitrage CLI — move GPU training jobs between clouds automatically"
11
11
  readme = "README.md"
@@ -1082,11 +1082,77 @@ def _checkpoint_helper_lines(job: "Job", dest_dir_expr: str) -> list[str]:
1082
1082
 
1083
1083
  _FRAMEWORK_TRAINER_IMPORTS = ("transformers", "trl", "pytorch_lightning", "lightning")
1084
1084
 
1085
+ # Launchers that start an HF/TRL/Lightning Trainer in a SUBPROCESS. The trainer
1086
+ # import never appears in the script we scan (`axolotl train cfg.yml` reaches
1087
+ # transformers via `accelerate launch` -> `python -m axolotl.cli.train`), so
1088
+ # detected_imports and the inline-script scan both miss it, no hook is shipped,
1089
+ # and resume silently restarts at step 0 with checkpoints sitting in R2. The
1090
+ # import scan alone is only correct when the user's script IS the trainer.
1091
+ _FRAMEWORK_TRAINER_LAUNCHERS = frozenset({
1092
+ "axolotl", "accelerate", "deepspeed", "torchrun", "llamafactory-cli",
1093
+ })
1094
+
1095
+ # `-m` modules whose ROOT package is too broad to match on. `torchrun` is a thin
1096
+ # console-script over `torch.distributed.run`, and both spellings are common, but
1097
+ # matching the root "torch" would ship the hook for every `python -m torch.*`
1098
+ # (e.g. torch.utils.bottleneck). Matched in FULL, never by root.
1099
+ _FRAMEWORK_TRAINER_LAUNCH_MODULES = frozenset({
1100
+ "torch.distributed.run",
1101
+ "torch.distributed.launch", # deprecated alias, still widely used
1102
+ })
1103
+
1104
+
1105
+ def _command_uses_framework_launcher(command: str) -> bool:
1106
+ """True if ``command`` invokes a known subprocess trainer launcher.
1107
+
1108
+ Matches on the executable BASENAME (so ``/usr/local/bin/accelerate`` counts),
1109
+ on the ROOT of a ``-m`` module (``python -m axolotl.cli.train`` -> axolotl),
1110
+ and on a FULL ``-m`` module path for launchers whose root is too broad to
1111
+ match (``python -m torch.distributed.run``). Flags are skipped, so
1112
+ ``python train.py --deepspeed ds.json`` does NOT match on the flag, and a
1113
+ script merely NAMED after a launcher (``python axolotl_test.py``) does not
1114
+ match either -- both would be false positives that ship the ~20KB hook to
1115
+ jobs that cannot use it.
1116
+ """
1117
+ # Guard non-str BEFORE shlex: shlex.split() treats a non-str as a file-like
1118
+ # stream, reads chars off it, and dies deep inside on `nextchar in whitespace`
1119
+ # (TypeError, not ValueError). job.command is "" on older rows and a Mock in
1120
+ # tests that build scripts from stubbed jobs.
1121
+ if not isinstance(command, str) or not command:
1122
+ return False
1123
+ try:
1124
+ tokens = shlex.split(command)
1125
+ except ValueError: # unbalanced quotes -- degrade, never raise
1126
+ tokens = command.split()
1127
+ expect_module = False
1128
+ for tok in tokens:
1129
+ if expect_module:
1130
+ expect_module = False
1131
+ if tok in _FRAMEWORK_TRAINER_LAUNCH_MODULES:
1132
+ return True
1133
+ if tok.split(".", 1)[0] in _FRAMEWORK_TRAINER_LAUNCHERS:
1134
+ return True
1135
+ continue
1136
+ if tok == "-m":
1137
+ expect_module = True
1138
+ continue
1139
+ if tok.startswith("-"):
1140
+ continue
1141
+ # Node paths are posix; rsplit keeps this correct when the broker runs
1142
+ # on Windows (os.path.basename would not split "/" there).
1143
+ name = tok.replace("\\", "/").rsplit("/", 1)[-1]
1144
+ if name in _FRAMEWORK_TRAINER_LAUNCHERS:
1145
+ return True
1146
+ return False
1147
+
1085
1148
 
1086
1149
  def _job_uses_framework_trainer(job: "Job") -> bool:
1087
- """True if the job's script uses an HF/TRL/Lightning Trainer (so it needs the
1088
- resume hook materialized). Driven by runtime_spec.detected_imports; falls back
1089
- to an inline-script scan when no runtime_spec is present."""
1150
+ """True if the job runs an HF/TRL/Lightning Trainer (so it needs the resume
1151
+ hook materialized). Checks the launcher command FIRST -- a subprocess launcher
1152
+ imports the trainer out of view of both scans below -- then falls back to
1153
+ runtime_spec.detected_imports, then to an inline-script scan."""
1154
+ if _command_uses_framework_launcher(getattr(job, "command", "") or ""):
1155
+ return True
1090
1156
  spec = _extract_runtime_spec(job)
1091
1157
  if spec is not None:
1092
1158
  detected = set(spec.detected_imports or [])
@@ -2851,7 +2917,7 @@ def build_vm_startup_script(broker, job: "Job", *, launch_provider: str = "") ->
2851
2917
  "touch $_VL_LOG",
2852
2918
  "echo '[VL] VM bootstrap started' | tee -a $_VL_LOG",
2853
2919
  # Ubuntu 24.04 ships only python3; ensure 'python' resolves so users can
2854
- # run `vaultlayer run python train.py` without getting exit code 127.
2920
+ # run `vaultlayer run train.py` without getting exit code 127.
2855
2921
  "python --version >/dev/null 2>&1 || "
2856
2922
  "ln -sf \"$(command -v python3)\" /usr/local/bin/python 2>/dev/null || true",
2857
2923
  ]
@@ -10,7 +10,7 @@ PROBLEM_STATEMENT = """
10
10
  VaultLayer wraps AI training commands and provides:
11
11
  1. Cost savings: 60-70% cheaper than AWS On-Demand by routing to Lambda Labs/CoreWeave
12
12
  2. Reliability: 99.9% job completion — detect Spot terminations, checkpoint, auto-resume
13
- 3. Zero code changes: user just runs 'vaultlayer run python train.py'
13
+ 3. Zero code changes: user just runs 'vaultlayer run train.py'
14
14
  4. Autonomous: an Orchestration Agent (Claude-powered) makes all decisions
15
15
  5. Checkpoints: stored in Cloudflare R2, restorable on any provider
16
16
  """
@@ -81,7 +81,7 @@ def _build_welcome_bodies() -> tuple[str, str, str]:
81
81
  f" Add them at {dash}/dashboard/billing (or run `vl credits buy`).\n"
82
82
  "\n"
83
83
  "5. Run your first job\n"
84
- " vl run python train.py\n"
84
+ " vl run train.py\n"
85
85
  "\n"
86
86
  f"Open {dash}/dashboard to watch your jobs, GPUs, and balance.\n"
87
87
  "\n"
@@ -112,9 +112,9 @@ def _build_welcome_bodies() -> tuple[str, str, str]:
112
112
  <a href="{dash}/dashboard/billing">billing</a> page (the CLI equivalent is
113
113
  <code>vl credits buy</code>).</p>
114
114
 
115
- <p><strong>5. Run your first job</strong> — just put <code>vl run</code> in
116
- front of your normal training command:</p>
117
- <pre style="{_code}">vl run python train.py</pre>
115
+ <p><strong>5. Run your first job</strong> — just point <code>vl run</code>
116
+ at your training script:</p>
117
+ <pre style="{_code}">vl run train.py</pre>
118
118
 
119
119
  <p><a href="{dash}/dashboard">Open your dashboard</a> to watch your jobs,
120
120
  GPUs, and balance.</p>
@@ -285,7 +285,14 @@ async def send_topup_confirmation(
285
285
  package: str,
286
286
  ):
287
287
  """Sent after a successful Stripe payment."""
288
- usd_paid = {"starter": 10, "growth": 50, "scale": 200}.get(package, 0)
288
+ # "Amount paid": fixed packages have a set price — and it can't be back-derived
289
+ # from the credit count because package credits INCLUDE a bonus (growth $50→5,500,
290
+ # scale $200→24,000). A custom top-up has no bonus, so its dollars are exactly
291
+ # credits / CREDITS_PER_USD. Deriving from the package NAME alone returned $0 for
292
+ # every 'custom' purchase — i.e. every dashboard buy, the launch path.
293
+ usd_paid = {"starter": 10.0, "growth": 50.0, "scale": 200.0}.get(
294
+ package, credits_added / CREDITS_PER_USD
295
+ )
289
296
  await _send(
290
297
  to=email,
291
298
  subject=f"VaultLayer — {credits_added:,} credits added",
@@ -298,7 +305,7 @@ async def send_topup_confirmation(
298
305
  </tr>
299
306
  <tr>
300
307
  <td style="padding:6px 16px 6px 0;color:#666;">Amount paid</td>
301
- <td style="padding:6px 0;"><strong>${usd_paid}</strong></td>
308
+ <td style="padding:6px 0;"><strong>${usd_paid:,.2f}</strong></td>
302
309
  </tr>
303
310
  <tr>
304
311
  <td style="padding:6px 16px 6px 0;color:#666;">Credits added</td>
@@ -460,7 +460,19 @@ def _resolve_prior_submit_state(job_id: str) -> dict:
460
460
  if result.data:
461
461
  qs = result.data[0].get("queue_state")
462
462
  if isinstance(qs, dict):
463
- return qs
463
+ # Durable queue_state persists credential material encrypted at rest:
464
+ # `environment` collapses to a single {"__vlenc_env__": "VLENC1:..."}
465
+ # entry, and registry_password to a "VLENC1:" string
466
+ # (shared.cred_at_rest.encrypt_queue_state_secrets, called on the
467
+ # write path). Returning it RAW made the environment inherit adopt
468
+ # that sentinel as if it were the user's env -- reproducing the exact
469
+ # resume failure the inherit exists to prevent, but only for
470
+ # DB-backed ancestors, i.e. after an API restart when _active_jobs no
471
+ # longer holds the prior leg. Idempotent on legacy plaintext rows;
472
+ # fails closed to {} on a decrypt error, which callers already treat
473
+ # as "not set".
474
+ from shared.cred_at_rest import decrypt_queue_state_secrets
475
+ return decrypt_queue_state_secrets(qs)
464
476
  except Exception:
465
477
  pass
466
478
  return {}
@@ -517,6 +529,85 @@ def _coerce_inherited_value(field: str, value):
517
529
  return None
518
530
 
519
531
 
532
+ def _inherit_environment_from_chain(body, resume_chain: list[str]) -> bool:
533
+ """Carry the original leg's `--env` values onto a resume submit.
534
+
535
+ Neither carry layer covered `environment`, so every resume leg started with an
536
+ EMPTY env. A script reading `os.environ.get("MODEL_ID", <default>)` silently
537
+ fell back to its default, loaded a DIFFERENT model, then resumed from the prior
538
+ model's checkpoint -- surfacing as
539
+ `ValueError: loaded state dict contains a parameter group that doesn't match`.
540
+ Two models with compatible param groups would have resumed with NO error at all.
541
+ It also dropped HF_TOKEN (gated models 401 on every resume) and any
542
+ hyperparameters passed via --env.
543
+
544
+ Server-side is the ONLY possible carry path: `environment` is deliberately
545
+ excluded from _SAFE_JOB_RESPONSE_FIELDS (it holds HF_TOKEN), so `vl restart`
546
+ never sees it and cannot resend it. Inheriting here keeps the secret server-side.
547
+
548
+ NOT gated on `inherit_constraints`: `--no-carry` exists to drop stale
549
+ ROUTING/BUDGET constraints. Dropping the environment would strip MODEL_ID and
550
+ HF_TOKEN and break the run outright -- an escape hatch that bricks the job.
551
+
552
+ MERGED per key, NOT all-or-nothing. The original version bailed out whenever the
553
+ body carried ANY environment ("the caller specified one, honour it"), mirroring
554
+ the is_unset predicate _inherit_constraints_from_chain uses for scalars. That was
555
+ wrong, and it made this whole carry a no-op in production: cli/_remote.py scrapes
556
+ the user's SHELL on every submit, including `vl restart` --
557
+
558
+ _FORWARD_PREFIXES = ("VL_USER_", "WANDB_", "HF_", "HUGGING_FACE_")
559
+ _FORWARD_EXACT = {"MAX_STEPS", ..., "MODEL_ID"}
560
+
561
+ -- so a developer with HF_TOKEN exported sends environment={"HF_TOKEN": ...} on a
562
+ restart, the guard short-circuits, and MODEL_ID/MAX_STEPS never carry. Observed
563
+ live: a resumed leg loaded the script's default model and printed
564
+ "save_steps: 10 (from args) != 20 (from trainer_state.json)".
565
+
566
+ body-wins is right for a SCALAR constraint (max_cost_usd); it is wrong for a MAP.
567
+ One unrelated forwarded var must not discard every other key. So: inherited keys
568
+ fill gaps, and any key the body actually sent wins.
569
+ """
570
+ # The key under `environment` that holds the encrypted blob when queue_state was
571
+ # persisted with credentials encrypted at rest. Imported here (not module scope)
572
+ # to keep cred_at_rest off the import path of callers that never resume.
573
+ from shared.cred_at_rest import _ENV_SENTINEL
574
+
575
+ _incoming = getattr(body, "environment", None)
576
+ incoming = dict(_incoming) if isinstance(_incoming, dict) else {}
577
+ for prior in resume_chain:
578
+ state = _resolve_prior_submit_state(prior)
579
+ if state.get("byoc_cleanup_pending"):
580
+ # Its stored env was REPLACED with AWS termination-confirmation creds
581
+ # only (job_worker._byoc_cleanup_queue_state_for_unconfirmed_aws). It is
582
+ # non-empty but carries none of the user's --env, so stopping here would
583
+ # inherit a stripped snapshot instead of walking to a complete ancestor.
584
+ continue
585
+ env = state.get("environment")
586
+ if not isinstance(env, dict) or not env:
587
+ continue
588
+ # Defense in depth. _resolve_prior_submit_state decrypts the DB fallback, so
589
+ # this should not trigger -- but the encrypted shape is str->str and would
590
+ # sail through the sanitizer below, handing the resumed leg
591
+ # {"__vlenc_env__": "VLENC1:..."} instead of MODEL_ID/HF_TOKEN. If decryption
592
+ # is ever unavailable (key rotated away, legacy row), skip the leg and keep
593
+ # walking rather than inherit a sentinel.
594
+ if _ENV_SENTINEL in env:
595
+ continue
596
+ # A legacy or hand-edited queue_state could hold a shape no live submit can
597
+ # produce. Keep str->str only; never 500 on inherited data.
598
+ clean = {k: v for k, v in env.items()
599
+ if isinstance(k, str) and k and isinstance(v, str)}
600
+ if not clean:
601
+ continue
602
+ if not any(k not in incoming for k in clean):
603
+ return False # ancestor found, but it adds nothing new
604
+ # Body wins per key: a var the caller explicitly sent (or that the CLI
605
+ # forwarded from the shell) overrides the stored value for that key only.
606
+ body.environment = {**clean, **incoming}
607
+ return True
608
+ return False
609
+
610
+
520
611
  def _inherit_constraints_from_chain(body, resume_chain: list[str]) -> list[str]:
521
612
  """Fill unset routing/budget constraints on a resume submit from the chain.
522
613
 
@@ -1546,6 +1637,19 @@ async def submit_job(body: RunJobRequest, user: dict = Depends(require_auth)):
1546
1637
  # `vl restart --no-carry` sets inherit_constraints=False to resume with
1547
1638
  # fresh (unconstrained) routing — the escape hatch when a carried
1548
1639
  # constraint no longer fits.
1640
+ #
1641
+ # The environment carry is NOT gated on inherit_constraints: --no-carry drops
1642
+ # stale ROUTING/BUDGET constraints, but dropping the env would strip MODEL_ID
1643
+ # and HF_TOKEN and break the run outright. Without this, a resume leg ran with
1644
+ # an EMPTY env, so a script's `os.environ.get("MODEL_ID", <default>)` silently
1645
+ # loaded a different model and resumed from the prior model's checkpoint.
1646
+ if body.resume_from_job_id and _resume_chain:
1647
+ if _inherit_environment_from_chain(body, _resume_chain):
1648
+ logger.info(
1649
+ "[submit] resume leg merged environment from prior leg(s) %s "
1650
+ "(%d vars total; keys only, never values -- HF_TOKEN lives here)",
1651
+ _resume_chain, len(body.environment),
1652
+ )
1549
1653
  if body.resume_from_job_id and _resume_chain and body.inherit_constraints:
1550
1654
  _inherited_constraints = _inherit_constraints_from_chain(body, _resume_chain)
1551
1655
  if "preferred_providers" in _inherited_constraints:
@@ -210,6 +210,143 @@ _HF_TRAINER_CLASSES = frozenset({"Trainer", "Seq2SeqTrainer"}) | _TRL_TRAINER_CL
210
210
  _TRAINER_MODULES = ("transformers", "trl")
211
211
 
212
212
 
213
+ # Launcher-driven frameworks: the user hands over a config and the launcher starts
214
+ # the real trainer in a SUBPROCESS. There is no user training loop to instrument,
215
+ # so the AST injector correctly refuses -- but it then tells the user to "add the
216
+ # manual 3-line integration", which is impossible: an Axolotl YAML has nowhere to
217
+ # put restore()/checkpoint(). Mirrors the broker-side launcher set that decides
218
+ # whether to ship the resume hook (agents/broker/startup_scripts.py).
219
+ _FRAMEWORK_LAUNCHERS = frozenset({
220
+ "axolotl", "accelerate", "deepspeed", "torchrun", "llamafactory-cli",
221
+ })
222
+
223
+ # Of the launchers above, ONLY these own the training loop themselves: the user
224
+ # supplies a YAML/config and the framework runs (and checkpoints) the Trainer. A
225
+ # zero_code verdict is safe for them because there is no user loop to instrument.
226
+ #
227
+ # torchrun / accelerate / deepspeed are GENERIC process launchers. They own
228
+ # nothing -- `torchrun train.py` may front a raw PyTorch loop that still needs the
229
+ # injector. Classifying those zero_code told the user "checkpointing handled by
230
+ # VaultLayer" and skipped injection, so NO checkpoints were ever written and a
231
+ # preemption lost the whole run. The _has_injectable_loop() guard did not save it:
232
+ # a loop whose optimizer.step() sits in a helper function has no .step()/.backward()
233
+ # in the for-body, so the guard reads False. A generic launcher must fall through to
234
+ # _detect_hf_trl_trainer / Lightning (which already classify `accelerate launch
235
+ # train.py` correctly when train.py uses HF Trainer) and otherwise stay "missing",
236
+ # where the user is at least WARNED that integration is absent.
237
+ #
238
+ # NOTE: the broker's launcher set (agents/broker/startup_scripts.py) deliberately
239
+ # stays wider -- shipping the resume hook to a generic launcher is a harmless
240
+ # superset. The two sets are not interchangeable; do not "consolidate" them.
241
+ _LAUNCHER_OWNS_TRAINING_LOOP = frozenset({"axolotl", "llamafactory-cli"})
242
+ # `-m` modules whose ROOT package is too broad to match on ("torch").
243
+ _FRAMEWORK_LAUNCH_MODULES = {
244
+ "torch.distributed.run": "torchrun",
245
+ "torch.distributed.launch": "torchrun",
246
+ }
247
+ _LAUNCHER_DISPLAY = {
248
+ "axolotl": "Axolotl", "accelerate": "Accelerate", "deepspeed": "DeepSpeed",
249
+ "torchrun": "torchrun", "llamafactory-cli": "LLaMA-Factory",
250
+ }
251
+
252
+
253
+ def _command_launcher_name(command) -> str:
254
+ """Launcher name if ``command`` invokes one, else "". Matches the executable
255
+ basename, a ``-m`` module root, and the full ``-m`` path for torch.distributed.*.
256
+ Flags are skipped, so ``python train.py --deepspeed ds.json`` does not match,
257
+ and a script merely NAMED after a launcher (``python axolotl_test.py``) does not
258
+ either -- it is an argument, not the executable."""
259
+ expect_module = False
260
+ for tok in command or ():
261
+ if not isinstance(tok, str):
262
+ continue
263
+ if expect_module:
264
+ expect_module = False
265
+ if tok in _FRAMEWORK_LAUNCH_MODULES:
266
+ return _FRAMEWORK_LAUNCH_MODULES[tok]
267
+ root = tok.split(".", 1)[0]
268
+ if root in _FRAMEWORK_LAUNCHERS:
269
+ return root
270
+ continue
271
+ if tok == "-m":
272
+ expect_module = True
273
+ continue
274
+ if tok.startswith("-"):
275
+ continue
276
+ name = tok.replace("\\", "/").rsplit("/", 1)[-1]
277
+ if name in _FRAMEWORK_LAUNCHERS:
278
+ return name
279
+ return ""
280
+
281
+
282
+ def _script_subprocess_launcher(src: str) -> str:
283
+ """Launcher name if the script shells out to one (``subprocess.call(["axolotl",
284
+ "train", cfg])``), else "". Covers the wrapper pattern, where the command is a
285
+ plain ``python wrapper.py`` and the launcher only appears at runtime.
286
+
287
+ Delegates the whole literal argv to _command_launcher_name rather than testing
288
+ argv[0]: the launcher is not always the executable. `subprocess.call(["python",
289
+ "-m", "torch.distributed.run", ...])` puts it in the `-m` module, and
290
+ `[sys.executable, "-m", ...]` hides argv[0] behind a non-literal entirely.
291
+ Non-literal elements are dropped, so a `-m` whose module is a variable simply
292
+ does not match.
293
+
294
+ Both subprocess arg shapes are handled: a literal argv list, and the string form
295
+ used with shell=True (`subprocess.run("axolotl train cfg.yml", shell=True)`),
296
+ which is shlex-split first -- otherwise it arrives as one unsplit token whose
297
+ "basename" is the entire command line and can never match a launcher.
298
+ """
299
+ import ast
300
+ import shlex
301
+ try:
302
+ tree = ast.parse(src)
303
+ except SyntaxError:
304
+ return ""
305
+ for node in ast.walk(tree):
306
+ if not isinstance(node, ast.Call) or not node.args:
307
+ continue
308
+ fn = node.func
309
+ if not (isinstance(fn, ast.Attribute) and isinstance(fn.value, ast.Name)
310
+ and fn.value.id == "subprocess"):
311
+ continue
312
+ arg0 = node.args[0]
313
+ if isinstance(arg0, ast.Constant) and isinstance(arg0.value, str):
314
+ try:
315
+ argv = shlex.split(arg0.value)
316
+ except ValueError: # unbalanced quotes -- degrade, never raise
317
+ argv = arg0.value.split()
318
+ else:
319
+ elts = arg0.elts if isinstance(arg0, (ast.List, ast.Tuple)) else [arg0]
320
+ argv = [e.value for e in elts
321
+ if isinstance(e, ast.Constant) and isinstance(e.value, str)]
322
+ name = _command_launcher_name(argv)
323
+ if name:
324
+ return name
325
+ return ""
326
+
327
+
328
+ def _has_injectable_loop(src: str) -> bool:
329
+ """True if the script owns a training loop the AST injector can instrument.
330
+
331
+ Load-bearing guard: `vl run torchrun train.py` names a launcher but train.py may
332
+ be a raw-PyTorch loop that still NEEDS injection. Classifying it zero_code would
333
+ silently strip its checkpointing -- worse than a misleading message.
334
+ """
335
+ import ast
336
+ try:
337
+ tree = ast.parse(src)
338
+ except SyntaxError:
339
+ return False
340
+ for node in ast.walk(tree):
341
+ if not isinstance(node, (ast.For, ast.While)):
342
+ continue
343
+ for sub in ast.walk(node):
344
+ if (isinstance(sub, ast.Call) and isinstance(sub.func, ast.Attribute)
345
+ and sub.func.attr in ("step", "backward")):
346
+ return True
347
+ return False
348
+
349
+
213
350
  def _detect_hf_trl_trainer(src: str) -> "tuple[bool, bool]":
214
351
  """``(uses_trainer, is_trl)`` from AST — REAL Trainer imports/instantiation.
215
352
 
@@ -461,7 +598,16 @@ def check_script_integration(command: list[str]) -> dict:
461
598
  script_path = pathlib.Path(token)
462
599
  break
463
600
 
601
+ # `axolotl train cfg.yml` carries no .py for us to scan, but the framework CLI
602
+ # itself proves a framework owns the loop and its checkpointing. A GENERIC
603
+ # launcher proves nothing (see _LAUNCHER_OWNS_TRAINING_LOOP) -- `torchrun
604
+ # train.py` with an unresolvable path stays "unknown", as it did before.
605
+ _cmd_launcher = _command_launcher_name(command)
606
+
464
607
  if script_path is None or not script_path.exists():
608
+ if _cmd_launcher in _LAUNCHER_OWNS_TRAINING_LOOP:
609
+ result["status"] = "zero_code"
610
+ result["framework"] = _LAUNCHER_DISPLAY.get(_cmd_launcher, _cmd_launcher)
465
611
  return result
466
612
 
467
613
  result["script"] = script_path.name
@@ -532,6 +678,17 @@ def check_script_integration(command: list[str]) -> dict:
532
678
  )
533
679
  return result
534
680
 
681
+ # Framework-CLI driven: either the command IS one, or the script shells out to
682
+ # one. Restricted to _LAUNCHER_OWNS_TRAINING_LOOP -- a generic launcher over a
683
+ # raw PyTorch loop must reach the injector (or its warning), never a silent
684
+ # zero_code. _has_injectable_loop stays as defense in depth: if a script somehow
685
+ # owns a loop AND shells out to a framework CLI, prefer instrumenting the loop.
686
+ _launcher = _cmd_launcher or _script_subprocess_launcher(src)
687
+ if _launcher in _LAUNCHER_OWNS_TRAINING_LOOP and not _has_injectable_loop(src):
688
+ result["status"] = "zero_code"
689
+ result["framework"] = _LAUNCHER_DISPLAY.get(_launcher, _launcher)
690
+ return result
691
+
535
692
  if "import jax" in lines or "import flax" in lines or "from flax" in lines:
536
693
  result["status"] = "missing"
537
694
  result["framework"] = "JAX/Flax"
@@ -754,14 +911,29 @@ def print_preflight(run_cmd: "RunCommand", yes: bool = False, train_mode: str =
754
911
  (G, Y, R, B, BO),
755
912
  )
756
913
  elif _hint:
757
- # Checkpoint saving is automatic; resume requires one line in the script.
758
- _example = integration.get("resume_example") or (
759
- f"os.environ.get('{_hint}')"
760
- )
761
- integ_line = (
762
- f"{G}{_fw} — checkpoints auto-saved ✓{R} "
763
- f"{Y}(for resume: {_example}){R}"
764
- )
914
+ # The framework (HF Trainer / Lightning) already saves checkpoints — VaultLayer
915
+ # syncs them to R2 and, on single-node, auto-resumes from the last one with NO
916
+ # code change (the runtime hook resolves the checkpoint from output_dir even when
917
+ # VAULTLAYER_RESUME_CHECKPOINT is unset). Do NOT tell the user to add a resume
918
+ # line there — the old "(for resume: trainer.train(resume_from_checkpoint=os.
919
+ # environ.get(...)))" hint was a footgun: on a restart/local leg that env var is
920
+ # None, which trained fresh AND suppressed the auto-resume hook. Multi-node does
921
+ # NOT arm the hook (it is _SINGLE_NODE-gated), so there the explicit hand-off IS
922
+ # still required — surface it only in that case.
923
+ _nnodes = int(getattr(run_cmd, "world_nnodes", 1) or 1)
924
+ if _nnodes > 1:
925
+ _example = integration.get("resume_example") or (
926
+ f"os.environ.get('{_hint}')"
927
+ )
928
+ integ_line = (
929
+ f"{G}{_fw} — checkpoints auto-saved ✓{R} "
930
+ f"{Y}(multi-node resume: {_example}){R}"
931
+ )
932
+ else:
933
+ integ_line = (
934
+ f"{G}{_fw} — checkpoints auto-saved ✓{R} "
935
+ f"{Y}(VaultLayer resumes from your last saved checkpoint automatically){R}"
936
+ )
765
937
  else:
766
938
  integ_line = f"{G}{_fw} — zero-code ✓{R}"
767
939
  elif _st == "missing":
@@ -221,13 +221,19 @@ _ALLOWED_REMOTE_MODULE_LAUNCHERS = {
221
221
 
222
222
 
223
223
  def _normalize_bare_python_script_command(command: list[str]) -> list[str]:
224
- """Run bare local Python scripts through the interpreter on remote nodes.
225
-
226
- Users naturally type `vl run train.py`. The GPU node receives the script as
227
- a regular uploaded file, not an executable with a guaranteed shebang/chmod,
228
- so the remote command must be `python train.py`. Explicit launchers such as
229
- `python -u train.py`, `torchrun ... train.py`, and `accelerate launch ...`
230
- are already intentional and must remain unchanged.
224
+ """Run bare local Python scripts through the interpreter.
225
+
226
+ Users naturally type `vl run train.py`. Neither end of the run can execute
227
+ that directly: the GPU node receives the script as a regular uploaded file,
228
+ not an executable with a guaranteed shebang/chmod, and the local-execution
229
+ path spawns argv via create_subprocess_exec. Both need `python train.py`.
230
+ Explicit launchers such as `python -u train.py`, `torchrun ... train.py`,
231
+ and `accelerate launch ...` are already intentional and must remain
232
+ unchanged.
233
+
234
+ Applied at RunCommand construction (cli/run.py), so callers downstream
235
+ already see a normalized command. Idempotent — re-normalizing a command
236
+ that already starts with an interpreter is a no-op.
231
237
  """
232
238
  if not command:
233
239
  return []
@@ -478,11 +484,11 @@ async def run_remote(
478
484
  raise SystemExit(
479
485
  f"vaultlayer run: script file not found: {py_args[0]}\n"
480
486
  "Run from the directory containing the script, or pass the path, "
481
- f"for example: vaultlayer run python path/to/{Path(py_args[0]).name}"
487
+ f"for example: vaultlayer run path/to/{Path(py_args[0]).name}"
482
488
  )
483
489
  raise SystemExit(
484
490
  "vaultlayer run: managed jobs require one local .py training script, "
485
- "for example: vaultlayer run python train.py"
491
+ "for example: vaultlayer run train.py"
486
492
  )
487
493
  _workspace_paths = _local_paths_requiring_workspace(command, script_arg_index)
488
494
  if _workspace_paths:
@@ -1173,8 +1179,8 @@ async def run_remote(
1173
1179
  print(f"\n ⚠ No {gpu_str} found after 15 minutes.")
1174
1180
  print(f" Options:")
1175
1181
  print(f" • Press Ctrl+C to cancel (no charge) and retry with a different GPU:")
1176
- print(f" vaultlayer run --gpu A100_40GB python train.py ...")
1177
- print(f" vaultlayer run --gpu A100_80GB python train.py ...")
1182
+ print(f" vaultlayer run --gpu A100_40GB train.py ...")
1183
+ print(f" vaultlayer run --gpu A100_80GB train.py ...")
1178
1184
  print(f" • Keep waiting — job will start automatically if capacity opens\n")
1179
1185
  _queue_warned = True
1180
1186
  sys.stdout.flush()
@@ -696,7 +696,7 @@ def connect_aws(role_arn, external_id, access_key, secret_key, region):
696
696
  _append_to_config_env(config_env, "AWS_REGION", region or "us-east-1")
697
697
 
698
698
  click.echo(" AWS connected.")
699
- click.echo(" Run: vaultlayer run python train.py")
699
+ click.echo(" Run: vaultlayer run train.py")
700
700
 
701
701
 
702
702
  @connect.command(name="gcp")
@@ -809,7 +809,7 @@ def connect_gcp(service_account_json, project_id, region):
809
809
  _append_to_config_env(config_env, "GCP_REGION", region)
810
810
 
811
811
  click.echo(" GCP connected.")
812
- click.echo(" Run: vaultlayer run python train.py")
812
+ click.echo(" Run: vaultlayer run train.py")
813
813
 
814
814
 
815
815
  @connect.command(name="azure")
@@ -926,7 +926,7 @@ def connect_azure(subscription_id, tenant_id, client_id, client_secret, resource
926
926
  _append_to_config_env(config_env, "AZURE_LOCATION", location)
927
927
 
928
928
  click.echo(" Azure connected.")
929
- click.echo(" Run: vaultlayer run python train.py")
929
+ click.echo(" Run: vaultlayer run train.py")
930
930
 
931
931
 
932
932
  @connect.command(name="storage")
@@ -995,7 +995,7 @@ def connect_storage(provider):
995
995
  _append_to_config_env(config_env, "VL_S3_BUCKET", bucket)
996
996
 
997
997
  click.echo(" S3 storage connected.")
998
- click.echo(" Run: vaultlayer run python train.py")
998
+ click.echo(" Run: vaultlayer run train.py")
999
999
 
1000
1000
  elif provider == "gcs":
1001
1001
  click.echo("")
@@ -94,7 +94,7 @@ def datasets(ctx, delete):
94
94
  Upload a dataset:
95
95
  vaultlayer sync ./data --dataset-id my-dataset
96
96
  Use in training:
97
- vaultlayer run --data r2://my-dataset python train.py
97
+ vaultlayer run --data r2://my-dataset train.py
98
98
  Delete a dataset:
99
99
  vaultlayer datasets delete <dataset-id>
100
100
  """
@@ -228,9 +228,9 @@ def regions_list_all():
228
228
  click.echo(f" {code:<20} {note}")
229
229
 
230
230
  click.echo("\n To restrict your job to specific regions:")
231
- click.echo(" vaultlayer run --regions eu-central-1,eu-west-1 python train.py")
231
+ click.echo(" vaultlayer run --regions eu-central-1,eu-west-1 train.py")
232
232
  click.echo(" To exclude a region:")
233
- click.echo(" vaultlayer run --excluded-regions cn-north-1 python train.py\n")
233
+ click.echo(" vaultlayer run --excluded-regions cn-north-1 train.py\n")
234
234
 
235
235
 
236
236
  @regions.command(name="current")
@@ -405,7 +405,7 @@ def estimate(script, gpu, hours, gpus, data, data_gb, checkpoint_gb, model_param
405
405
  else:
406
406
  click.echo(f" (band spans model sizes — larger models run longer; pass --model-params for a size-specific band)")
407
407
 
408
- click.echo(f" Run: vaultlayer run --gpu {gpu} python train.py")
408
+ click.echo(f" Run: vaultlayer run --gpu {gpu} train.py")
409
409
  click.echo("")
410
410
 
411
411