// MCP server registration. codex's `mcp add` writes into the user's // global config; we use a per-agent stable name so multiple Plexum // agents using codex don't collide. Registration is idempotent and // performed lazily before the first turn that needs it. package runner import ( "context" "fmt" "os" "os/exec" "path/filepath" "strings" "sync" ) var registered sync.Map // agentID → bool (registration done this process) // EnsureCodexMCPRegistered makes sure codex's user config has an MCP // server entry for this agent named plexum-host-. Returns // the entry's tool-name namespace prefix so the caller can build // req.Tools wire shapes if needed. // // The socket path baked into the env is StableSocketPath(agentID) — // the same value the per-turn bridge listens on. func EnsureCodexMCPRegistered(ctx context.Context, codexBinary, pluginBinary, agentID string) (string, error) { name := codexServerName(agentID) if _, ok := registered.Load(name); ok { return name, nil } // Remove first so we don't get "already exists"; ignore error. _ = exec.CommandContext(ctx, codexBinary, "mcp", "remove", name).Run() args := []string{ "mcp", "add", "--env", "PLEXUM_MCP_SOCKET=" + StableSocketPath(agentID), "--env", "PLEXUM_MCP_AGENT_ID=" + agentID, name, "--", pluginBinary, "mcp-host", } cmd := exec.CommandContext(ctx, codexBinary, args...) if out, err := cmd.CombinedOutput(); err != nil { return "", fmt.Errorf("codex mcp add: %w: %s", err, strings.TrimSpace(string(out))) } registered.Store(name, true) return name, nil } // StableSocketPath returns the per-agent unix socket path the bridge // listens on AND codex's mcp-host subprocess dials into. Keeping it // stable lets us register codex's MCP server once and reuse it. func StableSocketPath(agentID string) string { return filepath.Join(os.TempDir(), "plexum-codex-mcp-"+sanitize(agentID)+".sock") } // codexServerName produces the per-agent MCP server name codex stores // in its global config. Names need to be filesystem-safe; we just // reuse the sanitised agent id. func codexServerName(agentID string) string { return "plexum-host-" + sanitize(agentID) } func sanitize(s string) string { out := make([]byte, 0, len(s)) for i := 0; i < len(s); i++ { c := s[i] switch { case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9', c == '-', c == '_': out = append(out, c) default: out = append(out, '_') } } return string(out) }