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
|
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.
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user