feat(spawn): share host spawn-pi mesh + models.json with spawned containers

- bind $LVMH_HOST_PI_RUNTIME_DIR/spawn-pi -> /root/.pi/spawn-pi (rw) so
  container pi's join the host pi's node mesh (spawn_pi/list_pi_nodes/
  send_pi_message now reach across the host/container boundary)
- bind host models.json (when present) over the stale image-baked
  fallback so newly added catalog models resolve in spawns
This commit is contained in:
buenosair
2026-09-07 13:21:49 +00:00
parent 6ef27e5cd6
commit 4755f6d1e3
3 changed files with 80 additions and 1 deletions
+19 -1
View File
@@ -47,11 +47,14 @@ const (
volumePiCache string = "lvmh-pi-cache" // pi package cache (git:/npm:), shared across spawns volumePiCache string = "lvmh-pi-cache" // pi package cache (git:/npm:), shared across spawns
cacheMount string = "/root/.pi/agent/cache" cacheMount string = "/root/.pi/agent/cache"
authMountTarget string = "/root/.pi/agent/auth.json" authMountTarget string = "/root/.pi/agent/auth.json"
modelsMountTarget string = "/root/.pi/agent/models.json"
envHostPiAgentDir string = "LVMH_HOST_PI_AGENT_DIR" envHostPiAgentDir string = "LVMH_HOST_PI_AGENT_DIR"
envHostPiRuntimeDir string = "LVMH_HOST_PI_RUNTIME_DIR"
envSecretsDir string = "LVMH_SECRETS_DIR" envSecretsDir string = "LVMH_SECRETS_DIR"
envCloakCacheDir string = "LVMH_CLOAK_CACHE_DIR" envCloakCacheDir string = "LVMH_CLOAK_CACHE_DIR"
envPlaywrightCacheDir string = "LVMH_PLAYWRIGHT_CACHE_DIR" envPlaywrightCacheDir string = "LVMH_PLAYWRIGHT_CACHE_DIR"
sshMountTarget string = "/root/.ssh" sshMountTarget string = "/root/.ssh"
meshMountTarget string = "/root/.pi/spawn-pi"
gitconfigMountTarget string = "/root/.gitconfig" gitconfigMountTarget string = "/root/.gitconfig"
workspaceMount string = "/workspace" workspaceMount string = "/workspace"
sessionsMount string = "/pi-sessions" sessionsMount string = "/pi-sessions"
@@ -573,9 +576,24 @@ func (s *Spawner) createAndStart(ctx context.Context, repo, slug, model string,
volumePiCache+":"+cacheMount, volumePiCache+":"+cacheMount,
) )
// Host pi credentials (OAuth tokens for anthropic etc.), read-only, so // Host pi credentials (OAuth tokens for anthropic etc.), read-only, so
// spawned agents can use every model the catalog offers. // spawned agents can use every model the catalog offers. The host
// models.json rides along when present: image fallbacks are baked at
// build time and go stale the moment the dotfiles catalog changes.
if hostAgent := os.Getenv(envHostPiAgentDir); hostAgent != "" { if hostAgent := os.Getenv(envHostPiAgentDir); hostAgent != "" {
binds = append(binds, hostAgent+"/auth.json:"+authMountTarget+":ro") binds = append(binds, hostAgent+"/auth.json:"+authMountTarget+":ro")
if _, err := os.Stat(filepath.Join(hostAgent, "models.json")); err == nil {
binds = append(binds, hostAgent+"/models.json:"+modelsMountTarget+":ro")
}
}
// Shared spawn-pi mesh directory (node registry + AF_UNIX sockets),
// read-write: each pi creates its own socket and node file. Sharing it
// with the host puts container pi's on the same mesh as the host pi —
// without this each container is an isolated island.
if hostRuntime := os.Getenv(envHostPiRuntimeDir); hostRuntime != "" {
mesh := filepath.Join(hostRuntime, "spawn-pi")
if _, err := os.Stat(mesh); err == nil {
binds = append(binds, mesh+":"+meshMountTarget)
}
} }
// Shared playwright browser cache (host path, read-only); the env var // Shared playwright browser cache (host path, read-only); the env var
// below makes every playwright-based MCP use it instead of downloading. // below makes every playwright-based MCP use it instead of downloading.
+58
View File
@@ -519,6 +519,64 @@ func TestSpawnerSecretsBinds(t *testing.T) {
} }
} }
func TestWorkerBindsHostModelsJSON(t *testing.T) {
useFakeGit(t, fakeGitModeOK)
f := newFakeDocker()
sp, _ := newTestSpawner(t, f)
agent := t.TempDir()
if err := os.WriteFile(filepath.Join(agent, "models.json"), []byte(`{}`), 0o644); err != nil {
t.Fatal(err)
}
t.Setenv(envHostPiAgentDir, agent)
res, err := sp.Start(context.Background(), "group/project", "", "", false)
if err != nil {
t.Fatalf("Start: %v", err)
}
waitJobState(t, sp, res.SessionID, stateRunning)
creates := f.createsByName("lvmh-agent-")
binds := creates[0].HostConfig.Binds
want := agent + "/models.json:/root/.pi/agent/models.json:ro"
ok := false
for _, b := range binds {
if b == want {
ok = true
}
}
if !ok {
t.Fatalf("models.json bind missing from %v", binds)
}
}
func TestWorkerBindsSpawnPiMesh(t *testing.T) {
useFakeGit(t, fakeGitModeOK)
f := newFakeDocker()
sp, _ := newTestSpawner(t, f)
runtimeDir := t.TempDir()
if err := os.MkdirAll(filepath.Join(runtimeDir, "spawn-pi"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv(envHostPiRuntimeDir, runtimeDir)
res, err := sp.Start(context.Background(), "group/project", "", "", false)
if err != nil {
t.Fatalf("Start: %v", err)
}
waitJobState(t, sp, res.SessionID, stateRunning)
creates := f.createsByName("lvmh-agent-")
binds := creates[0].HostConfig.Binds
want := filepath.Join(runtimeDir, "spawn-pi") + ":/root/.pi/spawn-pi"
ok := false
for _, b := range binds {
if b == want {
ok = true
}
}
if !ok {
t.Fatalf("spawn-pi mesh bind missing from %v", binds)
}
}
func TestWorkerBindsCaches(t *testing.T) { func TestWorkerBindsCaches(t *testing.T) {
useFakeGit(t, fakeGitModeOK) useFakeGit(t, fakeGitModeOK)
f := newFakeDocker() f := newFakeDocker()
+3
View File
@@ -19,6 +19,9 @@ services:
# docker.sock bind semantics: bind sources resolve on the HOST, so # docker.sock bind semantics: bind sources resolve on the HOST, so
# this must be the HOST path of the pi config (auth.json etc.). # this must be the HOST path of the pi config (auth.json etc.).
LVMH_HOST_PI_AGENT_DIR: ${LVMH_PI_AGENT_DIR:-/home/alarm/.dotfiles/pi/agent} LVMH_HOST_PI_AGENT_DIR: ${LVMH_PI_AGENT_DIR:-/home/alarm/.dotfiles/pi/agent}
# host pi runtime dir (spawn-pi mesh: nodes + sockets), shared rw with
# spawned containers so host pi and container pi's form one mesh
LVMH_HOST_PI_RUNTIME_DIR: ${LVMH_PI_RUNTIME_DIR:-/home/alarm/.pi}
LVMH_SECRETS_DIR: /zdata/root/lvmh-secrets LVMH_SECRETS_DIR: /zdata/root/lvmh-secrets
LVMH_CLOAK_CACHE_DIR: /home/alarm/.cloakbrowser LVMH_CLOAK_CACHE_DIR: /home/alarm/.cloakbrowser
LVMH_PLAYWRIGHT_CACHE_DIR: /home/alarm/.cache/ms-playwright LVMH_PLAYWRIGHT_CACHE_DIR: /home/alarm/.cache/ms-playwright