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:
+19
-1
@@ -47,11 +47,14 @@ const (
|
||||
volumePiCache string = "lvmh-pi-cache" // pi package cache (git:/npm:), shared across spawns
|
||||
cacheMount string = "/root/.pi/agent/cache"
|
||||
authMountTarget string = "/root/.pi/agent/auth.json"
|
||||
modelsMountTarget string = "/root/.pi/agent/models.json"
|
||||
envHostPiAgentDir string = "LVMH_HOST_PI_AGENT_DIR"
|
||||
envHostPiRuntimeDir string = "LVMH_HOST_PI_RUNTIME_DIR"
|
||||
envSecretsDir string = "LVMH_SECRETS_DIR"
|
||||
envCloakCacheDir string = "LVMH_CLOAK_CACHE_DIR"
|
||||
envPlaywrightCacheDir string = "LVMH_PLAYWRIGHT_CACHE_DIR"
|
||||
sshMountTarget string = "/root/.ssh"
|
||||
meshMountTarget string = "/root/.pi/spawn-pi"
|
||||
gitconfigMountTarget string = "/root/.gitconfig"
|
||||
workspaceMount string = "/workspace"
|
||||
sessionsMount string = "/pi-sessions"
|
||||
@@ -573,9 +576,24 @@ func (s *Spawner) createAndStart(ctx context.Context, repo, slug, model string,
|
||||
volumePiCache+":"+cacheMount,
|
||||
)
|
||||
// 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 != "" {
|
||||
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
|
||||
// below makes every playwright-based MCP use it instead of downloading.
|
||||
|
||||
@@ -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) {
|
||||
useFakeGit(t, fakeGitModeOK)
|
||||
f := newFakeDocker()
|
||||
|
||||
Reference in New Issue
Block a user