letify 2.8.6__tar.gz → 2.8.7__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 (224) hide show
  1. letify-2.8.7/.github/workflows/publish-crates.yml +89 -0
  2. letify-2.8.7/.github/workflows/reserve-crates.yml +77 -0
  3. {letify-2.8.6 → letify-2.8.7}/.gitignore +2 -1
  4. {letify-2.8.6 → letify-2.8.7}/CLAUDE.md +2 -2
  5. {letify-2.8.6 → letify-2.8.7}/PKG-INFO +6 -9
  6. {letify-2.8.6 → letify-2.8.7}/PROJECT.md +4 -1
  7. {letify-2.8.6 → letify-2.8.7}/README.md +5 -8
  8. {letify-2.8.6 → letify-2.8.7}/docs/COMPONENT.md +4 -4
  9. {letify-2.8.6 → letify-2.8.7}/docs/NETWORK.md +1 -21
  10. {letify-2.8.6 → letify-2.8.7}/docs/SPEC.md +15 -10
  11. {letify-2.8.6 → letify-2.8.7}/docs/guide/03-execution-modes.md +1 -1
  12. {letify-2.8.6 → letify-2.8.7}/docs/guide/06-cost.md +1 -1
  13. {letify-2.8.6 → letify-2.8.7}/docs/guide/07-troubleshooting.md +1 -1
  14. {letify-2.8.6 → letify-2.8.7}/docs/guide/08-releasing.md +13 -2
  15. {letify-2.8.6 → letify-2.8.7}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md +1 -1
  16. {letify-2.8.6 → letify-2.8.7}/docs/guide/ko/06-/353/271/204/354/232/251.md +1 -1
  17. {letify-2.8.6 → letify-2.8.7}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md +1 -1
  18. {letify-2.8.6 → letify-2.8.7}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md +13 -2
  19. {letify-2.8.6 → letify-2.8.7}/docs/locales/README_ko.md +4 -7
  20. {letify-2.8.6 → letify-2.8.7}/letify/__init__.py +1 -1
  21. {letify-2.8.6 → letify-2.8.7}/letify/cli.py +0 -37
  22. letify-2.8.7/letify/remoting/__init__.py +22 -0
  23. {letify-2.8.6 → letify-2.8.7}/letify-core/Cargo.lock +3 -28
  24. letify-2.8.7/letify-core/Cargo.toml +14 -0
  25. letify-2.8.7/letify-core/build.py +75 -0
  26. letify-2.8.7/letify-core/letify/Cargo.toml +14 -0
  27. letify-2.8.7/letify-core/letify/README.md +21 -0
  28. letify-2.8.7/letify-core/letify/src/lib.rs +21 -0
  29. {letify-2.8.6 → letify-2.8.7}/letify-core/letify-quic/Cargo.toml +1 -0
  30. letify-2.8.7/letify-ext/LICENSE +201 -0
  31. {letify-2.8.6 → letify-2.8.7}/letify-ext/package-lock.json +2 -2
  32. {letify-2.8.6 → letify-2.8.7}/letify-ext/package.json +1 -1
  33. {letify-2.8.6 → letify-2.8.7}/pyproject.toml +2 -2
  34. {letify-2.8.6 → letify-2.8.7}/scripts/version.py +1 -1
  35. {letify-2.8.6 → letify-2.8.7}/tests/test_cli.py +3 -17
  36. {letify-2.8.6 → letify-2.8.7}/tests/test_cli_output.py +0 -14
  37. {letify-2.8.6 → letify-2.8.7}/tests/test_providers.py +0 -1
  38. letify-2.8.7/tests/test_remoting.py +37 -0
  39. {letify-2.8.6 → letify-2.8.7}/tests/test_version.py +25 -0
  40. letify-2.8.6/letify/remoting/__init__.py +0 -28
  41. letify-2.8.6/letify/remoting/capability.py +0 -50
  42. letify-2.8.6/letify/remoting/loader.py +0 -96
  43. letify-2.8.6/letify/remoting/probe.py +0 -145
  44. letify-2.8.6/letify-core/Cargo.toml +0 -18
  45. letify-2.8.6/letify-core/build.py +0 -109
  46. letify-2.8.6/letify-core/letify-agent/Cargo.toml +0 -13
  47. letify-2.8.6/letify-core/letify-agent/src/driver.rs +0 -352
  48. letify-2.8.6/letify-core/letify-agent/src/main.rs +0 -317
  49. letify-2.8.6/letify-core/letify-driver/Cargo.toml +0 -18
  50. letify-2.8.6/letify-core/letify-driver/src/client.rs +0 -271
  51. letify-2.8.6/letify-core/letify-driver/src/exports.rs +0 -464
  52. letify-2.8.6/letify-core/letify-driver/src/image.rs +0 -106
  53. letify-2.8.6/letify-core/letify-driver/src/lib.rs +0 -90
  54. letify-2.8.6/letify-core/letify-driver/src/status.rs +0 -61
  55. letify-2.8.6/letify-core/letify-driver/src/table.rs +0 -206
  56. letify-2.8.6/letify-core/letify-wire/Cargo.toml +0 -9
  57. letify-2.8.6/letify-core/letify-wire/src/lib.rs +0 -957
  58. letify-2.8.6/letify-core/letify-wire/tests/throughput.rs +0 -133
  59. letify-2.8.6/tests/test_remoting.py +0 -340
  60. {letify-2.8.6 → letify-2.8.7}/.github/workflows/ci.yml +0 -0
  61. {letify-2.8.6 → letify-2.8.7}/.github/workflows/docs.yml +0 -0
  62. {letify-2.8.6 → letify-2.8.7}/.github/workflows/publish.yml +0 -0
  63. {letify-2.8.6 → letify-2.8.7}/.researchtree +0 -0
  64. {letify-2.8.6 → letify-2.8.7}/LICENSE +0 -0
  65. {letify-2.8.6 → letify-2.8.7}/docs/INTENT.md +0 -0
  66. {letify-2.8.6 → letify-2.8.7}/docs/guide/01-getting-started.md +0 -0
  67. {letify-2.8.6 → letify-2.8.7}/docs/guide/02-providers.md +0 -0
  68. {letify-2.8.6 → letify-2.8.7}/docs/guide/04-environments-and-data.md +0 -0
  69. {letify-2.8.6 → letify-2.8.7}/docs/guide/05-concurrency.md +0 -0
  70. {letify-2.8.6 → letify-2.8.7}/docs/guide/README.md +0 -0
  71. {letify-2.8.6 → letify-2.8.7}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
  72. {letify-2.8.6 → letify-2.8.7}/docs/guide/ko/02-/355/224/204/353/241/234/353/260/224/354/235/264/353/215/224.md" +0 -0
  73. {letify-2.8.6 → letify-2.8.7}/docs/guide/ko/04-/355/231/230/352/262/275/352/263/274-/353/215/260/354/235/264/355/204/260.md" +0 -0
  74. {letify-2.8.6 → letify-2.8.7}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
  75. {letify-2.8.6 → letify-2.8.7}/docs/guide/ko/README.md +0 -0
  76. {letify-2.8.6 → letify-2.8.7}/docs/mkdocs.yml +0 -0
  77. {letify-2.8.6 → letify-2.8.7}/examples/README.md +0 -0
  78. {letify-2.8.6 → letify-2.8.7}/examples/nvfp4-lora/.letify.example +0 -0
  79. {letify-2.8.6 → letify-2.8.7}/examples/nvfp4-lora/00_smoke.py +0 -0
  80. {letify-2.8.6 → letify-2.8.7}/examples/nvfp4-lora/01_sweep.py +0 -0
  81. {letify-2.8.6 → letify-2.8.7}/examples/nvfp4-lora/02_resume.py +0 -0
  82. {letify-2.8.6 → letify-2.8.7}/examples/nvfp4-lora/03_watch.py +0 -0
  83. {letify-2.8.6 → letify-2.8.7}/examples/nvfp4-lora/README.md +0 -0
  84. {letify-2.8.6 → letify-2.8.7}/examples/nvfp4-lora/README_ko.md +0 -0
  85. {letify-2.8.6 → letify-2.8.7}/examples/nvfp4-lora/common.py +0 -0
  86. {letify-2.8.6 → letify-2.8.7}/examples/nvfp4-lora/recipe.py +0 -0
  87. {letify-2.8.6 → letify-2.8.7}/letify/config/__init__.py +0 -0
  88. {letify-2.8.6 → letify-2.8.7}/letify/config/inventory.py +0 -0
  89. {letify-2.8.6 → letify-2.8.7}/letify/config/login.py +0 -0
  90. {letify-2.8.6 → letify-2.8.7}/letify/config/schema.py +0 -0
  91. {letify-2.8.6 → letify-2.8.7}/letify/config/secrets.py +0 -0
  92. {letify-2.8.6 → letify-2.8.7}/letify/config/writer.py +0 -0
  93. {letify-2.8.6 → letify-2.8.7}/letify/declare/__init__.py +0 -0
  94. {letify-2.8.6 → letify-2.8.7}/letify/declare/cache.py +0 -0
  95. {letify-2.8.6 → letify-2.8.7}/letify/declare/env.py +0 -0
  96. {letify-2.8.6 → letify-2.8.7}/letify/declare/function.py +0 -0
  97. {letify-2.8.6 → letify-2.8.7}/letify/declare/instance.py +0 -0
  98. {letify-2.8.6 → letify-2.8.7}/letify/declare/reduce.py +0 -0
  99. {letify-2.8.6 → letify-2.8.7}/letify/declare/replicas.py +0 -0
  100. {letify-2.8.6 → letify-2.8.7}/letify/errors.py +0 -0
  101. {letify-2.8.6 → letify-2.8.7}/letify/install.py +0 -0
  102. {letify-2.8.6 → letify-2.8.7}/letify/launcher.py +0 -0
  103. {letify-2.8.6 → letify-2.8.7}/letify/paths.py +0 -0
  104. {letify-2.8.6 → letify-2.8.7}/letify/protocol/__init__.py +0 -0
  105. {letify-2.8.6 → letify-2.8.7}/letify/protocol/codec.py +0 -0
  106. {letify-2.8.6 → letify-2.8.7}/letify/protocol/driver.py +0 -0
  107. {letify-2.8.6 → letify-2.8.7}/letify/protocol/handle.py +0 -0
  108. {letify-2.8.6 → letify-2.8.7}/letify/protocol/wire.py +0 -0
  109. {letify-2.8.6 → letify-2.8.7}/letify/protocol/worker.py +0 -0
  110. {letify-2.8.6 → letify-2.8.7}/letify/providers/__init__.py +0 -0
  111. {letify-2.8.6 → letify-2.8.7}/letify/providers/base.py +0 -0
  112. {letify-2.8.6 → letify-2.8.7}/letify/providers/colab.py +0 -0
  113. {letify-2.8.6 → letify-2.8.7}/letify/providers/colab_files.py +0 -0
  114. {letify-2.8.6 → letify-2.8.7}/letify/providers/colab_keepalive.py +0 -0
  115. {letify-2.8.6 → letify-2.8.7}/letify/providers/elice.py +0 -0
  116. {letify-2.8.6 → letify-2.8.7}/letify/providers/kaggle.py +0 -0
  117. {letify-2.8.6 → letify-2.8.7}/letify/providers/kaggle_adapter.py +0 -0
  118. {letify-2.8.6 → letify-2.8.7}/letify/providers/local.py +0 -0
  119. {letify-2.8.6 → letify-2.8.7}/letify/providers/modal.py +0 -0
  120. {letify-2.8.6 → letify-2.8.7}/letify/providers/modal_adapter.py +0 -0
  121. {letify-2.8.6 → letify-2.8.7}/letify/providers/naming.py +0 -0
  122. {letify-2.8.6 → letify-2.8.7}/letify/providers/shell.py +0 -0
  123. {letify-2.8.6 → letify-2.8.7}/letify/providers/tunnel.py +0 -0
  124. {letify-2.8.6 → letify-2.8.7}/letify/providers/usage.py +0 -0
  125. {letify-2.8.6 → letify-2.8.7}/letify/remoting/device/__init__.py +0 -0
  126. {letify-2.8.6 → letify-2.8.7}/letify/remoting/device/client.py +0 -0
  127. {letify-2.8.6 → letify-2.8.7}/letify/remoting/device/cuda.py +0 -0
  128. {letify-2.8.6 → letify-2.8.7}/letify/remoting/device/executor.py +0 -0
  129. {letify-2.8.6 → letify-2.8.7}/letify/remoting/device/frames.py +0 -0
  130. {letify-2.8.6 → letify-2.8.7}/letify/remoting/device/guard.py +0 -0
  131. {letify-2.8.6 → letify-2.8.7}/letify/remoting/device/tensor.py +0 -0
  132. {letify-2.8.6 → letify-2.8.7}/letify/remoting/device/trace.py +0 -0
  133. {letify-2.8.6 → letify-2.8.7}/letify/remoting/device/value.py +0 -0
  134. {letify-2.8.6 → letify-2.8.7}/letify/render.py +0 -0
  135. {letify-2.8.6 → letify-2.8.7}/letify/runtime/__init__.py +0 -0
  136. {letify-2.8.6 → letify-2.8.7}/letify/runtime/bootstrap.py +0 -0
  137. {letify-2.8.6 → letify-2.8.7}/letify/runtime/broker.py +0 -0
  138. {letify-2.8.6 → letify-2.8.7}/letify/runtime/channel.py +0 -0
  139. {letify-2.8.6 → letify-2.8.7}/letify/runtime/lease.py +0 -0
  140. {letify-2.8.6 → letify-2.8.7}/letify/runtime/pool.py +0 -0
  141. {letify-2.8.6 → letify-2.8.7}/letify/runtime/session.py +0 -0
  142. {letify-2.8.6 → letify-2.8.7}/letify/runtime/telemetry.py +0 -0
  143. {letify-2.8.6 → letify-2.8.7}/letify/store/__init__.py +0 -0
  144. {letify-2.8.6 → letify-2.8.7}/letify/store/backends/__init__.py +0 -0
  145. {letify-2.8.6 → letify-2.8.7}/letify/store/backends/drive.py +0 -0
  146. {letify-2.8.6 → letify-2.8.7}/letify/store/backends/filesystem.py +0 -0
  147. {letify-2.8.6 → letify-2.8.7}/letify/store/backends/google_auth.py +0 -0
  148. {letify-2.8.6 → letify-2.8.7}/letify/store/backends/layout.py +0 -0
  149. {letify-2.8.6 → letify-2.8.7}/letify/store/backends/objects.py +0 -0
  150. {letify-2.8.6 → letify-2.8.7}/letify/store/cas.py +0 -0
  151. {letify-2.8.6 → letify-2.8.7}/letify/store/handoff.py +0 -0
  152. {letify-2.8.6 → letify-2.8.7}/letify/store/pathdata.py +0 -0
  153. {letify-2.8.6 → letify-2.8.7}/letify/store/sendorder.py +0 -0
  154. {letify-2.8.6 → letify-2.8.7}/letify/store/volume.py +0 -0
  155. {letify-2.8.6 → letify-2.8.7}/letify/stubs.py +0 -0
  156. {letify-2.8.6 → letify-2.8.7}/letify/tools.py +0 -0
  157. {letify-2.8.6 → letify-2.8.7}/letify/transport/__init__.py +0 -0
  158. {letify-2.8.6 → letify-2.8.7}/letify/transport/agent.py +0 -0
  159. {letify-2.8.6 → letify-2.8.7}/letify/transport/announce.py +0 -0
  160. {letify-2.8.6 → letify-2.8.7}/letify/transport/link.py +0 -0
  161. {letify-2.8.6 → letify-2.8.7}/letify/transport/nat.py +0 -0
  162. {letify-2.8.6 → letify-2.8.7}/letify/transport/pipeline.py +0 -0
  163. {letify-2.8.6 → letify-2.8.7}/letify/transport/portmap.py +0 -0
  164. {letify-2.8.6 → letify-2.8.7}/letify/transport/probe.py +0 -0
  165. {letify-2.8.6 → letify-2.8.7}/letify/transport/quic.py +0 -0
  166. {letify-2.8.6 → letify-2.8.7}/letify/transport/rendezvous.py +0 -0
  167. {letify-2.8.6 → letify-2.8.7}/letify/transport/setup.py +0 -0
  168. {letify-2.8.6 → letify-2.8.7}/letify/transport/sshopts.py +0 -0
  169. {letify-2.8.6 → letify-2.8.7}/letify/transport/strategies.py +0 -0
  170. {letify-2.8.6 → letify-2.8.7}/letify-core/.gitignore +0 -0
  171. {letify-2.8.6/letify-ext → letify-2.8.7/letify-core/letify}/LICENSE +0 -0
  172. {letify-2.8.6 → letify-2.8.7}/letify-core/letify-quic/src/main.rs +0 -0
  173. {letify-2.8.6 → letify-2.8.7}/letify-core/rust-toolchain.toml +0 -0
  174. {letify-2.8.6 → letify-2.8.7}/letify-ext/.vscodeignore +0 -0
  175. {letify-2.8.6 → letify-2.8.7}/letify-ext/README.md +0 -0
  176. {letify-2.8.6 → letify-2.8.7}/letify-ext/media/icon.svg +0 -0
  177. {letify-2.8.6 → letify-2.8.7}/letify-ext/src/cli.ts +0 -0
  178. {letify-2.8.6 → letify-2.8.7}/letify-ext/src/extension.ts +0 -0
  179. {letify-2.8.6 → letify-2.8.7}/letify-ext/src/history.ts +0 -0
  180. {letify-2.8.6 → letify-2.8.7}/letify-ext/src/model.ts +0 -0
  181. {letify-2.8.6 → letify-2.8.7}/letify-ext/src/poll.ts +0 -0
  182. {letify-2.8.6 → letify-2.8.7}/letify-ext/src/render.ts +0 -0
  183. {letify-2.8.6 → letify-2.8.7}/letify-ext/test/cli.test.ts +0 -0
  184. {letify-2.8.6 → letify-2.8.7}/letify-ext/test/manifest.test.ts +0 -0
  185. {letify-2.8.6 → letify-2.8.7}/letify-ext/test/model.test.ts +0 -0
  186. {letify-2.8.6 → letify-2.8.7}/letify-ext/test/regressions.test.ts +0 -0
  187. {letify-2.8.6 → letify-2.8.7}/letify-ext/tsconfig.json +0 -0
  188. {letify-2.8.6 → letify-2.8.7}/letify_providers.py +0 -0
  189. {letify-2.8.6 → letify-2.8.7}/scripts/sync-main.sh +0 -0
  190. {letify-2.8.6 → letify-2.8.7}/tests/conftest.py +0 -0
  191. {letify-2.8.6 → letify-2.8.7}/tests/fake_kaggle_adapter.py +0 -0
  192. {letify-2.8.6 → letify-2.8.7}/tests/fake_modal_adapter.py +0 -0
  193. {letify-2.8.6 → letify-2.8.7}/tests/test_colab_keepalive.py +0 -0
  194. {letify-2.8.6 → letify-2.8.7}/tests/test_config.py +0 -0
  195. {letify-2.8.6 → letify-2.8.7}/tests/test_core.py +0 -0
  196. {letify-2.8.6 → letify-2.8.7}/tests/test_data.py +0 -0
  197. {letify-2.8.6 → letify-2.8.7}/tests/test_data_streaming.py +0 -0
  198. {letify-2.8.6 → letify-2.8.7}/tests/test_declare.py +0 -0
  199. {letify-2.8.6 → letify-2.8.7}/tests/test_device.py +0 -0
  200. {letify-2.8.6 → letify-2.8.7}/tests/test_distributed.py +0 -0
  201. {letify-2.8.6 → letify-2.8.7}/tests/test_drive_handoff.py +0 -0
  202. {letify-2.8.6 → letify-2.8.7}/tests/test_drive_store.py +0 -0
  203. {letify-2.8.6 → letify-2.8.7}/tests/test_editor_sessions.py +0 -0
  204. {letify-2.8.6 → letify-2.8.7}/tests/test_elice.py +0 -0
  205. {letify-2.8.6 → letify-2.8.7}/tests/test_frames.py +0 -0
  206. {letify-2.8.6 → letify-2.8.7}/tests/test_install.py +0 -0
  207. {letify-2.8.6 → letify-2.8.7}/tests/test_kaggle.py +0 -0
  208. {letify-2.8.6 → letify-2.8.7}/tests/test_kaggle_cookie.py +0 -0
  209. {letify-2.8.6 → letify-2.8.7}/tests/test_kaggle_link.py +0 -0
  210. {letify-2.8.6 → letify-2.8.7}/tests/test_link_log.py +0 -0
  211. {letify-2.8.6 → letify-2.8.7}/tests/test_notebook.py +0 -0
  212. {letify-2.8.6 → letify-2.8.7}/tests/test_parallel_placement.py +0 -0
  213. {letify-2.8.6 → letify-2.8.7}/tests/test_paths.py +0 -0
  214. {letify-2.8.6 → letify-2.8.7}/tests/test_portmap.py +0 -0
  215. {letify-2.8.6 → letify-2.8.7}/tests/test_protocol.py +0 -0
  216. {letify-2.8.6 → letify-2.8.7}/tests/test_quic.py +0 -0
  217. {letify-2.8.6 → letify-2.8.7}/tests/test_render.py +0 -0
  218. {letify-2.8.6 → letify-2.8.7}/tests/test_runtime.py +0 -0
  219. {letify-2.8.6 → letify-2.8.7}/tests/test_ship.py +0 -0
  220. {letify-2.8.6 → letify-2.8.7}/tests/test_store.py +0 -0
  221. {letify-2.8.6 → letify-2.8.7}/tests/test_strategies.py +0 -0
  222. {letify-2.8.6 → letify-2.8.7}/tests/test_stubs.py +0 -0
  223. {letify-2.8.6 → letify-2.8.7}/tests/test_transport.py +0 -0
  224. {letify-2.8.6 → letify-2.8.7}/tests/test_tunnel_setup.py +0 -0
