herds 0.1.0__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 (259) hide show
  1. herds-0.1.0/.gitignore +34 -0
  2. herds-0.1.0/DESIGN.md +113 -0
  3. herds-0.1.0/PKG-INFO +293 -0
  4. herds-0.1.0/README.md +256 -0
  5. herds-0.1.0/ROADMAP.md +57 -0
  6. herds-0.1.0/assets/dashboard.png +0 -0
  7. herds-0.1.0/assets/machine.png +0 -0
  8. herds-0.1.0/assets/sandbox.png +0 -0
  9. herds-0.1.0/assets/volumes.png +0 -0
  10. herds-0.1.0/examples/claude_agent.py +58 -0
  11. herds-0.1.0/examples/quickstart.py +43 -0
  12. herds-0.1.0/examples/remote_function.py +46 -0
  13. herds-0.1.0/pyproject.toml +61 -0
  14. herds-0.1.0/scripts/build_release.sh +19 -0
  15. herds-0.1.0/src/herds/__init__.py +51 -0
  16. herds-0.1.0/src/herds/cli/__init__.py +491 -0
  17. herds-0.1.0/src/herds/config.py +170 -0
  18. herds-0.1.0/src/herds/control/__init__.py +810 -0
  19. herds-0.1.0/src/herds/control/store.py +567 -0
  20. herds-0.1.0/src/herds/daemon/__init__.py +258 -0
  21. herds-0.1.0/src/herds/daemon/__main__.py +6 -0
  22. herds-0.1.0/src/herds/daemon/executor.py +330 -0
  23. herds-0.1.0/src/herds/daemon/files.py +79 -0
  24. herds-0.1.0/src/herds/daemon/images.py +106 -0
  25. herds-0.1.0/src/herds/daemon/machine.py +69 -0
  26. herds-0.1.0/src/herds/daemon/metrics.py +57 -0
  27. herds-0.1.0/src/herds/host.py +369 -0
  28. herds-0.1.0/src/herds/protocol.py +259 -0
  29. herds-0.1.0/src/herds/relay.py +633 -0
  30. herds-0.1.0/src/herds/sdk/__init__.py +28 -0
  31. herds-0.1.0/src/herds/sdk/app.py +155 -0
  32. herds-0.1.0/src/herds/sdk/client.py +179 -0
  33. herds-0.1.0/src/herds/sdk/image.py +62 -0
  34. herds-0.1.0/src/herds/sdk/mac.py +171 -0
  35. herds-0.1.0/src/herds/sdk/sandbox.py +163 -0
  36. herds-0.1.0/src/herds/sdk/secret.py +46 -0
  37. herds-0.1.0/src/herds/sdk/volume.py +31 -0
  38. herds-0.1.0/src/herds/skill.py +74 -0
  39. herds-0.1.0/src/herds/web_dist/404.html +1 -0
  40. herds-0.1.0/src/herds/web_dist/__next.__PAGE__.txt +9 -0
  41. herds-0.1.0/src/herds/web_dist/__next._full.txt +22 -0
  42. herds-0.1.0/src/herds/web_dist/__next._head.txt +5 -0
  43. herds-0.1.0/src/herds/web_dist/__next._index.txt +8 -0
  44. herds-0.1.0/src/herds/web_dist/__next._tree.txt +4 -0
  45. herds-0.1.0/src/herds/web_dist/_next/static/chunks/02zcztjkk52mq.js +1 -0
  46. herds-0.1.0/src/herds/web_dist/_next/static/chunks/05e83anyrmnuv.js +1 -0
  47. herds-0.1.0/src/herds/web_dist/_next/static/chunks/0755w0rpp670_.js +1 -0
  48. herds-0.1.0/src/herds/web_dist/_next/static/chunks/0cz1d0mv5g_q7.js +1 -0
  49. herds-0.1.0/src/herds/web_dist/_next/static/chunks/0i36h7rkvi9ay.js +1 -0
  50. herds-0.1.0/src/herds/web_dist/_next/static/chunks/0iyrc8fu-0ayb.js +9 -0
  51. herds-0.1.0/src/herds/web_dist/_next/static/chunks/0pq0d6vtp3kvw.js +1 -0
  52. herds-0.1.0/src/herds/web_dist/_next/static/chunks/0tumh1559hcih.js +1 -0
  53. herds-0.1.0/src/herds/web_dist/_next/static/chunks/120ol6mue7vp2.js +1 -0
  54. herds-0.1.0/src/herds/web_dist/_next/static/chunks/1gn-yoma2aujf.js +5 -0
  55. herds-0.1.0/src/herds/web_dist/_next/static/chunks/1op8bvg-0q6tl.js +1 -0
  56. herds-0.1.0/src/herds/web_dist/_next/static/chunks/1pn5tud4t835x.js +9 -0
  57. herds-0.1.0/src/herds/web_dist/_next/static/chunks/1sfqbf5qbd8y-.js +0 -0
  58. herds-0.1.0/src/herds/web_dist/_next/static/chunks/248b_461ikafx.js +1 -0
  59. herds-0.1.0/src/herds/web_dist/_next/static/chunks/2c-aykqsty-dq.js +1 -0
  60. herds-0.1.0/src/herds/web_dist/_next/static/chunks/2j4_bxbfhamp6.js +1 -0
  61. herds-0.1.0/src/herds/web_dist/_next/static/chunks/2sp6kvnbpdi-p.js +31 -0
  62. herds-0.1.0/src/herds/web_dist/_next/static/chunks/2vmk9ipzrtj92.js +4 -0
  63. herds-0.1.0/src/herds/web_dist/_next/static/chunks/37gl9pjlkkam1.js +1 -0
  64. herds-0.1.0/src/herds/web_dist/_next/static/chunks/3aspsawddjes8.js +2 -0
  65. herds-0.1.0/src/herds/web_dist/_next/static/chunks/3iww1zdedzdwz.js +1 -0
  66. herds-0.1.0/src/herds/web_dist/_next/static/chunks/3j_4r2-hqycta.js +1 -0
  67. herds-0.1.0/src/herds/web_dist/_next/static/chunks/3oi-7u0vpjh9h.js +1 -0
  68. herds-0.1.0/src/herds/web_dist/_next/static/chunks/3sqbodmsyaqkm.js +1 -0
  69. herds-0.1.0/src/herds/web_dist/_next/static/chunks/3v-rqdys4eu0a.js +0 -0
  70. herds-0.1.0/src/herds/web_dist/_next/static/chunks/413vhtqaffkli.css +3 -0
  71. herds-0.1.0/src/herds/web_dist/_next/static/chunks/415in7dhfqo3x.js +1 -0
  72. herds-0.1.0/src/herds/web_dist/_next/static/chunks/44nsp8_bkorwp.js +0 -0
  73. herds-0.1.0/src/herds/web_dist/_next/static/chunks/turbopack-0oar_l0ke-f9y.js +1 -0
  74. herds-0.1.0/src/herds/web_dist/_next/static/media/GeistMono_Variable.p.3ms9vq719j3f8.woff2 +0 -0
  75. herds-0.1.0/src/herds/web_dist/_next/static/media/Geist_Variable-s.p.0mrjj4bg00-he.woff2 +0 -0
  76. herds-0.1.0/src/herds/web_dist/_next/static/vje3Zo17YIetwDl0k5oV4/_buildManifest.js +11 -0
  77. herds-0.1.0/src/herds/web_dist/_next/static/vje3Zo17YIetwDl0k5oV4/_clientMiddlewareManifest.js +1 -0
  78. herds-0.1.0/src/herds/web_dist/_next/static/vje3Zo17YIetwDl0k5oV4/_ssgManifest.js +1 -0
  79. herds-0.1.0/src/herds/web_dist/_not-found/__next._full.txt +18 -0
  80. herds-0.1.0/src/herds/web_dist/_not-found/__next._head.txt +5 -0
  81. herds-0.1.0/src/herds/web_dist/_not-found/__next._index.txt +8 -0
  82. herds-0.1.0/src/herds/web_dist/_not-found/__next._not-found.__PAGE__.txt +5 -0
  83. herds-0.1.0/src/herds/web_dist/_not-found/__next._not-found.txt +5 -0
  84. herds-0.1.0/src/herds/web_dist/_not-found/__next._tree.txt +2 -0
  85. herds-0.1.0/src/herds/web_dist/_not-found.html +1 -0
  86. herds-0.1.0/src/herds/web_dist/_not-found.txt +18 -0
  87. herds-0.1.0/src/herds/web_dist/dashboard/__next._full.txt +24 -0
  88. herds-0.1.0/src/herds/web_dist/dashboard/__next._head.txt +5 -0
  89. herds-0.1.0/src/herds/web_dist/dashboard/__next._index.txt +8 -0
  90. herds-0.1.0/src/herds/web_dist/dashboard/__next._tree.txt +4 -0
  91. herds-0.1.0/src/herds/web_dist/dashboard/__next.dashboard.__PAGE__.txt +9 -0
  92. herds-0.1.0/src/herds/web_dist/dashboard/__next.dashboard.txt +5 -0
  93. herds-0.1.0/src/herds/web_dist/dashboard.html +1 -0
  94. herds-0.1.0/src/herds/web_dist/dashboard.txt +24 -0
  95. herds-0.1.0/src/herds/web_dist/index.html +9 -0
  96. herds-0.1.0/src/herds/web_dist/index.txt +22 -0
  97. herds-0.1.0/src/herds/web_dist/login/__next._full.txt +24 -0
  98. herds-0.1.0/src/herds/web_dist/login/__next._head.txt +5 -0
  99. herds-0.1.0/src/herds/web_dist/login/__next._index.txt +8 -0
  100. herds-0.1.0/src/herds/web_dist/login/__next._tree.txt +4 -0
  101. herds-0.1.0/src/herds/web_dist/login/__next.login.__PAGE__.txt +9 -0
  102. herds-0.1.0/src/herds/web_dist/login/__next.login.txt +5 -0
  103. herds-0.1.0/src/herds/web_dist/login.html +1 -0
  104. herds-0.1.0/src/herds/web_dist/login.txt +24 -0
  105. herds-0.1.0/src/herds/web_dist/machine/__next._full.txt +24 -0
  106. herds-0.1.0/src/herds/web_dist/machine/__next._head.txt +5 -0
  107. herds-0.1.0/src/herds/web_dist/machine/__next._index.txt +8 -0
  108. herds-0.1.0/src/herds/web_dist/machine/__next._tree.txt +4 -0
  109. herds-0.1.0/src/herds/web_dist/machine/__next.machine.__PAGE__.txt +9 -0
  110. herds-0.1.0/src/herds/web_dist/machine/__next.machine.txt +5 -0
  111. herds-0.1.0/src/herds/web_dist/machine.html +1 -0
  112. herds-0.1.0/src/herds/web_dist/machine.txt +24 -0
  113. herds-0.1.0/src/herds/web_dist/machines/__next._full.txt +24 -0
  114. herds-0.1.0/src/herds/web_dist/machines/__next._head.txt +5 -0
  115. herds-0.1.0/src/herds/web_dist/machines/__next._index.txt +8 -0
  116. herds-0.1.0/src/herds/web_dist/machines/__next._tree.txt +4 -0
  117. herds-0.1.0/src/herds/web_dist/machines/__next.machines.__PAGE__.txt +9 -0
  118. herds-0.1.0/src/herds/web_dist/machines/__next.machines.txt +5 -0
  119. herds-0.1.0/src/herds/web_dist/machines.html +1 -0
  120. herds-0.1.0/src/herds/web_dist/machines.txt +24 -0
  121. herds-0.1.0/src/herds/web_dist/runs/__next._full.txt +24 -0
  122. herds-0.1.0/src/herds/web_dist/runs/__next._head.txt +5 -0
  123. herds-0.1.0/src/herds/web_dist/runs/__next._index.txt +8 -0
  124. herds-0.1.0/src/herds/web_dist/runs/__next._tree.txt +4 -0
  125. herds-0.1.0/src/herds/web_dist/runs/__next.runs.__PAGE__.txt +9 -0
  126. herds-0.1.0/src/herds/web_dist/runs/__next.runs.txt +5 -0
  127. herds-0.1.0/src/herds/web_dist/runs.html +1 -0
  128. herds-0.1.0/src/herds/web_dist/runs.txt +24 -0
  129. herds-0.1.0/src/herds/web_dist/sandbox/__next._full.txt +24 -0
  130. herds-0.1.0/src/herds/web_dist/sandbox/__next._head.txt +5 -0
  131. herds-0.1.0/src/herds/web_dist/sandbox/__next._index.txt +8 -0
  132. herds-0.1.0/src/herds/web_dist/sandbox/__next._tree.txt +4 -0
  133. herds-0.1.0/src/herds/web_dist/sandbox/__next.sandbox.__PAGE__.txt +9 -0
  134. herds-0.1.0/src/herds/web_dist/sandbox/__next.sandbox.txt +5 -0
  135. herds-0.1.0/src/herds/web_dist/sandbox.html +1 -0
  136. herds-0.1.0/src/herds/web_dist/sandbox.txt +24 -0
  137. herds-0.1.0/src/herds/web_dist/sandboxes/__next._full.txt +24 -0
  138. herds-0.1.0/src/herds/web_dist/sandboxes/__next._head.txt +5 -0
  139. herds-0.1.0/src/herds/web_dist/sandboxes/__next._index.txt +8 -0
  140. herds-0.1.0/src/herds/web_dist/sandboxes/__next._tree.txt +4 -0
  141. herds-0.1.0/src/herds/web_dist/sandboxes/__next.sandboxes.__PAGE__.txt +9 -0
  142. herds-0.1.0/src/herds/web_dist/sandboxes/__next.sandboxes.txt +5 -0
  143. herds-0.1.0/src/herds/web_dist/sandboxes.html +1 -0
  144. herds-0.1.0/src/herds/web_dist/sandboxes.txt +24 -0
  145. herds-0.1.0/src/herds/web_dist/secrets/__next._full.txt +24 -0
  146. herds-0.1.0/src/herds/web_dist/secrets/__next._head.txt +5 -0
  147. herds-0.1.0/src/herds/web_dist/secrets/__next._index.txt +8 -0
  148. herds-0.1.0/src/herds/web_dist/secrets/__next._tree.txt +4 -0
  149. herds-0.1.0/src/herds/web_dist/secrets/__next.secrets.__PAGE__.txt +9 -0
  150. herds-0.1.0/src/herds/web_dist/secrets/__next.secrets.txt +5 -0
  151. herds-0.1.0/src/herds/web_dist/secrets.html +1 -0
  152. herds-0.1.0/src/herds/web_dist/secrets.txt +24 -0
  153. herds-0.1.0/src/herds/web_dist/settings/__next._full.txt +24 -0
  154. herds-0.1.0/src/herds/web_dist/settings/__next._head.txt +5 -0
  155. herds-0.1.0/src/herds/web_dist/settings/__next._index.txt +8 -0
  156. herds-0.1.0/src/herds/web_dist/settings/__next._tree.txt +4 -0
  157. herds-0.1.0/src/herds/web_dist/settings/__next.settings.__PAGE__.txt +9 -0
  158. herds-0.1.0/src/herds/web_dist/settings/__next.settings.txt +5 -0
  159. herds-0.1.0/src/herds/web_dist/settings.html +5 -0
  160. herds-0.1.0/src/herds/web_dist/settings.txt +24 -0
  161. herds-0.1.0/src/herds/web_dist/signup/__next._full.txt +24 -0
  162. herds-0.1.0/src/herds/web_dist/signup/__next._head.txt +5 -0
  163. herds-0.1.0/src/herds/web_dist/signup/__next._index.txt +8 -0
  164. herds-0.1.0/src/herds/web_dist/signup/__next._tree.txt +4 -0
  165. herds-0.1.0/src/herds/web_dist/signup/__next.signup.__PAGE__.txt +9 -0
  166. herds-0.1.0/src/herds/web_dist/signup/__next.signup.txt +5 -0
  167. herds-0.1.0/src/herds/web_dist/signup.html +1 -0
  168. herds-0.1.0/src/herds/web_dist/signup.txt +24 -0
  169. herds-0.1.0/src/herds/web_dist/skill/__next._full.txt +24 -0
  170. herds-0.1.0/src/herds/web_dist/skill/__next._head.txt +5 -0
  171. herds-0.1.0/src/herds/web_dist/skill/__next._index.txt +8 -0
  172. herds-0.1.0/src/herds/web_dist/skill/__next._tree.txt +4 -0
  173. herds-0.1.0/src/herds/web_dist/skill/__next.skill.__PAGE__.txt +9 -0
  174. herds-0.1.0/src/herds/web_dist/skill/__next.skill.txt +5 -0
  175. herds-0.1.0/src/herds/web_dist/skill.html +1 -0
  176. herds-0.1.0/src/herds/web_dist/skill.md +67 -0
  177. herds-0.1.0/src/herds/web_dist/skill.txt +24 -0
  178. herds-0.1.0/src/herds/web_dist/volume/__next._full.txt +24 -0
  179. herds-0.1.0/src/herds/web_dist/volume/__next._head.txt +5 -0
  180. herds-0.1.0/src/herds/web_dist/volume/__next._index.txt +8 -0
  181. herds-0.1.0/src/herds/web_dist/volume/__next._tree.txt +4 -0
  182. herds-0.1.0/src/herds/web_dist/volume/__next.volume.__PAGE__.txt +9 -0
  183. herds-0.1.0/src/herds/web_dist/volume/__next.volume.txt +5 -0
  184. herds-0.1.0/src/herds/web_dist/volume.html +1 -0
  185. herds-0.1.0/src/herds/web_dist/volume.txt +24 -0
  186. herds-0.1.0/src/herds/web_dist/volumes/__next._full.txt +24 -0
  187. herds-0.1.0/src/herds/web_dist/volumes/__next._head.txt +5 -0
  188. herds-0.1.0/src/herds/web_dist/volumes/__next._index.txt +8 -0
  189. herds-0.1.0/src/herds/web_dist/volumes/__next._tree.txt +4 -0
  190. herds-0.1.0/src/herds/web_dist/volumes/__next.volumes.__PAGE__.txt +9 -0
  191. herds-0.1.0/src/herds/web_dist/volumes/__next.volumes.txt +5 -0
  192. herds-0.1.0/src/herds/web_dist/volumes.html +1 -0
  193. herds-0.1.0/src/herds/web_dist/volumes.txt +24 -0
  194. herds-0.1.0/src/herds/web_dist/welcome/__next._full.txt +24 -0
  195. herds-0.1.0/src/herds/web_dist/welcome/__next._head.txt +5 -0
  196. herds-0.1.0/src/herds/web_dist/welcome/__next._index.txt +8 -0
  197. herds-0.1.0/src/herds/web_dist/welcome/__next._tree.txt +4 -0
  198. herds-0.1.0/src/herds/web_dist/welcome/__next.welcome.__PAGE__.txt +9 -0
  199. herds-0.1.0/src/herds/web_dist/welcome/__next.welcome.txt +5 -0
  200. herds-0.1.0/src/herds/web_dist/welcome.html +1 -0
  201. herds-0.1.0/src/herds/web_dist/welcome.txt +24 -0
  202. herds-0.1.0/tests/test_executor.py +96 -0
  203. herds-0.1.0/tests/test_protocol.py +40 -0
  204. herds-0.1.0/tests/test_store_and_images.py +47 -0
  205. herds-0.1.0/web/.gitignore +1 -0
  206. herds-0.1.0/web/.vercel/README.txt +11 -0
  207. herds-0.1.0/web/.vercel/project.json +1 -0
  208. herds-0.1.0/web/app/dashboard/page.tsx +167 -0
  209. herds-0.1.0/web/app/globals.css +111 -0
  210. herds-0.1.0/web/app/layout.tsx +31 -0
  211. herds-0.1.0/web/app/login/page.tsx +179 -0
  212. herds-0.1.0/web/app/machine/page.tsx +132 -0
  213. herds-0.1.0/web/app/machines/page.tsx +76 -0
  214. herds-0.1.0/web/app/page.tsx +262 -0
  215. herds-0.1.0/web/app/runs/page.tsx +122 -0
  216. herds-0.1.0/web/app/sandbox/page.tsx +345 -0
  217. herds-0.1.0/web/app/sandboxes/page.tsx +238 -0
  218. herds-0.1.0/web/app/secrets/page.tsx +203 -0
  219. herds-0.1.0/web/app/settings/page.tsx +151 -0
  220. herds-0.1.0/web/app/signup/page.tsx +189 -0
  221. herds-0.1.0/web/app/skill/page.tsx +141 -0
  222. herds-0.1.0/web/app/template.tsx +16 -0
  223. herds-0.1.0/web/app/volume/page.tsx +56 -0
  224. herds-0.1.0/web/app/volumes/page.tsx +104 -0
  225. herds-0.1.0/web/app/welcome/page.tsx +184 -0
  226. herds-0.1.0/web/components/AppChrome.tsx +27 -0
  227. herds-0.1.0/web/components/CommandPalette.tsx +194 -0
  228. herds-0.1.0/web/components/FileBrowser.tsx +196 -0
  229. herds-0.1.0/web/components/JobsTable.tsx +80 -0
  230. herds-0.1.0/web/components/LiveTail.tsx +54 -0
  231. herds-0.1.0/web/components/LogDrawer.tsx +103 -0
  232. herds-0.1.0/web/components/Logo.tsx +26 -0
  233. herds-0.1.0/web/components/NavProgress.tsx +46 -0
  234. herds-0.1.0/web/components/NewSandboxModal.tsx +120 -0
  235. herds-0.1.0/web/components/OfflineBanner.tsx +15 -0
  236. herds-0.1.0/web/components/Onboarding.tsx +51 -0
  237. herds-0.1.0/web/components/Skeleton.tsx +31 -0
  238. herds-0.1.0/web/components/Toast.tsx +51 -0
  239. herds-0.1.0/web/components/TokenGate.tsx +75 -0
  240. herds-0.1.0/web/components/TopNav.tsx +119 -0
  241. herds-0.1.0/web/components/charts.tsx +199 -0
  242. herds-0.1.0/web/components/platform/Landing.tsx +777 -0
  243. herds-0.1.0/web/components/platform/WorldMap.tsx +40 -0
  244. herds-0.1.0/web/components/platform/world-grid.ts +5 -0
  245. herds-0.1.0/web/components/ui.tsx +100 -0
  246. herds-0.1.0/web/components/widgets.tsx +177 -0
  247. herds-0.1.0/web/lib/api.ts +308 -0
  248. herds-0.1.0/web/lib/format.ts +35 -0
  249. herds-0.1.0/web/lib/highlight.ts +0 -0
  250. herds-0.1.0/web/lib/platform.ts +164 -0
  251. herds-0.1.0/web/next-env.d.ts +6 -0
  252. herds-0.1.0/web/next.config.mjs +10 -0
  253. herds-0.1.0/web/package.json +31 -0
  254. herds-0.1.0/web/pnpm-lock.yaml +4292 -0
  255. herds-0.1.0/web/postcss.config.mjs +6 -0
  256. herds-0.1.0/web/public/skill.md +67 -0
  257. herds-0.1.0/web/scripts/gen_map.mjs +39 -0
  258. herds-0.1.0/web/tailwind.config.ts +51 -0
  259. herds-0.1.0/web/tsconfig.json +41 -0
