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.
- herds-0.1.0/.gitignore +34 -0
- herds-0.1.0/DESIGN.md +113 -0
- herds-0.1.0/PKG-INFO +293 -0
- herds-0.1.0/README.md +256 -0
- herds-0.1.0/ROADMAP.md +57 -0
- herds-0.1.0/assets/dashboard.png +0 -0
- herds-0.1.0/assets/machine.png +0 -0
- herds-0.1.0/assets/sandbox.png +0 -0
- herds-0.1.0/assets/volumes.png +0 -0
- herds-0.1.0/examples/claude_agent.py +58 -0
- herds-0.1.0/examples/quickstart.py +43 -0
- herds-0.1.0/examples/remote_function.py +46 -0
- herds-0.1.0/pyproject.toml +61 -0
- herds-0.1.0/scripts/build_release.sh +19 -0
- herds-0.1.0/src/herds/__init__.py +51 -0
- herds-0.1.0/src/herds/cli/__init__.py +491 -0
- herds-0.1.0/src/herds/config.py +170 -0
- herds-0.1.0/src/herds/control/__init__.py +810 -0
- herds-0.1.0/src/herds/control/store.py +567 -0
- herds-0.1.0/src/herds/daemon/__init__.py +258 -0
- herds-0.1.0/src/herds/daemon/__main__.py +6 -0
- herds-0.1.0/src/herds/daemon/executor.py +330 -0
- herds-0.1.0/src/herds/daemon/files.py +79 -0
- herds-0.1.0/src/herds/daemon/images.py +106 -0
- herds-0.1.0/src/herds/daemon/machine.py +69 -0
- herds-0.1.0/src/herds/daemon/metrics.py +57 -0
- herds-0.1.0/src/herds/host.py +369 -0
- herds-0.1.0/src/herds/protocol.py +259 -0
- herds-0.1.0/src/herds/relay.py +633 -0
- herds-0.1.0/src/herds/sdk/__init__.py +28 -0
- herds-0.1.0/src/herds/sdk/app.py +155 -0
- herds-0.1.0/src/herds/sdk/client.py +179 -0
- herds-0.1.0/src/herds/sdk/image.py +62 -0
- herds-0.1.0/src/herds/sdk/mac.py +171 -0
- herds-0.1.0/src/herds/sdk/sandbox.py +163 -0
- herds-0.1.0/src/herds/sdk/secret.py +46 -0
- herds-0.1.0/src/herds/sdk/volume.py +31 -0
- herds-0.1.0/src/herds/skill.py +74 -0
- herds-0.1.0/src/herds/web_dist/404.html +1 -0
- herds-0.1.0/src/herds/web_dist/__next.__PAGE__.txt +9 -0
- herds-0.1.0/src/herds/web_dist/__next._full.txt +22 -0
- herds-0.1.0/src/herds/web_dist/__next._head.txt +5 -0
- herds-0.1.0/src/herds/web_dist/__next._index.txt +8 -0
- herds-0.1.0/src/herds/web_dist/__next._tree.txt +4 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/02zcztjkk52mq.js +1 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/05e83anyrmnuv.js +1 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/0755w0rpp670_.js +1 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/0cz1d0mv5g_q7.js +1 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/0i36h7rkvi9ay.js +1 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/0iyrc8fu-0ayb.js +9 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/0pq0d6vtp3kvw.js +1 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/0tumh1559hcih.js +1 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/120ol6mue7vp2.js +1 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/1gn-yoma2aujf.js +5 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/1op8bvg-0q6tl.js +1 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/1pn5tud4t835x.js +9 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/1sfqbf5qbd8y-.js +0 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/248b_461ikafx.js +1 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/2c-aykqsty-dq.js +1 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/2j4_bxbfhamp6.js +1 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/2sp6kvnbpdi-p.js +31 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/2vmk9ipzrtj92.js +4 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/37gl9pjlkkam1.js +1 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/3aspsawddjes8.js +2 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/3iww1zdedzdwz.js +1 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/3j_4r2-hqycta.js +1 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/3oi-7u0vpjh9h.js +1 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/3sqbodmsyaqkm.js +1 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/3v-rqdys4eu0a.js +0 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/413vhtqaffkli.css +3 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/415in7dhfqo3x.js +1 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/44nsp8_bkorwp.js +0 -0
- herds-0.1.0/src/herds/web_dist/_next/static/chunks/turbopack-0oar_l0ke-f9y.js +1 -0
- herds-0.1.0/src/herds/web_dist/_next/static/media/GeistMono_Variable.p.3ms9vq719j3f8.woff2 +0 -0
- herds-0.1.0/src/herds/web_dist/_next/static/media/Geist_Variable-s.p.0mrjj4bg00-he.woff2 +0 -0
- herds-0.1.0/src/herds/web_dist/_next/static/vje3Zo17YIetwDl0k5oV4/_buildManifest.js +11 -0
- herds-0.1.0/src/herds/web_dist/_next/static/vje3Zo17YIetwDl0k5oV4/_clientMiddlewareManifest.js +1 -0
- herds-0.1.0/src/herds/web_dist/_next/static/vje3Zo17YIetwDl0k5oV4/_ssgManifest.js +1 -0
- herds-0.1.0/src/herds/web_dist/_not-found/__next._full.txt +18 -0
- herds-0.1.0/src/herds/web_dist/_not-found/__next._head.txt +5 -0
- herds-0.1.0/src/herds/web_dist/_not-found/__next._index.txt +8 -0
- herds-0.1.0/src/herds/web_dist/_not-found/__next._not-found.__PAGE__.txt +5 -0
- herds-0.1.0/src/herds/web_dist/_not-found/__next._not-found.txt +5 -0
- herds-0.1.0/src/herds/web_dist/_not-found/__next._tree.txt +2 -0
- herds-0.1.0/src/herds/web_dist/_not-found.html +1 -0
- herds-0.1.0/src/herds/web_dist/_not-found.txt +18 -0
- herds-0.1.0/src/herds/web_dist/dashboard/__next._full.txt +24 -0
- herds-0.1.0/src/herds/web_dist/dashboard/__next._head.txt +5 -0
- herds-0.1.0/src/herds/web_dist/dashboard/__next._index.txt +8 -0
- herds-0.1.0/src/herds/web_dist/dashboard/__next._tree.txt +4 -0
- herds-0.1.0/src/herds/web_dist/dashboard/__next.dashboard.__PAGE__.txt +9 -0
- herds-0.1.0/src/herds/web_dist/dashboard/__next.dashboard.txt +5 -0
- herds-0.1.0/src/herds/web_dist/dashboard.html +1 -0
- herds-0.1.0/src/herds/web_dist/dashboard.txt +24 -0
- herds-0.1.0/src/herds/web_dist/index.html +9 -0
- herds-0.1.0/src/herds/web_dist/index.txt +22 -0
- herds-0.1.0/src/herds/web_dist/login/__next._full.txt +24 -0
- herds-0.1.0/src/herds/web_dist/login/__next._head.txt +5 -0
- herds-0.1.0/src/herds/web_dist/login/__next._index.txt +8 -0
- herds-0.1.0/src/herds/web_dist/login/__next._tree.txt +4 -0
- herds-0.1.0/src/herds/web_dist/login/__next.login.__PAGE__.txt +9 -0
- herds-0.1.0/src/herds/web_dist/login/__next.login.txt +5 -0
- herds-0.1.0/src/herds/web_dist/login.html +1 -0
- herds-0.1.0/src/herds/web_dist/login.txt +24 -0
- herds-0.1.0/src/herds/web_dist/machine/__next._full.txt +24 -0
- herds-0.1.0/src/herds/web_dist/machine/__next._head.txt +5 -0
- herds-0.1.0/src/herds/web_dist/machine/__next._index.txt +8 -0
- herds-0.1.0/src/herds/web_dist/machine/__next._tree.txt +4 -0
- herds-0.1.0/src/herds/web_dist/machine/__next.machine.__PAGE__.txt +9 -0
- herds-0.1.0/src/herds/web_dist/machine/__next.machine.txt +5 -0
- herds-0.1.0/src/herds/web_dist/machine.html +1 -0
- herds-0.1.0/src/herds/web_dist/machine.txt +24 -0
- herds-0.1.0/src/herds/web_dist/machines/__next._full.txt +24 -0
- herds-0.1.0/src/herds/web_dist/machines/__next._head.txt +5 -0
- herds-0.1.0/src/herds/web_dist/machines/__next._index.txt +8 -0
- herds-0.1.0/src/herds/web_dist/machines/__next._tree.txt +4 -0
- herds-0.1.0/src/herds/web_dist/machines/__next.machines.__PAGE__.txt +9 -0
- herds-0.1.0/src/herds/web_dist/machines/__next.machines.txt +5 -0
- herds-0.1.0/src/herds/web_dist/machines.html +1 -0
- herds-0.1.0/src/herds/web_dist/machines.txt +24 -0
- herds-0.1.0/src/herds/web_dist/runs/__next._full.txt +24 -0
- herds-0.1.0/src/herds/web_dist/runs/__next._head.txt +5 -0
- herds-0.1.0/src/herds/web_dist/runs/__next._index.txt +8 -0
- herds-0.1.0/src/herds/web_dist/runs/__next._tree.txt +4 -0
- herds-0.1.0/src/herds/web_dist/runs/__next.runs.__PAGE__.txt +9 -0
- herds-0.1.0/src/herds/web_dist/runs/__next.runs.txt +5 -0
- herds-0.1.0/src/herds/web_dist/runs.html +1 -0
- herds-0.1.0/src/herds/web_dist/runs.txt +24 -0
- herds-0.1.0/src/herds/web_dist/sandbox/__next._full.txt +24 -0
- herds-0.1.0/src/herds/web_dist/sandbox/__next._head.txt +5 -0
- herds-0.1.0/src/herds/web_dist/sandbox/__next._index.txt +8 -0
- herds-0.1.0/src/herds/web_dist/sandbox/__next._tree.txt +4 -0
- herds-0.1.0/src/herds/web_dist/sandbox/__next.sandbox.__PAGE__.txt +9 -0
- herds-0.1.0/src/herds/web_dist/sandbox/__next.sandbox.txt +5 -0
- herds-0.1.0/src/herds/web_dist/sandbox.html +1 -0
- herds-0.1.0/src/herds/web_dist/sandbox.txt +24 -0
- herds-0.1.0/src/herds/web_dist/sandboxes/__next._full.txt +24 -0
- herds-0.1.0/src/herds/web_dist/sandboxes/__next._head.txt +5 -0
- herds-0.1.0/src/herds/web_dist/sandboxes/__next._index.txt +8 -0
- herds-0.1.0/src/herds/web_dist/sandboxes/__next._tree.txt +4 -0
- herds-0.1.0/src/herds/web_dist/sandboxes/__next.sandboxes.__PAGE__.txt +9 -0
- herds-0.1.0/src/herds/web_dist/sandboxes/__next.sandboxes.txt +5 -0
- herds-0.1.0/src/herds/web_dist/sandboxes.html +1 -0
- herds-0.1.0/src/herds/web_dist/sandboxes.txt +24 -0
- herds-0.1.0/src/herds/web_dist/secrets/__next._full.txt +24 -0
- herds-0.1.0/src/herds/web_dist/secrets/__next._head.txt +5 -0
- herds-0.1.0/src/herds/web_dist/secrets/__next._index.txt +8 -0
- herds-0.1.0/src/herds/web_dist/secrets/__next._tree.txt +4 -0
- herds-0.1.0/src/herds/web_dist/secrets/__next.secrets.__PAGE__.txt +9 -0
- herds-0.1.0/src/herds/web_dist/secrets/__next.secrets.txt +5 -0
- herds-0.1.0/src/herds/web_dist/secrets.html +1 -0
- herds-0.1.0/src/herds/web_dist/secrets.txt +24 -0
- herds-0.1.0/src/herds/web_dist/settings/__next._full.txt +24 -0
- herds-0.1.0/src/herds/web_dist/settings/__next._head.txt +5 -0
- herds-0.1.0/src/herds/web_dist/settings/__next._index.txt +8 -0
- herds-0.1.0/src/herds/web_dist/settings/__next._tree.txt +4 -0
- herds-0.1.0/src/herds/web_dist/settings/__next.settings.__PAGE__.txt +9 -0
- herds-0.1.0/src/herds/web_dist/settings/__next.settings.txt +5 -0
- herds-0.1.0/src/herds/web_dist/settings.html +5 -0
- herds-0.1.0/src/herds/web_dist/settings.txt +24 -0
- herds-0.1.0/src/herds/web_dist/signup/__next._full.txt +24 -0
- herds-0.1.0/src/herds/web_dist/signup/__next._head.txt +5 -0
- herds-0.1.0/src/herds/web_dist/signup/__next._index.txt +8 -0
- herds-0.1.0/src/herds/web_dist/signup/__next._tree.txt +4 -0
- herds-0.1.0/src/herds/web_dist/signup/__next.signup.__PAGE__.txt +9 -0
- herds-0.1.0/src/herds/web_dist/signup/__next.signup.txt +5 -0
- herds-0.1.0/src/herds/web_dist/signup.html +1 -0
- herds-0.1.0/src/herds/web_dist/signup.txt +24 -0
- herds-0.1.0/src/herds/web_dist/skill/__next._full.txt +24 -0
- herds-0.1.0/src/herds/web_dist/skill/__next._head.txt +5 -0
- herds-0.1.0/src/herds/web_dist/skill/__next._index.txt +8 -0
- herds-0.1.0/src/herds/web_dist/skill/__next._tree.txt +4 -0
- herds-0.1.0/src/herds/web_dist/skill/__next.skill.__PAGE__.txt +9 -0
- herds-0.1.0/src/herds/web_dist/skill/__next.skill.txt +5 -0
- herds-0.1.0/src/herds/web_dist/skill.html +1 -0
- herds-0.1.0/src/herds/web_dist/skill.md +67 -0
- herds-0.1.0/src/herds/web_dist/skill.txt +24 -0
- herds-0.1.0/src/herds/web_dist/volume/__next._full.txt +24 -0
- herds-0.1.0/src/herds/web_dist/volume/__next._head.txt +5 -0
- herds-0.1.0/src/herds/web_dist/volume/__next._index.txt +8 -0
- herds-0.1.0/src/herds/web_dist/volume/__next._tree.txt +4 -0
- herds-0.1.0/src/herds/web_dist/volume/__next.volume.__PAGE__.txt +9 -0
- herds-0.1.0/src/herds/web_dist/volume/__next.volume.txt +5 -0
- herds-0.1.0/src/herds/web_dist/volume.html +1 -0
- herds-0.1.0/src/herds/web_dist/volume.txt +24 -0
- herds-0.1.0/src/herds/web_dist/volumes/__next._full.txt +24 -0
- herds-0.1.0/src/herds/web_dist/volumes/__next._head.txt +5 -0
- herds-0.1.0/src/herds/web_dist/volumes/__next._index.txt +8 -0
- herds-0.1.0/src/herds/web_dist/volumes/__next._tree.txt +4 -0
- herds-0.1.0/src/herds/web_dist/volumes/__next.volumes.__PAGE__.txt +9 -0
- herds-0.1.0/src/herds/web_dist/volumes/__next.volumes.txt +5 -0
- herds-0.1.0/src/herds/web_dist/volumes.html +1 -0
- herds-0.1.0/src/herds/web_dist/volumes.txt +24 -0
- herds-0.1.0/src/herds/web_dist/welcome/__next._full.txt +24 -0
- herds-0.1.0/src/herds/web_dist/welcome/__next._head.txt +5 -0
- herds-0.1.0/src/herds/web_dist/welcome/__next._index.txt +8 -0
- herds-0.1.0/src/herds/web_dist/welcome/__next._tree.txt +4 -0
- herds-0.1.0/src/herds/web_dist/welcome/__next.welcome.__PAGE__.txt +9 -0
- herds-0.1.0/src/herds/web_dist/welcome/__next.welcome.txt +5 -0
- herds-0.1.0/src/herds/web_dist/welcome.html +1 -0
- herds-0.1.0/src/herds/web_dist/welcome.txt +24 -0
- herds-0.1.0/tests/test_executor.py +96 -0
- herds-0.1.0/tests/test_protocol.py +40 -0
- herds-0.1.0/tests/test_store_and_images.py +47 -0
- herds-0.1.0/web/.gitignore +1 -0
- herds-0.1.0/web/.vercel/README.txt +11 -0
- herds-0.1.0/web/.vercel/project.json +1 -0
- herds-0.1.0/web/app/dashboard/page.tsx +167 -0
- herds-0.1.0/web/app/globals.css +111 -0
- herds-0.1.0/web/app/layout.tsx +31 -0
- herds-0.1.0/web/app/login/page.tsx +179 -0
- herds-0.1.0/web/app/machine/page.tsx +132 -0
- herds-0.1.0/web/app/machines/page.tsx +76 -0
- herds-0.1.0/web/app/page.tsx +262 -0
- herds-0.1.0/web/app/runs/page.tsx +122 -0
- herds-0.1.0/web/app/sandbox/page.tsx +345 -0
- herds-0.1.0/web/app/sandboxes/page.tsx +238 -0
- herds-0.1.0/web/app/secrets/page.tsx +203 -0
- herds-0.1.0/web/app/settings/page.tsx +151 -0
- herds-0.1.0/web/app/signup/page.tsx +189 -0
- herds-0.1.0/web/app/skill/page.tsx +141 -0
- herds-0.1.0/web/app/template.tsx +16 -0
- herds-0.1.0/web/app/volume/page.tsx +56 -0
- herds-0.1.0/web/app/volumes/page.tsx +104 -0
- herds-0.1.0/web/app/welcome/page.tsx +184 -0
- herds-0.1.0/web/components/AppChrome.tsx +27 -0
- herds-0.1.0/web/components/CommandPalette.tsx +194 -0
- herds-0.1.0/web/components/FileBrowser.tsx +196 -0
- herds-0.1.0/web/components/JobsTable.tsx +80 -0
- herds-0.1.0/web/components/LiveTail.tsx +54 -0
- herds-0.1.0/web/components/LogDrawer.tsx +103 -0
- herds-0.1.0/web/components/Logo.tsx +26 -0
- herds-0.1.0/web/components/NavProgress.tsx +46 -0
- herds-0.1.0/web/components/NewSandboxModal.tsx +120 -0
- herds-0.1.0/web/components/OfflineBanner.tsx +15 -0
- herds-0.1.0/web/components/Onboarding.tsx +51 -0
- herds-0.1.0/web/components/Skeleton.tsx +31 -0
- herds-0.1.0/web/components/Toast.tsx +51 -0
- herds-0.1.0/web/components/TokenGate.tsx +75 -0
- herds-0.1.0/web/components/TopNav.tsx +119 -0
- herds-0.1.0/web/components/charts.tsx +199 -0
- herds-0.1.0/web/components/platform/Landing.tsx +777 -0
- herds-0.1.0/web/components/platform/WorldMap.tsx +40 -0
- herds-0.1.0/web/components/platform/world-grid.ts +5 -0
- herds-0.1.0/web/components/ui.tsx +100 -0
- herds-0.1.0/web/components/widgets.tsx +177 -0
- herds-0.1.0/web/lib/api.ts +308 -0
- herds-0.1.0/web/lib/format.ts +35 -0
- herds-0.1.0/web/lib/highlight.ts +0 -0
- herds-0.1.0/web/lib/platform.ts +164 -0
- herds-0.1.0/web/next-env.d.ts +6 -0
- herds-0.1.0/web/next.config.mjs +10 -0
- herds-0.1.0/web/package.json +31 -0
- herds-0.1.0/web/pnpm-lock.yaml +4292 -0
- herds-0.1.0/web/postcss.config.mjs +6 -0
- herds-0.1.0/web/public/skill.md +67 -0
- herds-0.1.0/web/scripts/gen_map.mjs +39 -0
- herds-0.1.0/web/tailwind.config.ts +51 -0
- 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
|
+

|
|
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
|
+
|  |  |
|
|
227
|
+
| *Per-Mac live gauges* | *Sandboxes — activity + exposed ports* |
|
|
228
|
+
|  | |
|
|
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
|
+

|
|
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
|
+
|  |  |
|
|
190
|
+
| *Per-Mac live gauges* | *Sandboxes — activity + exposed ports* |
|
|
191
|
+
|  | |
|
|
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
|