@@ -0,0 +1,89 @@
1
+ # Publish the crate letify to crates.io when a version tag is pushed.
2
+ #
3
+ # This runs beside publish.yml on the same tag v<version>. Order inside this file:
4
+ # 1. verify: the tag agrees with every version file, the tagged commit is on develop, the
5
+ # workspace builds and its tests pass, and `cargo publish --dry-run` packages and builds
6
+ # the crate alone.
7
+ # 2. publish-crate: the upload, only after verify succeeds.
8
+ # The upload is last because crates.io never replaces a version. A published number can be
9
+ # yanked but is taken forever, so every check that can fail runs before it.
10
+ #
11
+ # GitHub Release creation stays in publish.yml. Other crates in letify-core set publish = false.
12
+ #
13
+ # One-time setup: add the crates.io API token as the secret CARGO_REGISTRY_TOKEN in the GitHub
14
+ # environment crates-io. See docs/guide/08-releasing.md.
15
+ name: publish-crates
16
+
17
+ on:
18
+ push:
19
+ tags: ["v*"]
20
+
21
+ permissions:
22
+ contents: read
23
+
24
+ concurrency:
25
+ group: publish-crates-${{ github.ref }}
26
+ cancel-in-progress: false
27
+
28
+ jobs:
29
+ verify:
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+ with:
34
+ fetch-depth: 0
35
+ - uses: astral-sh/setup-uv@v6
36
+ - name: Install Rust
37
+ # rustup reads letify-core/rust-toolchain.toml when cargo runs there.
38
+ run: |
39
+ rustup toolchain install stable --profile minimal
40
+ rustup default stable
41
+ - name: Check the tag and every file carry one version
42
+ run: uv run --no-project python scripts/version.py check --tag "$GITHUB_REF_NAME"
43
+ - name: Check the tagged commit is on develop
44
+ # letify tags develop, not main, so develop is the branch a release must come from.
45
+ run: |
46
+ git fetch origin develop
47
+ if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/develop; then
48
+ echo "Tagged commit $GITHUB_SHA is not an ancestor of origin/develop."
49
+ echo "Merge the release commit into develop before tagging."
50
+ exit 1
51
+ fi
52
+ - name: Build the workspace
53
+ # All five crates are pure Rust with no system packages needed on ubuntu-latest.
54
+ working-directory: letify-core
55
+ run: cargo build --workspace --locked
56
+ - name: Test the workspace
57
+ working-directory: letify-core
58
+ run: cargo test --workspace --locked
59
+ - name: Package and build the crate without uploading
60
+ working-directory: letify-core
61
+ run: cargo publish -p letify --dry-run --locked
62
+
63
+ publish-crate:
64
+ needs: verify
65
+ runs-on: ubuntu-latest
66
+ environment: crates-io
67
+ steps:
68
+ - uses: actions/checkout@v4
69
+ - name: Install Rust
70
+ run: |
71
+ rustup toolchain install stable --profile minimal
72
+ rustup default stable
73
+ - name: Publish letify to crates.io
74
+ working-directory: letify-core
75
+ env:
76
+ CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
77
+ run: |
78
+ version="${GITHUB_REF_NAME#v}"
79
+ status=$(curl -s -o /dev/null -w '%{http_code}' -A "letify-release (https://github.com/DarkPyonix/letify)" "https://crates.io/api/v1/crates/letify/${version}")
80
+ if [ "$status" = "200" ]; then
81
+ echo "letify ${version} is already on crates.io, nothing to publish."
82
+ exit 0
83
+ fi
84
+ if [ -z "$CARGO_REGISTRY_TOKEN" ]; then
85
+ echo "CARGO_REGISTRY_TOKEN is empty."
86
+ echo "Add a crates.io API token as the secret CARGO_REGISTRY_TOKEN in the crates-io environment."
87
+ exit 1
88
+ fi
89
+ cargo publish -p letify --locked
@@ -0,0 +1,77 @@
1
+ # Reserve a letify-* crate name on crates.io before its first real release.
2
+ #
3
+ # Pushing the tag reserve/<crate>, for example reserve/letify-link, publishes version 0.0.0 of
4
+ # that crate: a library with no code whose README says which letify release ships it. The real
5
+ # crate later publishes at letify's own version, as every crate in letify-core does, so 0.0.0
6
+ # stays below every real release.
7
+ #
8
+ # Only names starting with letify- are accepted, and a name crates.io already has is skipped,
9
+ # so pushing the tag again changes nothing. The crate letify itself is not reserved here: it
10
+ # publishes at the release version from publish-crates.yml.
11
+ #
12
+ # The upload runs in the GitHub environment crates-io with the secret CARGO_REGISTRY_TOKEN, as
13
+ # publish-crates.yml does.
14
+ name: reserve-crates
15
+
16
+ on:
17
+ push:
18
+ tags: ["reserve/*"]
19
+
20
+ permissions:
21
+ contents: read
22
+
23
+ concurrency:
24
+ group: reserve-crates-${{ github.ref }}
25
+ cancel-in-progress: false
26
+
27
+ jobs:
28
+ reserve:
29
+ runs-on: ubuntu-latest
30
+ environment: crates-io
31
+ steps:
32
+ - name: Install Rust
33
+ run: |
34
+ rustup toolchain install stable --profile minimal
35
+ rustup default stable
36
+ - name: Publish 0.0.0 unless the name is taken
37
+ env:
38
+ CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
39
+ CRATE: ${{ github.ref_name }}
40
+ run: |
41
+ set -euo pipefail
42
+ name="${CRATE#reserve/}"
43
+ if [[ ! "$name" =~ ^letify-[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
44
+ echo "reserve/$name: only names of the form letify-<word> are reserved here" >&2
45
+ exit 1
46
+ fi
47
+ code="$(curl -s -o /dev/null -w '%{http_code}' -A 'letify-reserve (github.com/DarkPyonix/letify)' "https://crates.io/api/v1/crates/$name")"
48
+ if [[ "$code" == "200" ]]; then
49
+ echo "$name is already on crates.io; nothing to reserve"
50
+ exit 0
51
+ fi
52
+ if [[ -z "$CARGO_REGISTRY_TOKEN" ]]; then
53
+ echo "CARGO_REGISTRY_TOKEN is empty. Add it to the crates-io environment." >&2
54
+ exit 1
55
+ fi
56
+ dir="$RUNNER_TEMP/$name"
57
+ mkdir -p "$dir/src"
58
+ cat > "$dir/Cargo.toml" <<TOML
59
+ [package]
60
+ name = "$name"
61
+ version = "0.0.0"
62
+ edition = "2024"
63
+ license = "MIT"
64
+ description = "Reserved for the letify project. The first release ships with letify 3.0."
65
+ repository = "https://github.com/DarkPyonix/letify"
66
+ readme = "README.md"
67
+ TOML
68
+ cat > "$dir/README.md" <<MD
69
+ # $name
70
+
71
+ This name is reserved for the letify project, https://github.com/DarkPyonix/letify.
72
+ Version 0.0.0 holds no code. The first release ships with letify 3.0 and carries
73
+ letify's version, as every letify crate does.
74
+ MD
75
+ echo '#![doc = include_str!("../README.md")]' > "$dir/src/lib.rs"
76
+ cd "$dir"
77
+ cargo publish --allow-dirty
@@ -253,10 +253,11 @@ __marimo__/
253
253
  .claude
254
254
 
255
255
  # letify
256
+ .letify-home/
256
257
  .letify/
257
258
  typings/
258
259
 
259
- # letify-core binaries, copied here by letify-core/build.py
260
+ # The letify-quic binary, copied here by letify-core/build.py
260
261
  letify/remoting/lib/
261
262
 
262
263
  # Artifacts the examples write where they run
@@ -114,7 +114,7 @@ This repository uses [ResearchTree](https://darkpyonix.github.io/researchtree/)
114
114
 
115
115
  ## Code layout
116
116
 
117
- The Python package is pure Python and links no Python extension. The package lives in `letify/` at the repository root. letify-core, the Rust workspace in `letify-core/`, is built in CI and shipped as plain binaries in `letify/remoting/lib/` inside platform wheels.
117
+ The Python package is pure Python and links no Python extension. The package lives in `letify/` at the repository root. letify-core, the Rust workspace in `letify-core/`, is built in CI, and its `letify-quic` binary is shipped in `letify/remoting/lib/` inside platform wheels.
118
118
 
119
119
  ```
120
120
  letify/
@@ -129,7 +129,7 @@ letify/
129
129
  runtime/ Channel, session, pool, lease, bootstrap and telemetry
130
130
  store/ Content addressed blob store, volumes and backends
131
131
  providers/ Provider base class and one module per provider
132
- remoting/ CUDA API forwarding, used only where a low-latency path exists
132
+ remoting/ PyTorch operator forwarding for host="local" (remoting/device/) and the efficiency arithmetic; also holds the letify-quic binary in lib/
133
133
  _vendor/ Vendored third party code
134
134
  letify-ext/ VS Code extension (TypeScript) showing quota and GPU activity from the CLI's --json output
135
135
  ```
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: letify
3
- Version: 2.8.6
3
+ Version: 2.8.7
4
4
  Summary: Declarations that become infrastructure.
5
5
  Project-URL: Homepage, https://github.com/DarkPyonix/letify
6
6
  Project-URL: Documentation, https://github.com/DarkPyonix/letify/tree/main/docs
@@ -171,7 +171,7 @@ uv add letify
171
171
 
172
172
  That is the whole install, for every provider. letify installs only cloudpickle and blake3. It needs [uv](https://docs.astral.sh/uv/) on the machine, because provider tools such as the Colab CLI and the Modal client run through uv in their own environments, not in your `.venv`.
173
173
 
174
- The wheel carries [letify-core](letify-core/), the Rust component behind `host=letify.local` that stands in for the CUDA driver. Wheels are built for Linux x86_64 and aarch64, Windows x86_64 and arm64, and macOS arm64 and x86_64. On another platform, pip falls back to the source distribution, which has no letify-core, and `host=letify.local` is refused.
174
+ The wheel carries the `letify-quic` binary built from [letify-core](letify-core/), the Rust workspace. Wheels are built for Linux x86_64 and aarch64, Windows x86_64 and arm64, and macOS arm64 and x86_64. On another platform, pip falls back to the source distribution, which has no `letify-quic`, so the QUIC connection strategy is skipped.
175
175
 
176
176
  ---
177
177
 
@@ -259,9 +259,9 @@ Two words, and neither of them is a mechanism.
259
259
 
260
260
  **`device`** carries the provider, the account and the accelerator in one value, because those are one decision. Core count and memory come with the shape the provider registered, so there is nothing to ask for.
261
261
 
262
- **`host`** is the CUDA word for the CPU side. `letify.local` keeps Python and the libraries in this process and forwards only CUDA calls. `letify.remote` ships the function to the machine that holds the device. The strings `"local"` and `"remote"` are the same values.
262
+ **`host`** is the CUDA word for the CPU side. `letify.local` keeps Python and the libraries in this process and forwards only PyTorch operators. `letify.remote` ships the function to the machine that holds the device. The strings `"local"` and `"remote"` are the same values.
263
263
 
264
- Leave `host` out and you get `letify.local`. When the device is far away, forwarding every CUDA call is slow, so the call warns with the expected efficiency and then runs. For a remote GPU, `host=letify.remote` is usually what you want.
264
+ Leave `host` out and you get `letify.local`. When the device is far away, forwarding operators is slow, so the call warns with the expected efficiency and then runs. For a remote GPU, `host=letify.remote` is usually what you want.
265
265
 
266
266
  How long a session lives is not a declaration argument. A call ends its session. `with let.keep_alive():` keeps sessions for the length of a block, so a run of separate calls does not pay session start each time.
267
267
 
@@ -283,7 +283,7 @@ Counter-intuitive consequence: **a faster GPU makes forwarding worse**, because
283
283
 
284
284
  Decoding is the case that stays bad. Throughput is bounded near `1000 / (k × RTT)` tokens per second, so the card stops mattering. Ship the whole `generate` call instead.
285
285
 
286
- Measure your own `k` with `torch.cuda.set_sync_debug_mode("warn")` and your round trip with `letify probe`. See [docs/NETWORK.md](docs/NETWORK.md).
286
+ Measure your own `k` with `torch.cuda.set_sync_debug_mode("warn")` and your round trip with `ping`. See [docs/NETWORK.md](docs/NETWORK.md).
287
287
 
288
288
  </details>
289
289
 
@@ -495,7 +495,6 @@ letify status # what is running right now
495
495
  letify usage # what is left on each account
496
496
  letify utilization # how busy each instance's GPU is
497
497
  letify check lab # does this machine answer?
498
- letify probe lab # is host=letify.local worth using here?
499
498
  letify efficiency 0.5 3 150 # the formula, from measured terms
500
499
  ```
501
500
 
@@ -513,7 +512,7 @@ Add `--json` to `usage`, `utilization` or `status` for output a program can read
513
512
  | 📐 [docs/SPEC.md](docs/SPEC.md) | The design as it stands, decision by decision |
514
513
  | 🧩 [docs/COMPONENT.md](docs/COMPONENT.md) | Every class, and the vocabulary |
515
514
  | 🌐 [docs/NETWORK.md](docs/NETWORK.md) | Transports, latency measurements, tunnel choices |
516
- | 🦀 [letify-core/](letify-core/) | The Rust workspace behind `host=letify.local` |
515
+ | 🦀 [letify-core/](letify-core/) | The Rust workspace: the `letify` crate and the `letify-quic` binary |
517
516
  | 🧭 [docs/guide/](docs/guide/) | Task-oriented guides |
518
517
  | 🇰🇷 [docs/locales/README_ko.md](docs/locales/README_ko.md) | 한국어 |
519
518
 
@@ -527,11 +526,9 @@ Alpha, and honest about it. What works today:
527
526
  ✅ Persistent sessions: a session cache keeps values between calls, large arguments travel once
528
527
  ✅ Content addressed storage, configuration and secrets
529
528
  ✅ The `Local` and `Colab` providers
530
- ✅ `letify-core`, verified on a real GPU: the agent opens the driver, the local driver forwards an allocation and a copy in both directions, and the bytes match
531
529
 
532
530
  Not finished yet:
533
531
 
534
- 🚧 `letify-driver` covers the entry points a PyTorch process needs to start up and run one kernel. Anything else names itself and returns `CUDA_ERROR_NOT_SUPPORTED`, so a real run prints the list of what to build next
535
532
  🚧 `Modal` and `Elice` follow each published interface but have not been run against the live services
536
533
  🚧 Unified memory cannot be forwarded at all, so a paged optimizer needs `host=letify.remote`
537
534
 
@@ -238,7 +238,6 @@ letify status # live sessions and what they are costing
238
238
  letify usage [alias] # what is left on each account
239
239
  letify utilization [alias] # how busy each instance's accelerator is
240
240
  letify check <alias> # confirm a machine answers
241
- letify probe <host> # whether host="local" is worth using
242
241
  letify efficiency 0.5 3 150 # expected share of a direct run
243
242
  letify stubs # write typings/letify_providers.pyi for editor completion
244
243
  ```
@@ -298,6 +297,10 @@ That is the only install. `letify` installs cloudpickle and blake3 and nothing e
298
297
 
299
298
  `host="local"` additionally needs PyTorch 2.1 or newer in the project, locally and in the runtime's environment, with the same major.minor version on both sides. letify never installs it: torch is the project's own dependency.
300
299
 
300
+ ## Rust crate
301
+
302
+ The Rust crate `letify` is published on crates.io at the same version as the Python package. It will re-export the components meant for use outside letify, first the connection library `letify-link`.
303
+
301
304
  ## How this project is built
302
305
 
303
306
  Spec driven and test driven. [docs/SPEC.md](docs/SPEC.md) is the source of truth, a change edits it before the code, and every test traces to a section of it. The rules are in [CLAUDE.md](CLAUDE.md).
@@ -146,7 +146,7 @@ uv add letify
146
146
 
147
147
  That is the whole install, for every provider. letify installs only cloudpickle and blake3. It needs [uv](https://docs.astral.sh/uv/) on the machine, because provider tools such as the Colab CLI and the Modal client run through uv in their own environments, not in your `.venv`.
148
148
 
149
- The wheel carries [letify-core](letify-core/), the Rust component behind `host=letify.local` that stands in for the CUDA driver. Wheels are built for Linux x86_64 and aarch64, Windows x86_64 and arm64, and macOS arm64 and x86_64. On another platform, pip falls back to the source distribution, which has no letify-core, and `host=letify.local` is refused.
149
+ The wheel carries the `letify-quic` binary built from [letify-core](letify-core/), the Rust workspace. Wheels are built for Linux x86_64 and aarch64, Windows x86_64 and arm64, and macOS arm64 and x86_64. On another platform, pip falls back to the source distribution, which has no `letify-quic`, so the QUIC connection strategy is skipped.
150
150
 
151
151
  ---
152
152
 
@@ -234,9 +234,9 @@ Two words, and neither of them is a mechanism.
234
234
 
235
235
  **`device`** carries the provider, the account and the accelerator in one value, because those are one decision. Core count and memory come with the shape the provider registered, so there is nothing to ask for.
236
236
 
237
- **`host`** is the CUDA word for the CPU side. `letify.local` keeps Python and the libraries in this process and forwards only CUDA calls. `letify.remote` ships the function to the machine that holds the device. The strings `"local"` and `"remote"` are the same values.
237
+ **`host`** is the CUDA word for the CPU side. `letify.local` keeps Python and the libraries in this process and forwards only PyTorch operators. `letify.remote` ships the function to the machine that holds the device. The strings `"local"` and `"remote"` are the same values.
238
238
 
239
- Leave `host` out and you get `letify.local`. When the device is far away, forwarding every CUDA call is slow, so the call warns with the expected efficiency and then runs. For a remote GPU, `host=letify.remote` is usually what you want.
239
+ Leave `host` out and you get `letify.local`. When the device is far away, forwarding operators is slow, so the call warns with the expected efficiency and then runs. For a remote GPU, `host=letify.remote` is usually what you want.
240
240
 
241
241
  How long a session lives is not a declaration argument. A call ends its session. `with let.keep_alive():` keeps sessions for the length of a block, so a run of separate calls does not pay session start each time.
242
242
 
@@ -258,7 +258,7 @@ Counter-intuitive consequence: **a faster GPU makes forwarding worse**, because
258
258
 
259
259
  Decoding is the case that stays bad. Throughput is bounded near `1000 / (k × RTT)` tokens per second, so the card stops mattering. Ship the whole `generate` call instead.
260
260
 
261
- Measure your own `k` with `torch.cuda.set_sync_debug_mode("warn")` and your round trip with `letify probe`. See [docs/NETWORK.md](docs/NETWORK.md).
261
+ Measure your own `k` with `torch.cuda.set_sync_debug_mode("warn")` and your round trip with `ping`. See [docs/NETWORK.md](docs/NETWORK.md).
262
262
 
263
263
  </details>
264
264
 
@@ -470,7 +470,6 @@ letify status # what is running right now
470
470
  letify usage # what is left on each account
471
471
  letify utilization # how busy each instance's GPU is
472
472
  letify check lab # does this machine answer?
473
- letify probe lab # is host=letify.local worth using here?
474
473
  letify efficiency 0.5 3 150 # the formula, from measured terms
475
474
  ```
476
475
 
@@ -488,7 +487,7 @@ Add `--json` to `usage`, `utilization` or `status` for output a program can read
488
487
  | 📐 [docs/SPEC.md](docs/SPEC.md) | The design as it stands, decision by decision |
489
488
  | 🧩 [docs/COMPONENT.md](docs/COMPONENT.md) | Every class, and the vocabulary |
490
489
  | 🌐 [docs/NETWORK.md](docs/NETWORK.md) | Transports, latency measurements, tunnel choices |
491
- | 🦀 [letify-core/](letify-core/) | The Rust workspace behind `host=letify.local` |
490
+ | 🦀 [letify-core/](letify-core/) | The Rust workspace: the `letify` crate and the `letify-quic` binary |
492
491
  | 🧭 [docs/guide/](docs/guide/) | Task-oriented guides |
493
492
  | 🇰🇷 [docs/locales/README_ko.md](docs/locales/README_ko.md) | 한국어 |
494
493
 
@@ -502,11 +501,9 @@ Alpha, and honest about it. What works today:
502
501
  ✅ Persistent sessions: a session cache keeps values between calls, large arguments travel once
503
502
  ✅ Content addressed storage, configuration and secrets
504
503
  ✅ The `Local` and `Colab` providers
505
- ✅ `letify-core`, verified on a real GPU: the agent opens the driver, the local driver forwards an allocation and a copy in both directions, and the bytes match
506
504
 
507
505
  Not finished yet:
508
506
 
509
- 🚧 `letify-driver` covers the entry points a PyTorch process needs to start up and run one kernel. Anything else names itself and returns `CUDA_ERROR_NOT_SUPPORTED`, so a real run prints the list of what to build next
510
507
  🚧 `Modal` and `Elice` follow each published interface but have not been run against the live services
511
508
  🚧 Unified memory cannot be forwarded at all, so a paged optimizer needs `host=letify.remote`
512
509
 
@@ -24,7 +24,7 @@ Read this table first. Most confusion about letify is one of these words meaning
24
24
  | **persistence** | Whether a provider's storage outlives a session: `persistent` or `ephemeral`. |
25
25
  | **RemoteTensor** | Under `host="local"`, the local stand-in for a tensor on the runtime's GPU: a meta tensor for shape and dtype plus a handle. Reports its device as `cuda:0`. |
26
26
  | **device worker** | The process on the runtime that runs PyTorch operators on real tensors keyed by handle, one per session. |
27
- | **letify-core** | A Rust workspace from an earlier `host="local"` design that stood in for the CUDA driver. Retired from that path; nothing on it calls letify-core. |
27
+ | **letify-core** | The Rust workspace in `letify-core/`: the `letify` crate and the `letify-quic` binary. The CUDA driver stand-in it once held is removed. |
28
28
 
29
29
  ## The shape of the thing
30
30
 
@@ -58,7 +58,7 @@ One directory per concern, so the file you need is the one named after the thing
58
58
  | `runtime/` | The channel, the session, the pool, the lease, bootstrap source |
59
59
  | `providers/` | The base class, name normalization, and one module per provider |
60
60
  | `store/` | The content addressed store, volumes, and `backends/` |
61
- | `remoting/` | PyTorch forwarding in `remoting/device/`, and the round trip probe and efficiency arithmetic |
61
+ | `remoting/` | PyTorch forwarding in `remoting/device/`, and the efficiency arithmetic |
62
62
  | `launcher.py` | `Launcher` itself, which ties the rest together |
63
63
  | `errors.py` | The exception hierarchy, which everything imports |
64
64
  | `cli.py` | The command line |
@@ -180,7 +180,7 @@ Three protocol rules exist for performance rather than tidiness. Large arguments
180
180
 
181
181
  ## remoting
182
182
 
183
- PyTorch forwarding, in `remoting/device/`, and the round trip arithmetic beside it.
183
+ PyTorch forwarding, in `remoting/device/`, and the efficiency arithmetic beside it.
184
184
 
185
185
  | Module | Owns |
186
186
  |---|---|
@@ -197,4 +197,4 @@ It refuses rather than degrading when PyTorch is absent or too old. It does not
197
197
 
198
198
  ## letify-core
199
199
 
200
- A Rust workspace that stood in for the CUDA driver in an earlier design of `host="local"`. It is retired from that path because libcudart needs driver symbols the stand-in cannot provide; the spec's "The driver stand-in is retired" gives the measurement. The crates remain in `letify-core/` and nothing in the Python package calls them on the `host="local"` path.
200
+ The Rust workspace in `letify-core/`. It holds the crate `letify`, published to crates.io, and the binary `letify-quic`, which the wheels carry in `letify/remoting/lib/` and `letify/transport/quic.py` runs. The driver stand-in it once held is removed; the spec's "The driver stand-in is removed" gives the measurement and where the code is.
@@ -217,26 +217,6 @@ Both sides bound one TCP port, learned the mapping from `stun.nextcloud.com:443`
217
217
 
218
218
  `colab console` is not a usable channel: it is a terminal inside tmux, and a single input line of 4000 characters or more arrives truncated or mixed with terminal control sequences. The Colab runtime proxy reaches only port 8080 on the VM; other port prefixes return 404.
219
219
 
220
- ### letify-core copy throughput
221
-
222
- A copy to the device through letify-core, with the payload streamed from the caller's buffer into the agent's staging buffer, moves about 2000 MiB/s over loopback TCP. The same payload through an encoded frame, the path before streaming, moves about 520 MiB/s. Both carry one 256 MiB `CopyToDevice` request between a `BufWriter` and a `BufReader` with `TCP_NODELAY`, on an AMD EPYC 7763, and report the median of five runs.
223
-
224
- | Path | Three invocations |
225
- |---|---|
226
- | Encoded frame, before the change | 545, 544, 507 MiB/s |
227
- | Encoded frame, after the change | 515, 514, 519 MiB/s |
228
- | Streamed into staging | 2043, 1961, 1873 MiB/s |
229
-
230
- A copy to the host, with the payload streamed from the agent's staging buffer into the caller's buffer, moves about 3250 MiB/s over loopback TCP. The same payload through an encoded frame and a copy into the caller's buffer, the path before streaming, moves about 520 MiB/s. Both carry one 256 MiB `Payload` reply over the same socket pair and machine and report the median of five runs. The copy on the device is not included.
231
-
232
- | Path | Three invocations |
233
- |---|---|
234
- | Encoded frame, before the change | 522, 514, 519 MiB/s |
235
- | Encoded frame, after the change | 528, 527, 540 MiB/s |
236
- | Streamed into the destination | 3234, 3261, 3244 MiB/s |
237
-
238
- Loopback removes the network, so these numbers bound what the copy path itself costs. On a real link the link rate decides throughput whenever it is below them. To reproduce, run `cargo test --release -p letify-wire --test throughput -- --ignored --nocapture --test-threads=1` in `letify-core/`.
239
-
240
220
  ## PyTorch forwarding on dept_gpu
241
221
 
242
222
  > Reading the loss once every 50 steps, `host="local"` takes a median 3.45 ms per step against 1.70 ms directly on the server: about twice the direct time, and one round trip per 45 steps. Measured on 2026-09-14.
@@ -315,7 +295,7 @@ Three checks settle most of what is provider specific.
315
295
 
316
296
  ```bash
317
297
  # Round trip to the machine, which sets the efficiency formula.
318
- letify probe gpu.lab.example.edu
298
+ ping -c 5 gpu.lab.example.edu
319
299
 
320
300
  # Whether a layer 3 tunnel is possible at all.
321
301
  ls -l /dev/net/tun
@@ -278,7 +278,7 @@ Not opening a link is a decision of this project, not a rule of Kaggle's. Two so
278
278
  - `TPU_V3_8` is a TPU instance, carried by `Instance.tpu` rather than `Instance.gpu`, and requests `TPU_V3_8` as the accelerator in `CommitAndRun`. With `host="remote"`, letify ships the whole function to the runtime. Using the TPU is the user's code's responsibility, with `torch-xla` or JAX in the user's own lock file; letify neither installs nor checks those TPU libraries. letify requests and reports the TPU, without making the user's code TPU-capable. Kaggle refuses `host="local"` for TPU instances as it does for every other Kaggle instance.
279
279
  - When no account inventory is configured, the default inventory takes each discovered instance's device count, so the two-card `T4` shape can be reserved. An explicit account inventory keeps its configured counts and indices.
280
280
  - **Two credentials, both required.** The cookie starts and ends the interactive session and mints its Jupyter proxy URL. The API token authenticates every official Kaggle CLI invocation through `KAGGLE_API_TOKEN` in the child environment, never process arguments, for notebook deletion and weekly quota. There is no cookie fallback. Login stores the cookie in `~/.letify/accounts/<alias>/cookie` and the verbatim token, including `KGAT_`, in `access_token` beside it, using owner-only files (0600) and an owner-only directory (0700). The CLI accepts no working file-based token form, so keeping the token out of the environment cannot be honoured. Its child process environment is the narrowest available scope and exposes less than the argument list. An account without `access_token` is refused with `ConfigError` naming the login command when it reaches a CLI-only path.
281
- - **Token input and notebook owner.** `letify login kaggle <alias>` accepts `--key` or hidden input asking for the API token exactly as kaggle.com shows it, including `KGAT_`. It requires that prefix and a nonempty body with no whitespace; it does not strip or reformat the token. Length is not fixed because the measured 32-character body establishes a working token, not a universal length contract. A malformed token is refused with a message naming what to paste, before any cookie verification or file write. The token is not verified online at login. A login never silently reuses a token already on disk: `letify login kaggle <alias> --replace` asks for the token again, because the reason to renew an account is usually that the stored credential stopped working, and reusing it would make the renewal a no-op with no way to replace it. An account declared before the token was stored under this name still has to be renewed once. Login retains `--username` or a username prompt and stores it in `username` beside the credentials with mode 0600. This is only the owner for `kernels delete -y <owner>/<slug>`, not an authentication credential. Explicit input avoids another authenticated lookup and does not mistake a cookie display name for an owner. The owner is not read from an authenticated call. Reading an existing `access_token` file strips leading and trailing whitespace, including a trailing newline, before CLI calls or reuse by login when `--key` is omitted. Explicit `--key` and prompted token input still reject whitespace rather than reformatting it, so adding an owner with `--username` does not require pasting the token again. The owner is a public Kaggle username, not a secret, and its file uses the same owner-only permissions as the account directory. Login refuses a missing or blank owner before cookie verification or any file write. Nothing is written until both credentials, the owner and cookie verification pass. Before any session starts or notebook is created, `open_channel` requires a readable, nonempty `username` file. An incomplete account is refused with `ConfigError` naming the login command, so a run cannot reach teardown without a notebook owner.
281
+ - **Token input and notebook owner.** `letify login kaggle <alias>` accepts `--key` or hidden input asking for the API token exactly as kaggle.com shows it, including `KGAT_`. It requires that prefix and a nonempty body with no whitespace; it does not strip or reformat the token. Length is not fixed because the measured 32-character body establishes a working token, not a universal length contract. A malformed token is refused with a message naming what to paste, before any cookie verification or file write. The token is not verified online at login. A renewal asks for each credential rather than reusing what is on disk, and a blank answer keeps the stored one. The two credentials expire on different clocks: the cookie is a thirty day web session and the token lasts until it is rotated, so renewing one must not demand the other. `letify login kaggle <alias> --replace` therefore prompts for the token and then the cookie, each saying that blank keeps what is stored, and writes only what was answered. A blank answer with nothing stored is refused, naming what to paste. Silently reusing a stored credential would make a renewal a no-op with no way to replace a credential that stopped working. Login retains `--username` or a username prompt and stores it in `username` beside the credentials with mode 0600. This is only the owner for `kernels delete -y <owner>/<slug>`, not an authentication credential. Explicit input avoids another authenticated lookup and does not mistake a cookie display name for an owner. The owner is not read from an authenticated call. Reading an existing `access_token` file strips leading and trailing whitespace, including a trailing newline, before CLI calls or reuse by login when `--key` is omitted. Explicit `--key` and prompted token input still reject whitespace rather than reformatting it, so adding an owner with `--username` does not require pasting the token again. The owner is a public Kaggle username, not a secret, and its file uses the same owner-only permissions as the account directory. Login refuses a missing or blank owner before cookie verification or any file write. Nothing is written until both credentials, the owner and cookie verification pass. Before any session starts or notebook is created, `open_channel` requires a readable, nonempty `username` file. An incomplete account is refused with `ConfigError` naming the login command, so a run cannot reach teardown without a notebook owner.
282
282
  - **The cookie carries an ``exp`` claim, but that claim is not proof the cookie still works.** The `CLIENT-TOKEN` cookie is an `alg:none` JWT whose `exp` claim was thirty days after the login that minted it, in every case observed. But a live account has been seen with over nine days left on that claim while every internal call, including the plain `users.UsersService/GetCurrentUser` read, answered as a refused, anonymous session: Kaggle invalidates a cookie server side, for reasons this project has not pinned down, without the claim changing to say so. So `exp` is useful for exactly one direction: a cookie past its `exp` is certainly dead. It says nothing about a cookie before its `exp`, which may be live or may already be refused.
283
283
  - **Two checks, for two costs.** An offline check reads only the claim, costs no network call, and is what `letify providers` shows as the account's `exp claim says N days left (not checked live; see letify usage)`, or `EXPIRED` once `exp` has passed. An online check additionally calls `GetCurrentUser`, the same call the login proved the cookie with, and is made once per session start (`open_channel`) and once per `letify usage` read, each result trusted for sixty seconds so starting several runtimes together costs one round trip rather than one per runtime. A run whose cookie fails either check is refused with `ConfigError`: missing or past `exp` says exactly that; refused online says "the Kaggle cookie has not expired, but Kaggle no longer accepts it", which is the one fact that check establishes, never a guess at why. A run is also refused when fewer than one hour is left on the `exp` claim, since starting a session the claim says the cookie cannot outlive risks it dying mid run with no way to tell that apart from any other lost runtime; this offline guard and the online check both have to pass.
284
284
  - The online check is not made on every call into a running session, only at the two points above, so it is not the cost of every request a declared function makes.
@@ -1895,7 +1895,7 @@ A UDP pinhole opens where a TCP simultaneous open is refused, which is why Tailc
1895
1895
 
1896
1896
  QUIC is the one UDP payload such a device is unlikely to shape, because HTTP/3 is QUIC and shaping it breaks the web. So letify can carry the session over a real QUIC v1 connection: version 1 on the wire, a TLS 1.3 handshake, a certificate, and the ALPN `letify/1`.
1897
1897
 
1898
- `letify-quic` is a binary in letify-core, built for each wheel and installed beside the CUDA shim in `letify/remoting/lib/`. It takes the punched endpoints rather than discovering anything, because the rendezvous already exchanges them:
1898
+ `letify-quic` is a binary in letify-core, built for each wheel and installed in `letify/remoting/lib/`. It takes the punched endpoints rather than discovering anything, because the rendezvous already exchanges them:
1899
1899
 
1900
1900
  ```
1901
1901
  letify-quic serve --bind <port> --peer <ip:port> --token <hex> --forward <ssh port>
@@ -2585,11 +2585,13 @@ A lost worker process or link raises `RuntimeLost`, and the session is discarded
2585
2585
 
2586
2586
  `_make_wrapper_subclass`, `TorchFunctionMode` and `torch.utils._pytree` are each present in 2.1. An older PyTorch raises `UnsupportedMode` naming the version found and the version needed.
2587
2587
 
2588
- ### The driver stand-in is retired <!-- id: letify-core -->
2588
+ ### The driver stand-in is removed <!-- id: letify-core -->
2589
2589
 
2590
- > `host="local"` does not load letify-core. Standing in for the CUDA driver was replaced by operator forwarding.
2590
+ > `host="local"` does not load letify-core. Standing in for the CUDA driver was replaced by operator forwarding, and the driver stand-in was removed from the tree.
2591
2591
 
2592
- Measured on 2026-09-14 against a Tesla P100 server with torch 2.5.1 cu121: libcudart 12.1 resolves 425 driver symbols by name and the stand-in `libcuda.so.1` exports 20, so CUDA initialization fails with `cudaErrorInsufficientDriver`. The private `cuGetExportTable` blocks adding symbols one by one, and PyTorch kernels arrive through fatbinary registration that `cuModuleLoadData` does not see. Operator forwarding depends on PyTorch's public extension points instead of the driver's private ones. The Rust crates in `letify-core/` and `letify.remoting.probe` remain in the tree and the wheels, and nothing on the `host="local"` path calls them.
2592
+ Measured on 2026-09-14 against a Tesla P100 server with torch 2.5.1 cu121: libcudart 12.1 resolves 425 driver symbols by name and the stand-in `libcuda.so.1` exports 20, so CUDA initialization fails with `cudaErrorInsufficientDriver`. The private `cuGetExportTable` blocks adding symbols one by one, and PyTorch kernels arrive through fatbinary registration that `cuModuleLoadData` does not see. Operator forwarding depends on PyTorch's public extension points instead of the driver's private ones.
2593
+
2594
+ The crates `letify-wire`, `letify-driver` and `letify-agent` and the module `letify.remoting.probe` are not in the tree. The code is at the git tag `archive/cuda-driver-forwarding`. A stand-in at the CUDA runtime API layer, which libcudart does not need driver symbols for, is proposed in [pull request 74](https://github.com/DarkPyonix/letify/pull/74). `letify-core/` holds the crates `letify` and `letify-quic`.
2593
2595
 
2594
2596
  ## Command line
2595
2597
 
@@ -2615,7 +2617,7 @@ A heading, an alias at the top of a block and a table's column names are bold. S
2615
2617
 
2616
2618
  A `LetifyError` that reaches the command line prints `✗ <message>` on standard error and exits with 1. An argument error is argparse's own and exits with 2.
2617
2619
 
2618
- `--json` prints the records with no styling on `providers`, `devices`, `status`, `usage`, `utilization`, `probe` and `efficiency`.
2620
+ `--json` prints the records with no styling on `providers`, `devices`, `status`, `usage`, `utilization` and `efficiency`.
2619
2621
 
2620
2622
  Connection decision lines stay on standard error as `letify: <message>`, with the content set by Transport. When standard error is a terminal with colour, the `letify:` prefix is dim and nothing else changes.
2621
2623
 
@@ -2627,7 +2629,6 @@ Connection decision lines stay on standard error as `letify: <message>`, with th
2627
2629
  | `devices` | A table `PROVIDER ACCELERATORS`, the accelerators joined by `, `. `--json` is the mapping from alias to accelerator names |
2628
2630
  | `status` | The header `<name> <live> live, <busy> busy`, one block per live runtime, then a table `PROVIDER ACCELERATOR RESERVED INDICES` with reserved as `<reserved>/<count>`. With no runtime the blocks are replaced by `no live session in this process`. `--json` is `Launcher.status()` |
2629
2631
  | `usage`, `utilization` | As Remaining usage and GPU utilization describe |
2630
- | `probe` | A mark and `forwarding usable`, `forwarding usable but costly` or `forwarding not usable`, then aligned `platform`, `core`, `agent` and `round trip` fields and the reason, dim. `--json` is the capability record |
2631
2632
  | `efficiency` | `<p>% of a direct run`. `--json` is `{"efficiency": <fraction>}` |
2632
2633
  | `check` | `✓ <alias> answers`, then the machine's output indented by 2 spaces |
2633
2634
  | `login` | `✓ <alias> declared in <home>`, or `! <alias> was already declared in <home>, so nothing was asked for`, then `✓ <alias> referenced in <project>, which is safe to commit` |
@@ -2754,7 +2755,7 @@ Provider tools run out of process and never in the user's `.venv`. Colab runs th
2754
2755
 
2755
2756
  uv must be installed. letify finds it from the `UV` environment variable, then from `PATH`. If neither has it, letify raises an error that says uv is required.
2756
2757
 
2757
- The Python code is pure and links no Python extension. letify-core binaries are built in CI and placed in `letify/remoting/lib/`, so each release has one wheel per desktop platform, tagged `py3-none-<platform>`:
2758
+ The Python code is pure and links no Python extension. The `letify-quic` binary of letify-core is built in CI and placed in `letify/remoting/lib/`, so each release has one wheel per desktop platform, tagged `py3-none-<platform>`:
2758
2759
 
2759
2760
  | Platform | Wheel platform tag |
2760
2761
  |---|---|
@@ -2765,7 +2766,7 @@ The Python code is pure and links no Python extension. letify-core binaries are
2765
2766
  | macOS arm64 | `macosx_11_0_arm64` |
2766
2767
  | macOS x86_64 | `macosx_10_12_x86_64` |
2767
2768
 
2768
- Linux wheels are built inside the `manylinux_2_28` containers, so the binaries need glibc 2.28 or newer. That covers RHEL 8, Debian 10 and Ubuntu 18.10 onward. The sdist carries no binaries, and an install from it has no letify-core.
2769
+ Linux wheels are built inside the `manylinux_2_28` containers, so the binaries need glibc 2.28 or newer. That covers RHEL 8, Debian 10 and Ubuntu 18.10 onward. The sdist carries no binaries, and an install from it has no `letify-quic`, so the QUIC strategy is skipped.
2769
2770
 
2770
2771
  ### Versioning and releases <!-- id: versioning -->
2771
2772
 
@@ -2776,7 +2777,7 @@ Linux wheels are built inside the `manylinux_2_28` containers, so the binaries n
2776
2777
  | File | Field |
2777
2778
  |---|---|
2778
2779
  | `letify/__init__.py` | `__version__` |
2779
- | `letify-core/Cargo.toml` | `[workspace.package].version`, and the `letify-wire`, `letify-driver` and `letify-agent` entries in `Cargo.lock` |
2780
+ | `letify-core/Cargo.toml` | `[workspace.package].version`, and the `letify` and `letify-quic` entries in `Cargo.lock` |
2780
2781
  | `letify-ext/package.json` | `version`, and `version` and `packages[""].version` in `package-lock.json`, when `letify-ext/` exists |
2781
2782
  | Git tag | `v<version>` |
2782
2783
 
@@ -2784,6 +2785,10 @@ Linux wheels are built inside the `manylinux_2_28` containers, so the binaries n
2784
2785
 
2785
2786
  Pushing a tag `v*` runs `.github/workflows/publish.yml`. It builds the sdist, the six platform wheels above and, when `letify-ext/` exists, the extension's `letify-ext-<version>.vsix` after its unit tests pass. It then creates the GitHub Release for the tag with every one of those files attached, and uploads the sdist and wheels to PyPI. The extension is not published to the VS Code Marketplace.
2786
2787
 
2788
+ The same tag also runs `.github/workflows/publish-crates.yml`, which publishes the crate `letify` to crates.io at the same version. `letify` is the Rust face of the project: a library crate in `letify-core/letify` that re-exports the letify components meant for use outside letify, the first being the connection library `letify-link` once it exists. Every other crate in `letify-core/` sets `publish = false`, because each is a binary shipped inside the wheels and tied to the Python package of the same version. The workflow checks before it uploads anything: the tag agrees with every version file, the tagged commit is on `develop`, the workspace builds and its tests pass, and `cargo publish -p letify --dry-run` packages and builds the crate on its own. The upload comes last, because crates.io never replaces a version: a published number can be yanked but is taken forever. It runs in the GitHub environment `crates-io` with the repository secret `CARGO_REGISTRY_TOKEN`, a crates.io API token scoped to publishing `letify` and the future `letify-*` crates. A tag pushed before that secret exists fails at the upload and publishes nothing.
2789
+
2790
+ A `letify-*` crate name is reserved before its first release by pushing the tag `reserve/<crate>`, which runs `.github/workflows/reserve-crates.yml` and publishes version 0.0.0 of that name with no code and a README naming the letify release that ships it. A name crates.io already has is skipped, and a name not of the form `letify-<word>` is refused. The real crate then publishes at letify's version like every other, so 0.0.0 stays below every real release. `letify-link` and `letify-proto` are reserved this way, for letify 3.0.
2791
+
2787
2792
  ## Known gaps
2788
2793
 
2789
2794
  > Implemented and unimplemented, stated plainly so nobody builds on a promise.
@@ -86,7 +86,7 @@ torch.cuda.set_sync_debug_mode("warn")
86
86
  This does not depend on where the GPU is, so it can be measured on any CUDA device, including a Colab runtime. Under `host="local"`, `letify.remoting.device.current_client().stats` counts it for you: `round_trips` is `k` and `ops` is `n`. With `k` measured and the round trip measured, the formula gives you a real answer instead of an estimate.
87
87
 
88
88
  ```bash
89
- letify probe gpu.lab.example.edu # round trip, and whether forwarding is possible
89
+ ping -c 5 gpu.lab.example.edu # round trip
90
90
  ```
91
91
 
92
92
  ## Tuning `k` down
@@ -148,7 +148,7 @@ Colab figures assume the 600 credit pack at 49.99 USD, which is about 0.083 USD
148
148
  ## Checking before you spend
149
149
 
150
150
  ```bash
151
- letify probe gpu.lab.example.edu # round trip, and whether forwarding is viable
151
+ ping -c 5 gpu.lab.example.edu # round trip, which sets what forwarding costs
152
152
  letify check lab_a100 # does the machine answer at all?
153
153
  letify login shell lab_a100 # declare the machine before spending on it
154
154
  letify usage # what is left on each account
@@ -242,7 +242,7 @@ Pulling 20 GB from a bucket next to the runtime is 40 to 60 seconds against 27 m
242
242
  If `host="local"`, you are forwarding PyTorch operators and paying a round trip per host synchronization. Measure both terms:
243
243
 
244
244
  ```bash
245
- letify probe gpu.lab.example.edu
245
+ ping -c 5 gpu.lab.example.edu
246
246
  ```
247
247
 
248
248
  ```python
@@ -1,6 +1,6 @@
1
1
  # Releasing
2
2
 
3
- A release is one version number pushed as the tag `v<version>`. GitHub Actions builds everything and attaches it to a GitHub Release.
3
+ A release is one version number pushed as the tag `v<version>`. GitHub Actions builds everything, attaches it to a GitHub Release and publishes the crate `letify` to crates.io.
4
4
 
5
5
  ## The version rule
6
6
 
@@ -24,7 +24,7 @@ The `ci` workflow runs `check` on every push and pull request.
24
24
  git commit -am "Chore: Set the version to 1.2.0"
25
25
  ```
26
26
 
27
- 2. Bring the commit to `main` as usual, then tag that commit and push the tag:
27
+ 2. Tag that commit on `develop` and push the tag. The crates.io workflow refuses a tag whose commit is not in `develop`'s history:
28
28
 
29
29
  ```
30
30
  git tag v1.2.0
@@ -38,6 +38,17 @@ The `ci` workflow runs `check` on every push and pull request.
38
38
 
39
39
  4. It creates the GitHub Release `v1.2.0` with all of those files attached, and uploads the sdist and wheels to PyPI through trusted publishing.
40
40
 
41
+ 5. The `publish-crates` workflow runs on the same tag. Its `verify` job checks the version files, that the tagged commit is on `develop`, the workspace build and tests, and `cargo publish -p letify --dry-run`. Its `publish-crate` job then uploads the crate `letify` 1.2.0 to crates.io. If that version is already there, the job says so and succeeds without uploading. crates.io never replaces a version, so a wrong upload can only be yanked.
42
+
43
+ ## One-time setup for crates.io
44
+
45
+ 1. On crates.io, create an API token with the scopes `publish-new` and `publish-update`, limited to the crate patterns `letify` and `letify-*`.
46
+ 2. In the GitHub repository settings, create an environment named `crates-io`.
47
+ 3. Add the token to that environment as the secret `CARGO_REGISTRY_TOKEN`.
48
+ 4. Optionally add a required reviewer to the environment, so each crates.io upload waits for an approval.
49
+
50
+ A tag pushed before step 3 passes `verify` and fails at the upload with a message naming the missing secret. Nothing is published.
51
+
41
52
  A failed run publishes nothing to the Release. Fix the cause, delete the tag with `git push origin :v1.2.0`, and tag again.
42
53
 
43
54
  ## Installing the extension from a release
@@ -86,7 +86,7 @@ torch.cuda.set_sync_debug_mode("warn")
86
86
  GPU가 어디 있는지와 무관하므로 Colab 런타임을 포함해 어떤 CUDA 장치에서도 잴 수 있습니다. `host="local"`에서는 `letify.remoting.device.current_client().stats`가 대신 셉니다. `round_trips`가 `k`이고 `ops`가 `n`입니다. `k`와 왕복을 둘 다 재면 계산식이 추정이 아니라 실제 답을 줍니다.
87
87
 
88
88
  ```bash
89
- letify probe gpu.lab.example.edu # 왕복과 중계 가능 여부
89
+ ping -c 5 gpu.lab.example.edu # 왕복
90
90
  ```
91
91
 
92
92
  ## `k`를 줄이기
@@ -142,7 +142,7 @@ Colab 수치는 49.99달러 600 크레딧 팩 기준으로 크레딧당 약 0.08
142
142
  ## 쓰기 전에 확인하기
143
143
 
144
144
  ```bash
145
- letify probe gpu.lab.example.edu # 왕복, 그리고 중계가 쓸 만한지
145
+ ping -c 5 gpu.lab.example.edu # 왕복, 중계 비용을 정합니다
146
146
  letify login shell lab_a100 # 돈을 쓰기 전에 머신을 먼저 등록
147
147
  letify usage # 계정마다 남은 사용량
148
148
  letify utilization # 선언된 GPU가 지금 얼마나 바쁜가
@@ -242,7 +242,7 @@ def train(lr): ...
242
242
  `host="local"`이면 PyTorch 연산을 중계하고 있고 동기화마다 왕복을 내고 있습니다. 두 항을 모두 재세요.
243
243
 
244
244
  ```bash
245
- letify probe gpu.lab.example.edu
245
+ ping -c 5 gpu.lab.example.edu
246
246
  ```
247
247
 
248
248
  ```python