herds-0.1.0/.gitignore ADDED
@@ -0,0 +1,34 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+
14
+ # Node / Next.js (dashboard)
15
+ web/node_modules/
16
+ web/.next/
17
+ web/out/
18
+ web/.env*.local
19
+ *.tsbuildinfo
20
+
21
+ # Generated: the dashboard is built into the wheel by scripts/build_release.sh
22
+ src/herds/web_dist/
23
+
24
+ # Local Herds state / data
25
+ *.db
26
+ *.sqlite
27
+ .herds/
28
+ host_token
29
+
30
+ # OS / editor
31
+ .DS_Store
32
+ *.swp
33
+ .idea/
34
+ .vscode/
herds-0.1.0/DESIGN.md ADDED
@@ -0,0 +1,113 @@
1
+ # Herds — Design
2
+
3
+ This document explains *why* Herds is built the way it is. The product thesis:
4
+
5
+ > Connect your Mac to the internet and turn it into a programmable runtime.
6
+ > **Every Mac becomes an API.**
7
+
8
+ The developer surface deliberately mirrors Modal (`App`, `Image`, `Volume`,
9
+ `Sandbox`, `mac.run()`) because that mental model already lives in developers'
10
+ heads — but the runtime is the user's *own* Mac.
11
+
12
+ ## Three components
13
+
14
+ ### 1. Control plane (`herds.control`)
15
+ A small FastAPI service. It does exactly three things:
16
+
17
+ 1. Holds the persistent WebSocket from each Mac's daemon.
18
+ 2. Accepts `exec` requests from the SDK (REST) and pushes them down the right
19
+ agent socket.
20
+ 3. Fans streamed stdout/stderr/exit frames back to the listening SDK client,
21
+ correlated by `request_id`.
22
+
23
+ Durable facts — machines, API keys, device tokens, job history — live in SQLite
24
+ (`herds.control.store`). Live connection state and the log fan-out are
25
+ in-memory. **It never stores volumes, sandboxes, images, or caches.** Those stay
26
+ on the Mac. The Mac is the cloud.
27
+
28
+ ### 2. Daemon / agent (`herds.daemon`)
29
+ Runs on the Mac. Holds **one persistent outbound WebSocket** to the control
30
+ plane and services commands pushed down it, streaming results back up.
31
+
32
+ Why outbound WebSocket? The Mac is behind NAT/firewall — nothing on the internet
33
+ can dial *in*. Every tool that solves this (GitHub Actions self-hosted runners,
34
+ Tailscale's DERP fallback, Cloudflare Tunnel, ngrok, Temporal/Modal workers)
35
+ uses the same move: the machine dials home and work comes back down the open
36
+ connection. It runs over 443, traverses corporate proxies, and needs no
37
+ port-forwarding. gRPC bidi streaming is the documented graduation path once
38
+ WebSocket framing becomes the bottleneck.
39
+
40
+ The daemon installs as a launchd **LaunchAgent** (runs as the user, with their
41
+ toolchains and keychain — not root), with `KeepAlive` for auto-restart and
42
+ reconnect-with-backoff in the loop.
43
+
44
+ ### 3. SDK + CLI (`herds.sdk`, `herds.cli`)
45
+ The SDK is synchronous (user code is ordinary blocking Python): httpx to start a
46
+ job, the `websockets` sync client to stream logs back into a `Result`. The CLI
47
+ is Typer + Rich.
48
+
49
+ ## The execution model (`herds.daemon.executor`)
50
+
51
+ This is where the real macOS work happens. Each **sandbox** is the unit of
52
+ isolation:
53
+
54
+ - **Own directory tree** — `~/.herds/sandboxes/<id>/{workspace,tmp,home}`.
55
+ - **Clean environment** — rebuilt from an allowlist (`env -i` style). `HOME`,
56
+ `TMPDIR`, and the common toolchain caches (`DERIVED_DATA_PATH`,
57
+ `npm_config_cache`, `PIP_CACHE_DIR`, `CARGO_HOME`, `XDG_*`) are redirected
58
+ *into* the sandbox so concurrent jobs never clobber each other's caches.
59
+ - **Own process session** — `start_new_session=True`, so a timeout or cancel
60
+ kills the entire process tree (`killpg`), not just the parent. macOS has no
61
+ cgroups; process-group teardown is the lifecycle primitive.
62
+ - **Write-fence** — when `sandbox-exec` is present, the command is wrapped in a
63
+ Seatbelt profile that confines writes to the sandbox + mounted volumes and can
64
+ cut the network. This is workspace confinement for *trusted* code (the user
65
+ owns the Mac), not an adversarial jail.
66
+
67
+ Everything degrades gracefully: a missing tool never hard-fails a run.
68
+
69
+ ### Images = environment recipes
70
+ On a Mac an `Image` isn't a container; it's a recipe resolved on the host
71
+ (`herds.daemon.images`):
72
+
73
+ - `Image.xcode("26")` → selects `DEVELOPER_DIR` (per-process — never clobbers a
74
+ concurrent job the way `xcode-select --switch` would), via `xcodes` if needed.
75
+ - `Image.node("22")` / `Image.python("3.13")` → pins through `mise`.
76
+
77
+ If the toolchain isn't installed, the command runs on the host and Herds
78
+ reports what it *would* have pinned (surfaced as a `herds:` stderr note).
79
+
80
+ ### Volumes = persistent directories
81
+ A `Volume` is a named directory under `~/.herds/volumes/<name>`. Mounted into a
82
+ run, it's symlinked under the working directory at the mount name and exposed via
83
+ `$HERDS_VOLUME_<NAME>`. No commit/reload — a local dir is durable on write — but
84
+ the no-op `commit()`/`reload()` methods exist so Modal code ports unchanged.
85
+
86
+ ## Wire protocol (`herds.protocol`)
87
+
88
+ One module holds every message shape so the three components can't drift. Frames
89
+ carry a `type`, a `request_id` (correlation), an optional `seq` (ordering/loss
90
+ detection), and a `data` payload. The agent socket multiplexes many concurrent
91
+ commands over one connection by `request_id`. A late log subscriber misses
92
+ nothing: the control plane buffers a request's frames and replays them on
93
+ subscribe.
94
+
95
+ ## Why this shape scales without a rewrite
96
+
97
+ - **In-memory fan-out → Redis pub/sub**: the moment there's more than one control
98
+ plane process, swap the in-memory subscriber map for Redis. The boundary is
99
+ drawn at `Hub.publish` / `Hub.subscribe`.
100
+ - **SQLite → Postgres**: `Store` is the only thing touching the DB.
101
+ - **Host-process isolation → Tart VMs**: `Sandbox` and `Image` are interfaces; a
102
+ Tart backend resolves `Image.xcode("26")` to an OCI tag and a `Volume` to a
103
+ virtio-fs `--dir` share, with no SDK change. (Constraint to design around:
104
+ Apple's EULA caps macOS VMs at **2 per physical host**, so macOS-VM slots are a
105
+ scarce per-machine resource while host-process sandboxes scale with CPU.)
106
+
107
+ ## Auth (roadmap-ready, permissive by default)
108
+
109
+ The store already models API keys (SDK → control plane) and device tokens
110
+ (daemon → control plane). For local use auth is off so the magic works
111
+ instantly; set `HERDS_REQUIRE_AUTH=1` to enforce keys and machine-ownership
112
+ ACLs. The intended `herds connect` flow is OAuth device authorization → a
113
+ per-machine device token, exactly the IoT-style enrollment pattern.
herds-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,293 @@
1
+ Metadata-Version: 2.4
2
+ Name: herds
3
+ Version: 0.1.0
4
+ Summary: Connect your Mac to the internet and turn it into a programmable runtime. Modal, for Macs.
5
+ Project-URL: Homepage, https://herds.run
6
+ Project-URL: Repository, https://github.com/teddyoweh/herds
7
+ Project-URL: Issues, https://github.com/teddyoweh/herds/issues
8
+ Author-email: Spawn Labs <teddy@spawnlabs.ai>
9
+ License-Expression: Apache-2.0
10
+ Keywords: agents,ci,mac,macos,modal,runtime,sandbox,xcode
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: MacOS X
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Build Tools
20
+ Classifier: Topic :: System :: Distributed Computing
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: fastapi>=0.115
23
+ Requires-Dist: httpx>=0.27
24
+ Requires-Dist: pydantic>=2.7
25
+ Requires-Dist: rich>=13.7
26
+ Requires-Dist: typer>=0.12
27
+ Requires-Dist: uvicorn[standard]>=0.30
28
+ Requires-Dist: websockets>=13.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: anyio>=4.0; extra == 'dev'
31
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
32
+ Requires-Dist: pytest>=8.0; extra == 'dev'
33
+ Provides-Extra: relay
34
+ Requires-Dist: psycopg-pool>=3.2; extra == 'relay'
35
+ Requires-Dist: psycopg[binary]>=3.1; extra == 'relay'
36
+ Description-Content-Type: text/markdown
37
+
38
+ <div align="center">
39
+
40
+ # 🍎 Herds
41
+
42
+ **Connect your Mac to the internet and turn it into a programmable runtime.**
43
+
44
+ *Modal, for Macs.*
45
+
46
+ <br/>
47
+
48
+ ![Herds dashboard](https://raw.githubusercontent.com/teddyoweh/herds/main/assets/dashboard.png)
49
+
50
+ </div>
51
+
52
+ ---
53
+
54
+ Herds makes any Mac you own into a runtime that agents, SDKs, CLIs, cron jobs,
55
+ and applications can execute against from anywhere. Install the daemon, sign in,
56
+ and your Mac becomes an API.
57
+
58
+ ```python
59
+ import herds
60
+
61
+ mac = herds.mac()
62
+ result = mac.run("xcodebuild -scheme MyApp build")
63
+ print(result.stdout)
64
+ ```
65
+
66
+ Nobody cares about SSH. Nobody cares about Tailscale. Nobody cares about machine
67
+ management. **They just have a Mac.**
68
+
69
+ ## The mental model
70
+
71
+ It's not "rent Macs." It's not "manage servers." It's not "a CI system."
72
+
73
+ > **Every Mac becomes an API.**
74
+
75
+ The developer surface intentionally echoes Modal, so the mental model transfers
76
+ directly — `App`, `Image`, `Volume`, `Sandbox` — except the runtime is *your
77
+ Mac*, and Apple's licensing makes that something Modal/AWS structurally can't
78
+ offer as dense rented cloud. Your Mac, already licensed, is the cloud.
79
+
80
+ ## Architecture
81
+
82
+ Three small pieces. Your Mac never opens an inbound port; the daemon dials home
83
+ over a persistent WebSocket (the same NAT-traversal pattern as GitHub Actions
84
+ runners, Tailscale, and Cloudflare Tunnel), and commands are pushed back down
85
+ that socket.
86
+
87
+ ```
88
+ ┌─────────────┐ REST: start a job ┌──────────────┐ WS (agent dials home) ┌─────────────┐
89
+ │ Python SDK │ ───────────────────► │ Control Plane│ ◄────────────────────── │ Mac Daemon │
90
+ │ + CLI │ ◄═══ WS: stream logs ══ │ (FastAPI) │ ═══ exec / stdout ════► │ (executor) │
91
+ └─────────────┘ └──────────────┘ └─────────────┘
92
+ herds.mac().run() sqlite + fan-out your real Mac
93
+ ```
94
+
95
+ The control plane is deliberately tiny — it remembers *who owns what* and job
96
+ status. **Volumes, sandboxes, images, and caches never leave the Mac.** The Mac
97
+ is the cloud.
98
+
99
+ ## Quickstart
100
+
101
+ ```bash
102
+ pip install herds # or: uv tool install herds
103
+ ```
104
+
105
+ ### Self-host in one command
106
+
107
+ `herds host` turns this Mac into a self-hosted Herds: control plane + the full
108
+ web dashboard + a secure public link + this Mac as a compute node — one process,
109
+ one SQLite file, no managed infrastructure.
110
+
111
+ ```bash
112
+ herds host
113
+ # ✓ Herds host is live
114
+ # Dashboard https://<you>.trycloudflare.com (or a permanent Tailscale Funnel link)
115
+ # Host token herds_sk_…
116
+ # Add a Mac herds connect https://… herds_sk_…
117
+ ```
118
+
119
+ Open the link, paste the token once, and you're in. Other Macs join the pool with
120
+ `herds connect <link> <token>`. For a **permanent** link, run `herds host setup`
121
+ once to enable Tailscale Funnel (free).
122
+
123
+ ### Or drive it from Python
124
+
125
+ ```python
126
+ import herds
127
+
128
+ mac = herds.mac()
129
+ print(mac.run("sw_vers").stdout)
130
+ print(mac.run("xcodebuild -version").stdout)
131
+ ```
132
+
133
+ ## The SDK
134
+
135
+ ### Run commands
136
+
137
+ ```python
138
+ mac = herds.mac()
139
+
140
+ # blocking, returns a Result(exit_code, stdout, stderr, duration_ms)
141
+ r = mac.run("swift build", check=True)
142
+
143
+ # stream output live to your terminal
144
+ mac.run("npm test", stream=True)
145
+
146
+ # iterate output yourself
147
+ for stream, line in mac.stream("xcodebuild build"):
148
+ handle(line)
149
+ ```
150
+
151
+ ### Images — environment recipes resolved on the Mac
152
+
153
+ ```python
154
+ mac.run("xcodebuild build", image=herds.Image.xcode("26")) # selects DEVELOPER_DIR
155
+ mac.run("node --version", image=herds.Image.node("22")) # pins via mise
156
+ mac.run("python script.py", image=herds.Image.python("3.13"))
157
+ ```
158
+
159
+ On a Mac an Image isn't a container — it's a recipe that selects the right Xcode
160
+ (`DEVELOPER_DIR`, never clobbering concurrent jobs) or runtime (`mise`). If a
161
+ toolchain isn't installed, the command still runs against the host and Herds
162
+ tells you what it would have pinned.
163
+
164
+ ### Volumes — persistent directories on the Mac
165
+
166
+ ```python
167
+ vol = herds.Volume.from_name("ios-builds")
168
+ # Reachable as ./builds (relative to the working dir) and via the env var.
169
+ mac.run("xcodebuild archive -archivePath $HERDS_VOLUME_IOS_BUILDS/App.xcarchive",
170
+ volumes={"builds": vol})
171
+ ```
172
+
173
+ On a bare Mac there's no container, so a volume is mounted under the working
174
+ directory at the mount name *and* exposed as an absolute path through
175
+ `$HERDS_VOLUME_<NAME>` — both unambiguous. (Absolute `/workspace`-style mounts
176
+ arrive with the Tart VM backend.)
177
+
178
+ ### Sandboxes — isolated, persistent workspaces
179
+
180
+ ```python
181
+ with herds.Sandbox.create(image="xcode:26") as sbx:
182
+ sbx.exec("git clone https://github.com/me/app .")
183
+ sbx.exec("xcodebuild -scheme App build", check=True)
184
+ ```
185
+
186
+ Each sandbox is its own directory tree with redirected `HOME`/`TMPDIR` and
187
+ toolchain caches, its own process session (so timeouts kill the whole tree), and
188
+ an optional `sandbox-exec` write-fence. Files persist between `exec` calls.
189
+
190
+ ### Expose a server — a sandbox becomes a URL
191
+
192
+ ```python
193
+ sbx.spawn("python -m http.server 8000", keep_alive=True)
194
+ url = sbx.expose(8000) # → https://<you>.trycloudflare.com/p/<sbx>/8000/
195
+ ```
196
+
197
+ Run a web app or API inside a sandbox and get a hittable public link. Requests
198
+ tunnel through the agent WebSocket — control plane → daemon → the sandbox's
199
+ `localhost:port` — so it works behind NAT with no inbound ports. With a wildcard
200
+ domain you get named subdomains (`https://myapi--teddy.herds.run`).
201
+
202
+ ### Apps & functions — run real Python on your Mac
203
+
204
+ ```python
205
+ app = herds.App("builds")
206
+
207
+ @app.function(image=herds.Image.python("3.13"))
208
+ def inspect(target: str) -> dict:
209
+ import platform
210
+ return {"target": target, "ran_on": platform.node()}
211
+
212
+ @app.local_entrypoint()
213
+ def main():
214
+ print(inspect.remote("release")) # ships source, runs on the Mac
215
+ ```
216
+
217
+ ## The dashboard
218
+
219
+ `herds host` serves a full web dashboard — bundled into the package as a static
220
+ build, served by the control plane (no Node.js at runtime). Live metrics, a
221
+ sandbox explorer with exposed ports, a deep file browser for volumes, secrets,
222
+ run history — all polling the same API the SDK and CLI use.
223
+
224
+ | | |
225
+ |:--:|:--:|
226
+ | ![Machine](https://raw.githubusercontent.com/teddyoweh/herds/main/assets/machine.png) | ![Sandbox](https://raw.githubusercontent.com/teddyoweh/herds/main/assets/sandbox.png) |
227
+ | *Per-Mac live gauges* | *Sandboxes — activity + exposed ports* |
228
+ | ![Volumes](https://raw.githubusercontent.com/teddyoweh/herds/main/assets/volumes.png) | |
229
+ | *Volumes — a real file explorer* | |
230
+
231
+ ## The CLI
232
+
233
+ ```
234
+ herds host self-host: control plane + dashboard + public link
235
+ herds host setup enable a permanent Tailscale Funnel link
236
+ herds connect <link> <token> join another Mac to a host
237
+ herds serve run a bare control plane locally
238
+ herds machines list your connected Macs
239
+ herds run -- <cmd> run a command on a Mac (streams output)
240
+ herds shell -c <cmd> one-off command (SSH-equivalent)
241
+ herds logs recent jobs
242
+ herds status local configuration
243
+ herds volume ls|create|rm
244
+ herds image ls toolchain images available on this Mac
245
+ herds install launchd LaunchAgent — stay online on login
246
+ herds uninstall
247
+ ```
248
+
249
+ ## Isolation, honestly
250
+
251
+ The MVP isolates with per-sandbox directories, a clean allowlisted environment,
252
+ process-group teardown, and (when available) a `sandbox-exec` write-fence. This
253
+ is the right model for *trusted* code — the user owns the Mac and runs their own
254
+ builds — and it starts instantly.
255
+
256
+ The documented next tier is **Tart** VMs (Apple's Virtualization.framework, OCI
257
+ images, near-instant APFS copy-on-write clones) for true OS-level isolation, and
258
+ Apple's native `container` for Linux jobs on macOS 26. The `Image`/`Volume`/
259
+ `Sandbox` API is drawn so those become a backend swap, not an API change. See
260
+ [`DESIGN.md`](DESIGN.md) and [`ROADMAP.md`](ROADMAP.md).
261
+
262
+ ## Apple licensing — the moat
263
+
264
+ Apple's macOS SLA limits virtualization to **2 VMs per physical Mac** and forbids
265
+ "service bureau / time-sharing." The BYO-Mac model sidesteps this: the Mac and
266
+ its macOS license belong to *you*, so Herds runs as personal/dev use on hardware
267
+ you own — which is exactly what the license permits and what makes "Modal for
268
+ Macs" both accurate and hard to copy as a rented-fleet cloud.
269
+
270
+ ## Build from source
271
+
272
+ ```bash
273
+ git clone https://github.com/teddyoweh/herds
274
+ cd herds
275
+ uv venv && uv pip install -e ".[dev]"
276
+ uv run pytest # backend tests
277
+ ./scripts/build_release.sh # build the dashboard + wheel (with UI bundled)
278
+ ```
279
+
280
+ The dashboard lives in `web/` (Next.js, static-exported). `scripts/build_release.sh`
281
+ exports it and bundles it into the wheel, so `pip install` ships the whole UI.
282
+
283
+ ## Status
284
+
285
+ Works today, end-to-end: `herds host` (control plane + bundled dashboard +
286
+ public tunnel + token auth), connect Macs, run commands, stream logs, mount
287
+ volumes, drive sandboxes, expose ports as URLs, run remote Python. See
288
+ [`ROADMAP.md`](ROADMAP.md) for what's next (self-hostable tunnel relay, Tart VM
289
+ backend, code-shipping for functions).
290
+
291
+ ## License
292
+
293
+ Apache-2.0
herds-0.1.0/README.md ADDED
@@ -0,0 +1,256 @@
1
+ <div align="center">
2
+
3
+ # 🍎 Herds
4
+
5
+ **Connect your Mac to the internet and turn it into a programmable runtime.**
6
+
7
+ *Modal, for Macs.*
8
+
9
+ <br/>
10
+
11
+ ![Herds dashboard](https://raw.githubusercontent.com/teddyoweh/herds/main/assets/dashboard.png)
12
+
13
+ </div>
14
+
15
+ ---
16
+
17
+ Herds makes any Mac you own into a runtime that agents, SDKs, CLIs, cron jobs,
18
+ and applications can execute against from anywhere. Install the daemon, sign in,
19
+ and your Mac becomes an API.
20
+
21
+ ```python
22
+ import herds
23
+
24
+ mac = herds.mac()
25
+ result = mac.run("xcodebuild -scheme MyApp build")
26
+ print(result.stdout)
27
+ ```
28
+
29
+ Nobody cares about SSH. Nobody cares about Tailscale. Nobody cares about machine
30
+ management. **They just have a Mac.**
31
+
32
+ ## The mental model
33
+
34
+ It's not "rent Macs." It's not "manage servers." It's not "a CI system."
35
+
36
+ > **Every Mac becomes an API.**
37
+
38
+ The developer surface intentionally echoes Modal, so the mental model transfers
39
+ directly — `App`, `Image`, `Volume`, `Sandbox` — except the runtime is *your
40
+ Mac*, and Apple's licensing makes that something Modal/AWS structurally can't
41
+ offer as dense rented cloud. Your Mac, already licensed, is the cloud.
42
+
43
+ ## Architecture
44
+
45
+ Three small pieces. Your Mac never opens an inbound port; the daemon dials home
46
+ over a persistent WebSocket (the same NAT-traversal pattern as GitHub Actions
47
+ runners, Tailscale, and Cloudflare Tunnel), and commands are pushed back down
48
+ that socket.
49
+
50
+ ```
51
+ ┌─────────────┐ REST: start a job ┌──────────────┐ WS (agent dials home) ┌─────────────┐
52
+ │ Python SDK │ ───────────────────► │ Control Plane│ ◄────────────────────── │ Mac Daemon │
53
+ │ + CLI │ ◄═══ WS: stream logs ══ │ (FastAPI) │ ═══ exec / stdout ════► │ (executor) │
54
+ └─────────────┘ └──────────────┘ └─────────────┘
55
+ herds.mac().run() sqlite + fan-out your real Mac
56
+ ```
57
+
58
+ The control plane is deliberately tiny — it remembers *who owns what* and job
59
+ status. **Volumes, sandboxes, images, and caches never leave the Mac.** The Mac
60
+ is the cloud.
61
+
62
+ ## Quickstart
63
+
64
+ ```bash
65
+ pip install herds # or: uv tool install herds
66
+ ```
67
+
68
+ ### Self-host in one command
69
+
70
+ `herds host` turns this Mac into a self-hosted Herds: control plane + the full
71
+ web dashboard + a secure public link + this Mac as a compute node — one process,
72
+ one SQLite file, no managed infrastructure.
73
+
74
+ ```bash
75
+ herds host
76
+ # ✓ Herds host is live
77
+ # Dashboard https://<you>.trycloudflare.com (or a permanent Tailscale Funnel link)
78
+ # Host token herds_sk_…
79
+ # Add a Mac herds connect https://… herds_sk_…
80
+ ```
81
+
82
+ Open the link, paste the token once, and you're in. Other Macs join the pool with
83
+ `herds connect <link> <token>`. For a **permanent** link, run `herds host setup`
84
+ once to enable Tailscale Funnel (free).
85
+
86
+ ### Or drive it from Python
87
+
88
+ ```python
89
+ import herds
90
+
91
+ mac = herds.mac()
92
+ print(mac.run("sw_vers").stdout)
93
+ print(mac.run("xcodebuild -version").stdout)
94
+ ```
95
+
96
+ ## The SDK
97
+
98
+ ### Run commands
99
+
100
+ ```python
101
+ mac = herds.mac()
102
+
103
+ # blocking, returns a Result(exit_code, stdout, stderr, duration_ms)
104
+ r = mac.run("swift build", check=True)
105
+
106
+ # stream output live to your terminal
107
+ mac.run("npm test", stream=True)
108
+
109
+ # iterate output yourself
110
+ for stream, line in mac.stream("xcodebuild build"):
111
+ handle(line)
112
+ ```
113
+
114
+ ### Images — environment recipes resolved on the Mac
115
+
116
+ ```python
117
+ mac.run("xcodebuild build", image=herds.Image.xcode("26")) # selects DEVELOPER_DIR
118
+ mac.run("node --version", image=herds.Image.node("22")) # pins via mise
119
+ mac.run("python script.py", image=herds.Image.python("3.13"))
120
+ ```
121
+
122
+ On a Mac an Image isn't a container — it's a recipe that selects the right Xcode
123
+ (`DEVELOPER_DIR`, never clobbering concurrent jobs) or runtime (`mise`). If a
124
+ toolchain isn't installed, the command still runs against the host and Herds
125
+ tells you what it would have pinned.
126
+
127
+ ### Volumes — persistent directories on the Mac
128
+
129
+ ```python
130
+ vol = herds.Volume.from_name("ios-builds")
131
+ # Reachable as ./builds (relative to the working dir) and via the env var.
132
+ mac.run("xcodebuild archive -archivePath $HERDS_VOLUME_IOS_BUILDS/App.xcarchive",
133
+ volumes={"builds": vol})
134
+ ```
135
+
136
+ On a bare Mac there's no container, so a volume is mounted under the working
137
+ directory at the mount name *and* exposed as an absolute path through
138
+ `$HERDS_VOLUME_<NAME>` — both unambiguous. (Absolute `/workspace`-style mounts
139
+ arrive with the Tart VM backend.)
140
+
141
+ ### Sandboxes — isolated, persistent workspaces
142
+
143
+ ```python
144
+ with herds.Sandbox.create(image="xcode:26") as sbx:
145
+ sbx.exec("git clone https://github.com/me/app .")
146
+ sbx.exec("xcodebuild -scheme App build", check=True)
147
+ ```
148
+
149
+ Each sandbox is its own directory tree with redirected `HOME`/`TMPDIR` and
150
+ toolchain caches, its own process session (so timeouts kill the whole tree), and
151
+ an optional `sandbox-exec` write-fence. Files persist between `exec` calls.
152
+
153
+ ### Expose a server — a sandbox becomes a URL
154
+
155
+ ```python
156
+ sbx.spawn("python -m http.server 8000", keep_alive=True)
157
+ url = sbx.expose(8000) # → https://<you>.trycloudflare.com/p/<sbx>/8000/
158
+ ```
159
+
160
+ Run a web app or API inside a sandbox and get a hittable public link. Requests
161
+ tunnel through the agent WebSocket — control plane → daemon → the sandbox's
162
+ `localhost:port` — so it works behind NAT with no inbound ports. With a wildcard
163
+ domain you get named subdomains (`https://myapi--teddy.herds.run`).
164
+
165
+ ### Apps & functions — run real Python on your Mac
166
+
167
+ ```python
168
+ app = herds.App("builds")
169
+
170
+ @app.function(image=herds.Image.python("3.13"))
171
+ def inspect(target: str) -> dict:
172
+ import platform
173
+ return {"target": target, "ran_on": platform.node()}
174
+
175
+ @app.local_entrypoint()
176
+ def main():
177
+ print(inspect.remote("release")) # ships source, runs on the Mac
178
+ ```
179
+
180
+ ## The dashboard
181
+
182
+ `herds host` serves a full web dashboard — bundled into the package as a static
183
+ build, served by the control plane (no Node.js at runtime). Live metrics, a
184
+ sandbox explorer with exposed ports, a deep file browser for volumes, secrets,
185
+ run history — all polling the same API the SDK and CLI use.
186
+
187
+ | | |
188
+ |:--:|:--:|
189
+ | ![Machine](https://raw.githubusercontent.com/teddyoweh/herds/main/assets/machine.png) | ![Sandbox](https://raw.githubusercontent.com/teddyoweh/herds/main/assets/sandbox.png) |
190
+ | *Per-Mac live gauges* | *Sandboxes — activity + exposed ports* |
191
+ | ![Volumes](https://raw.githubusercontent.com/teddyoweh/herds/main/assets/volumes.png) | |
192
+ | *Volumes — a real file explorer* | |
193
+
194
+ ## The CLI
195
+
196
+ ```
197
+ herds host self-host: control plane + dashboard + public link
198
+ herds host setup enable a permanent Tailscale Funnel link
199
+ herds connect <link> <token> join another Mac to a host
200
+ herds serve run a bare control plane locally
201
+ herds machines list your connected Macs
202
+ herds run -- <cmd> run a command on a Mac (streams output)
203
+ herds shell -c <cmd> one-off command (SSH-equivalent)
204
+ herds logs recent jobs
205
+ herds status local configuration
206
+ herds volume ls|create|rm
207
+ herds image ls toolchain images available on this Mac
208
+ herds install launchd LaunchAgent — stay online on login
209
+ herds uninstall
210
+ ```
211
+
212
+ ## Isolation, honestly
213
+
214
+ The MVP isolates with per-sandbox directories, a clean allowlisted environment,
215
+ process-group teardown, and (when available) a `sandbox-exec` write-fence. This
216
+ is the right model for *trusted* code — the user owns the Mac and runs their own
217
+ builds — and it starts instantly.
218
+
219
+ The documented next tier is **Tart** VMs (Apple's Virtualization.framework, OCI
220
+ images, near-instant APFS copy-on-write clones) for true OS-level isolation, and
221
+ Apple's native `container` for Linux jobs on macOS 26. The `Image`/`Volume`/
222
+ `Sandbox` API is drawn so those become a backend swap, not an API change. See
223
+ [`DESIGN.md`](DESIGN.md) and [`ROADMAP.md`](ROADMAP.md).
224
+
225
+ ## Apple licensing — the moat
226
+
227
+ Apple's macOS SLA limits virtualization to **2 VMs per physical Mac** and forbids
228
+ "service bureau / time-sharing." The BYO-Mac model sidesteps this: the Mac and
229
+ its macOS license belong to *you*, so Herds runs as personal/dev use on hardware
230
+ you own — which is exactly what the license permits and what makes "Modal for
231
+ Macs" both accurate and hard to copy as a rented-fleet cloud.
232
+
233
+ ## Build from source
234
+
235
+ ```bash
236
+ git clone https://github.com/teddyoweh/herds
237
+ cd herds
238
+ uv venv && uv pip install -e ".[dev]"
239
+ uv run pytest # backend tests
240
+ ./scripts/build_release.sh # build the dashboard + wheel (with UI bundled)
241
+ ```
242
+
243
+ The dashboard lives in `web/` (Next.js, static-exported). `scripts/build_release.sh`
244
+ exports it and bundles it into the wheel, so `pip install` ships the whole UI.
245
+
246
+ ## Status
247
+
248
+ Works today, end-to-end: `herds host` (control plane + bundled dashboard +
249
+ public tunnel + token auth), connect Macs, run commands, stream logs, mount
250
+ volumes, drive sandboxes, expose ports as URLs, run remote Python. See
251
+ [`ROADMAP.md`](ROADMAP.md) for what's next (self-hostable tunnel relay, Tart VM
252
+ backend, code-shipping for functions).
253
+
254
+ ## License
255
+
256
+ Apache-2.0